From dad5b331eaee2d4e7f87061b412097c21e98ef81 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 9 Feb 2024 10:07:17 -0600 Subject: [PATCH 001/228] add basics --- src/vs/platform/terminal/common/terminal.ts | 4 + .../terminal/browser/media/terminal.css | 11 ++ .../contrib/terminal/common/terminal.ts | 4 + .../terminal/common/terminalContextKey.ts | 17 ++ .../contrib/terminal/terminal.all.ts | 1 + .../browser/terminal.chat.contribution.ts | 160 ++++++++++++++++++ .../chat/browser/terminalChatWidget.ts | 76 +++++++++ 7 files changed, 273 insertions(+) create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index ffe0e56a189..56ebf9ddae5 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -121,6 +121,10 @@ export const enum TerminalSettingId { StickyScrollEnabled = 'terminal.integrated.stickyScroll.enabled', StickyScrollMaxLineCount = 'terminal.integrated.stickyScroll.maxLineCount', MouseWheelZoom = 'terminal.integrated.mouseWheelZoom', + FocusChat = 'workbench.action.terminal.focusChat', + HideChat = 'workbench.action.terminal.hideChat', + SubmitChat = 'workbench.action.terminal.submitChat', + CancelChat = 'workbench.action.terminal.cancelChat', // Debug settings that are hidden from user diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 488815cf258..a6ea80d22d6 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -565,3 +565,14 @@ .monaco-workbench .xterm.terminal.hide { visibility: hidden; } + +.monaco-workbench .terminal-chat-widget { + z-index: 33 !important; + position: absolute; + bottom: 30px; + left: 10px; +} + +.monaco-workbench .terminal-chat-widget.hide { + visibility: hidden; +} diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index fc925b646bb..0579628b521 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -500,6 +500,10 @@ export const enum TerminalCommandId { FontZoomIn = 'workbench.action.terminal.fontZoomIn', FontZoomOut = 'workbench.action.terminal.fontZoomOut', FontZoomReset = 'workbench.action.terminal.fontZoomReset', + FocusChat = 'workbench.action.terminal.focusChat', + HideChat = 'workbench.action.terminal.hideChat', + SubmitChat = 'workbench.action.terminal.submitChat', + CancelChat = 'workbench.action.terminal.cancelChat', // Developer commands diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 72335480aee..1de53d61930 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -39,6 +39,10 @@ export const enum TerminalContextKeyStrings { ShellType = 'terminalShellType', InTerminalRunCommandPicker = 'inTerminalRunCommandPicker', TerminalShellIntegrationEnabled = 'terminalShellIntegrationEnabled', + ChatFocus = 'terminalChatFocus', + ChatVisible = 'terminalChatVisible', + ChatSessionInProgress = 'terminalChatSessionInProgress', + ChatInputHasText = 'terminalChatInputHasText', } export namespace TerminalContextKeys { @@ -158,4 +162,17 @@ export namespace TerminalContextKeys { ContextKeyExpr.equals(`config.${TerminalSettingId.TabsShowActions}`, 'always') ) ); + + + /** Whether the chat widget is focused */ + export const chatFocused = new RawContextKey(TerminalContextKeyStrings.ChatFocus, false, localize('chatFocusedContextKey', "Whether the chat view is focused.")); + + /** Whether the chat widget is visible */ + export const chatVisible = new RawContextKey(TerminalContextKeyStrings.ChatVisible, false, localize('chatVisibleContextKey', "Whether the chat view is visible.")); + + /** Whether a chat session is in progress */ + export const chatSessionInProgress = new RawContextKey(TerminalContextKeyStrings.ChatSessionInProgress, false, localize('chatSessionInProgressContextKey', "Whether a chat session is in progress.")); + + /** Whether the chat input has text */ + export const chatInputHasText = new RawContextKey(TerminalContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); } diff --git a/src/vs/workbench/contrib/terminal/terminal.all.ts b/src/vs/workbench/contrib/terminal/terminal.all.ts index b49aa829f7b..e9cdc253212 100644 --- a/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -17,6 +17,7 @@ import 'vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.acce import 'vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution'; import 'vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution'; import 'vs/workbench/contrib/terminalContrib/find/browser/terminal.find.contribution'; +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution'; import 'vs/workbench/contrib/terminalContrib/highlight/browser/terminal.highlight.contribution'; import 'vs/workbench/contrib/terminalContrib/links/browser/terminal.links.contribution'; import 'vs/workbench/contrib/terminalContrib/zoom/browser/terminal.zoom.contribution'; 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 new file mode 100644 index 00000000000..bb9cbd24540 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.chat.contribution.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDimension } from 'vs/base/browser/dom'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { localize2 } from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; +import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { ITerminalProcessInfo, ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; +import { Codicon } from 'vs/base/common/codicons'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; + +export class TerminalChatContribution extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.Chat'; + + /** + * Currently focused Chat widget. This is used to track action context since + * 'active terminals' are only tracked for non-detached terminal instanecs. + */ + static activeChatWidget?: TerminalChatContribution; + + static get(instance: ITerminalInstance | IDetachedTerminalInstance): TerminalChatContribution | null { + return instance.getContribution(TerminalChatContribution.ID); + } + + private _chatWidget: Lazy | undefined; + private _lastLayoutDimensions: IDimension | undefined; + + get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } + + constructor( + private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, + processManager: ITerminalProcessManager | ITerminalProcessInfo, + widgetManager: TerminalWidgetManager, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ITerminalService private readonly _terminalService: ITerminalService + ) { + super(); + } + + layout(_xterm: IXtermTerminal & { raw: RawXtermTerminal }, dimension: IDimension): void { + this._lastLayoutDimensions = dimension; + this._chatWidget?.rawValue?.layout(dimension.width); + } + + xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + this._chatWidget = new Lazy(() => { + const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); + + // Track focus and set state so we can force the scroll bar to be visible + chatWidget.onDidFocus(() => { + TerminalChatContribution.activeChatWidget = this; + this._instance.forceScrollbarVisibility(); + if (!isDetachedTerminalInstance(this._instance)) { + this._terminalService.setActiveInstance(this._instance); + } + }); + // chatWidget.onDidBlur(() => { + // TerminalChatContribution.activeChatWidget = undefined; + // this._instance.resetScrollbarVisibility(); + // }); + + if (!this._instance.domElement) { + throw new Error('FindWidget expected terminal DOM to be initialized'); + } + + // this._instance.domElement?.appendChild(chatWidget.getDomNode()); + if (this._lastLayoutDimensions) { + chatWidget.layout(this._lastLayoutDimensions.width); + } + + return chatWidget; + }); + } + + override dispose() { + if (TerminalChatContribution.activeChatWidget === this) { + TerminalChatContribution.activeChatWidget = undefined; + } + super.dispose(); + this._chatWidget?.rawValue?.dispose(); + } +} +registerTerminalContribution(TerminalChatContribution.ID, TerminalChatContribution, true); + +registerActiveXtermAction({ + id: TerminalCommandId.FocusChat, + title: localize2('workbench.action.terminal.focusChat', 'Terminal: Focus Chat'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.KeyI, + when: ContextKeyExpr.or(TerminalContextKeys.chatFocused, TerminalContextKeys.focusInAny), + weight: KeybindingWeight.WorkbenchContrib + }, + f1: true, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + run: (_xterm, _accessor, activeInstance) => { + const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + contr?.chatWidget?.reveal(); + } +}); + +registerActiveXtermAction({ + id: TerminalCommandId.HideChat, + title: localize2('workbench.action.terminal.hideChat', 'Terminal: Hide Chat'), + keybinding: { + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(TerminalContextKeys.chatFocused, TerminalContextKeys.chatVisible), + weight: KeybindingWeight.WorkbenchContrib + }, + f1: true, + precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + run: (_xterm, _accessor, activeInstance) => { + const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + contr?.chatWidget?.hide(); + } +}); + +registerActiveXtermAction({ + id: TerminalCommandId.SubmitChat, + title: localize2('workbench.action.terminal.submitChat', 'Terminal: Submit Chat'), + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.chatInputHasText), + icon: Codicon.send, + menu: { + id: MenuId.ChatExecute, + when: TerminalContextKeys.chatSessionInProgress.negate(), + group: 'navigation', + }, + run: (_xterm, _accessor, activeInstance) => { + const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + contr?.chatWidget?.acceptInput(); + } +}); + +registerActiveXtermAction({ + id: TerminalCommandId.CancelChat, + title: localize2('workbench.action.terminal.cancelChat', 'Cancel Chat'), + precondition: TerminalContextKeys.chatSessionInProgress, + icon: Codicon.debugStop, + menu: { + id: MenuId.ChatExecute, + group: 'navigation', + }, + run: (_xterm, _accessor, activeInstance) => { + const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + contr?.chatWidget?.cancel(); + } +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts new file mode 100644 index 00000000000..5b44b0a0cf7 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.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. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; +import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { IDetachedTerminalInstance, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; + +export class TerminalChatWidget extends Disposable { + private _widget: ChatWidget | undefined; + private _scopedInstantiationService: IInstantiationService; + private readonly _onDidFocus = this._register(new Emitter()); + readonly onDidFocus = this._onDidFocus.event; + private _widgetContainer: HTMLElement | undefined; + private _chatWidgetFocused: IContextKey; + private _chatWidgetVisible: IContextKey; + constructor( + private readonly _container: HTMLElement, + private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, + + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService) { + super(); + const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); + this._scopedInstantiationService = this._instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); + this._chatWidgetFocused = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); + this._chatWidgetVisible = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); + } + reveal(): void { + this._widgetContainer = document.createElement('div'); + this._widgetContainer.classList.add('terminal-chat-widget'); + this._widget = this._register(this._scopedInstantiationService.createInstance( + ChatWidget, + { viewId: 'terminal' }, + { supportsFileReferences: false, renderStyle: 'compact' }, + { + listForeground: editorForeground, + listBackground: editorBackground, + inputEditorBackground: inputBackground, + resultEditorBackground: editorBackground + })); + this._widget.render(this._widgetContainer); + this._container.appendChild(this._widgetContainer); + this._register(this._widget.onDidFocus(() => { + this._onDidFocus.fire(); + this._chatWidgetFocused.set(true); + })); + this._widget.setVisible(true); + this._chatWidgetVisible.set(true); + this._widget.setInput('@terminal'); + this._widget.setInputPlaceholder('Request a terminal command'); + this._widget.focusInput(); + } + hide(): void { + if (this._widgetContainer) { + this._container.removeChild(this._widgetContainer); + } + this._chatWidgetVisible.set(false); + } + cancel(): void { + this._widget?.clear(); + } + acceptInput(): void { + this._widget?.acceptInput(); + } + layout(width: number): void { + this._widget?.layout(40, width); + } +} From 4f219d7a161c595d66dce6e376aa77f6703a5158 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 9 Feb 2024 10:11:24 -0600 Subject: [PATCH 002/228] fix some issues --- .../browser/terminal.chat.contribution.ts | 36 +++---------------- .../chat/browser/terminalChatWidget.ts | 5 +-- 2 files changed, 6 insertions(+), 35 deletions(-) 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 bb9cbd24540..0f803200129 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 @@ -11,7 +11,7 @@ import { localize2 } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; @@ -25,12 +25,6 @@ import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/br export class TerminalChatContribution extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; - /** - * Currently focused Chat widget. This is used to track action context since - * 'active terminals' are only tracked for non-detached terminal instanecs. - */ - static activeChatWidget?: TerminalChatContribution; - static get(instance: ITerminalInstance | IDetachedTerminalInstance): TerminalChatContribution | null { return instance.getContribution(TerminalChatContribution.ID); } @@ -59,19 +53,6 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon this._chatWidget = new Lazy(() => { const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); - // Track focus and set state so we can force the scroll bar to be visible - chatWidget.onDidFocus(() => { - TerminalChatContribution.activeChatWidget = this; - this._instance.forceScrollbarVisibility(); - if (!isDetachedTerminalInstance(this._instance)) { - this._terminalService.setActiveInstance(this._instance); - } - }); - // chatWidget.onDidBlur(() => { - // TerminalChatContribution.activeChatWidget = undefined; - // this._instance.resetScrollbarVisibility(); - // }); - if (!this._instance.domElement) { throw new Error('FindWidget expected terminal DOM to be initialized'); } @@ -86,9 +67,6 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon } override dispose() { - if (TerminalChatContribution.activeChatWidget === this) { - TerminalChatContribution.activeChatWidget = undefined; - } super.dispose(); this._chatWidget?.rawValue?.dispose(); } @@ -106,8 +84,7 @@ registerActiveXtermAction({ f1: true, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), run: (_xterm, _accessor, activeInstance) => { - const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); - contr?.chatWidget?.reveal(); + TerminalChatContribution.get(activeInstance)?.chatWidget?.reveal(); } }); @@ -123,8 +100,7 @@ registerActiveXtermAction({ f1: true, precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), run: (_xterm, _accessor, activeInstance) => { - const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); - contr?.chatWidget?.hide(); + TerminalChatContribution.get(activeInstance)?.chatWidget?.hide(); } }); @@ -139,8 +115,7 @@ registerActiveXtermAction({ group: 'navigation', }, run: (_xterm, _accessor, activeInstance) => { - const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); - contr?.chatWidget?.acceptInput(); + TerminalChatContribution.get(activeInstance)?.chatWidget?.acceptInput(); } }); @@ -154,7 +129,6 @@ registerActiveXtermAction({ group: 'navigation', }, run: (_xterm, _accessor, activeInstance) => { - const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); - contr?.chatWidget?.cancel(); + TerminalChatContribution.get(activeInstance)?.chatWidget?.cancel(); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 5b44b0a0cf7..b6dbcfa67ee 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -16,8 +15,6 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin export class TerminalChatWidget extends Disposable { private _widget: ChatWidget | undefined; private _scopedInstantiationService: IInstantiationService; - private readonly _onDidFocus = this._register(new Emitter()); - readonly onDidFocus = this._onDidFocus.event; private _widgetContainer: HTMLElement | undefined; private _chatWidgetFocused: IContextKey; private _chatWidgetVisible: IContextKey; @@ -49,7 +46,6 @@ export class TerminalChatWidget extends Disposable { this._widget.render(this._widgetContainer); this._container.appendChild(this._widgetContainer); this._register(this._widget.onDidFocus(() => { - this._onDidFocus.fire(); this._chatWidgetFocused.set(true); })); this._widget.setVisible(true); @@ -63,6 +59,7 @@ export class TerminalChatWidget extends Disposable { this._container.removeChild(this._widgetContainer); } this._chatWidgetVisible.set(false); + this._instance.focus(); } cancel(): void { this._widget?.clear(); From ab4a6959f7298fbd463daf364309350e16c40812 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 9 Feb 2024 10:44:29 -0600 Subject: [PATCH 003/228] add hold for speech --- .../contrib/inlineChat/electron-sandbox/inlineChatActions.ts | 5 +++-- .../chat/browser/terminal.chat.contribution.ts | 4 ++-- .../terminalContrib/chat/browser/terminalChatWidget.ts | 4 +--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts index d30d230a2b7..ec8c7cd1c47 100644 --- a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts @@ -20,16 +20,17 @@ import { HasSpeechProvider, ISpeechService } from 'vs/workbench/contrib/speech/c import { localize2 } from 'vs/nls'; import { Action2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; export class HoldToSpeak extends AbstractInlineChatAction { constructor() { super({ id: 'inlineChat.holdForSpeech', - precondition: ContextKeyExpr.and(HasSpeechProvider, CTX_INLINE_CHAT_VISIBLE), + precondition: ContextKeyExpr.and(HasSpeechProvider, ContextKeyExpr.or(CTX_INLINE_CHAT_VISIBLE, TerminalContextKeys.chatVisible)), title: localize2('holdForSpeech', "Hold for Speech"), keybinding: { - when: EditorContextKeys.textInputFocus, + when: ContextKeyExpr.or(EditorContextKeys.textInputFocus, TerminalContextKeys.chatFocused), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyI, }, 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 0f803200129..46bef94963c 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 @@ -39,7 +39,7 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon processManager: ITerminalProcessManager | ITerminalProcessInfo, widgetManager: TerminalWidgetManager, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ITerminalService private readonly _terminalService: ITerminalService + @ITerminalService terminalService: ITerminalService ) { super(); } @@ -78,7 +78,7 @@ registerActiveXtermAction({ title: localize2('workbench.action.terminal.focusChat', 'Terminal: Focus Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, - when: ContextKeyExpr.or(TerminalContextKeys.chatFocused, TerminalContextKeys.focusInAny), + when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), weight: KeybindingWeight.WorkbenchContrib }, f1: true, diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index b6dbcfa67ee..cb2db4bb1be 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -45,9 +45,7 @@ export class TerminalChatWidget extends Disposable { })); this._widget.render(this._widgetContainer); this._container.appendChild(this._widgetContainer); - this._register(this._widget.onDidFocus(() => { - this._chatWidgetFocused.set(true); - })); + this._register(this._widget.onDidFocus(() => this._chatWidgetFocused.set(true))); this._widget.setVisible(true); this._chatWidgetVisible.set(true); this._widget.setInput('@terminal'); From dc4ebd856d49a1ae73b93214f635d755a1f0ad58 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 9 Feb 2024 11:04:23 -0600 Subject: [PATCH 004/228] fix some issues --- .../terminal/browser/media/terminal.css | 3 +-- .../chat/browser/terminalChatWidget.ts | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index a6ea80d22d6..8fc9e6be0eb 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -569,8 +569,7 @@ .monaco-workbench .terminal-chat-widget { z-index: 33 !important; position: absolute; - bottom: 30px; - left: 10px; + top: 10px; } .monaco-workbench .terminal-chat-widget.hide { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index cb2db4bb1be..4f0c787fc6b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -13,9 +13,9 @@ import { IDetachedTerminalInstance, ITerminalInstance } from 'vs/workbench/contr import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; export class TerminalChatWidget extends Disposable { - private _widget: ChatWidget | undefined; + private _widget: ChatWidget; private _scopedInstantiationService: IInstantiationService; - private _widgetContainer: HTMLElement | undefined; + private _widgetContainer: HTMLElement; private _chatWidgetFocused: IContextKey; private _chatWidgetVisible: IContextKey; constructor( @@ -29,10 +29,9 @@ export class TerminalChatWidget extends Disposable { this._scopedInstantiationService = this._instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); this._chatWidgetFocused = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); this._chatWidgetVisible = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); - } - reveal(): void { this._widgetContainer = document.createElement('div'); this._widgetContainer.classList.add('terminal-chat-widget'); + this._container.appendChild(this._widgetContainer); this._widget = this._register(this._scopedInstantiationService.createInstance( ChatWidget, { viewId: 'terminal' }, @@ -44,19 +43,22 @@ export class TerminalChatWidget extends Disposable { resultEditorBackground: editorBackground })); this._widget.render(this._widgetContainer); - this._container.appendChild(this._widgetContainer); this._register(this._widget.onDidFocus(() => this._chatWidgetFocused.set(true))); + } + reveal(): void { + this._widgetContainer.classList.remove('hide'); this._widget.setVisible(true); + this._chatWidgetFocused.set(true); this._chatWidgetVisible.set(true); this._widget.setInput('@terminal'); this._widget.setInputPlaceholder('Request a terminal command'); this._widget.focusInput(); } hide(): void { - if (this._widgetContainer) { - this._container.removeChild(this._widgetContainer); - } + this._widgetContainer.classList.add('hide'); + this._chatWidgetFocused.set(false); this._chatWidgetVisible.set(false); + this._widget.clear(); this._instance.focus(); } cancel(): void { @@ -66,6 +68,6 @@ export class TerminalChatWidget extends Disposable { this._widget?.acceptInput(); } layout(width: number): void { - this._widget?.layout(40, width); + this._widget?.layout(100, width < 300 ? 300 : width); } } From eedd668787628d52ff190f60a52a6f49522500b6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 09:23:24 -0800 Subject: [PATCH 005/228] Add setting to disable term inline chat by default --- src/vs/platform/terminal/common/terminal.ts | 5 +-- .../terminal/common/terminalConfiguration.ts | 5 +++ .../browser/terminal.chat.contribution.ts | 33 ++++++++++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 56ebf9ddae5..6d291481c7f 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -121,10 +121,7 @@ export const enum TerminalSettingId { StickyScrollEnabled = 'terminal.integrated.stickyScroll.enabled', StickyScrollMaxLineCount = 'terminal.integrated.stickyScroll.maxLineCount', MouseWheelZoom = 'terminal.integrated.mouseWheelZoom', - FocusChat = 'workbench.action.terminal.focusChat', - HideChat = 'workbench.action.terminal.hideChat', - SubmitChat = 'workbench.action.terminal.submitChat', - CancelChat = 'workbench.action.terminal.cancelChat', + ExperimentalInlineChat = 'workbench.action.terminal.experimentalInlineChat', // Debug settings that are hidden from user diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index ac0903b8ae3..397ff3bc97a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -653,6 +653,11 @@ const terminalConfiguration: IConfigurationNode = { type: 'boolean', default: false }, + [TerminalSettingId.ExperimentalInlineChat]: { + markdownDescription: localize('terminal.integrated.experimentalInlineChat', "Whether to enable the upcoming experimental inline terminal chat UI."), + type: 'boolean', + default: false + } } }; 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 46bef94963c..c0ee3d62f1b 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 @@ -21,6 +21,8 @@ import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { Codicon } from 'vs/base/common/codicons'; import { MenuId } from 'vs/platform/actions/common/actions'; import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class TerminalChatContribution extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; @@ -39,17 +41,27 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon processManager: ITerminalProcessManager | ITerminalProcessInfo, widgetManager: TerminalWidgetManager, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private _configurationService: IConfigurationService, @ITerminalService terminalService: ITerminalService ) { super(); + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } } layout(_xterm: IXtermTerminal & { raw: RawXtermTerminal }, dimension: IDimension): void { + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } this._lastLayoutDimensions = dimension; this._chatWidget?.rawValue?.layout(dimension.width); } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } this._chatWidget = new Lazy(() => { const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); @@ -82,7 +94,10 @@ registerActiveXtermAction({ weight: KeybindingWeight.WorkbenchContrib }, f1: true, - precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + ), run: (_xterm, _accessor, activeInstance) => { TerminalChatContribution.get(activeInstance)?.chatWidget?.reveal(); } @@ -98,7 +113,10 @@ registerActiveXtermAction({ weight: KeybindingWeight.WorkbenchContrib }, f1: true, - precondition: ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + ), run: (_xterm, _accessor, activeInstance) => { TerminalChatContribution.get(activeInstance)?.chatWidget?.hide(); } @@ -107,7 +125,11 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalCommandId.SubmitChat, title: localize2('workbench.action.terminal.submitChat', 'Terminal: Submit Chat'), - precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.chatInputHasText), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalContextKeys.chatInputHasText + ), icon: Codicon.send, menu: { id: MenuId.ChatExecute, @@ -122,7 +144,10 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalCommandId.CancelChat, title: localize2('workbench.action.terminal.cancelChat', 'Cancel Chat'), - precondition: TerminalContextKeys.chatSessionInProgress, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalContextKeys.chatSessionInProgress, + ), icon: Codicon.debugStop, menu: { id: MenuId.ChatExecute, From 0d202eff3d87ae7cb3e943fb82fa9a98f6aeffcd Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 9 Feb 2024 11:46:48 -0600 Subject: [PATCH 006/228] widget per terminal --- .../browser/terminal.chat.contribution.ts | 33 ++++++++++++++----- .../chat/browser/terminalChatWidget.ts | 6 ++++ 2 files changed, 31 insertions(+), 8 deletions(-) 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 c0ee3d62f1b..b9fab74a3a2 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 @@ -11,7 +11,7 @@ import { localize2 } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; @@ -30,7 +30,11 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon static get(instance: ITerminalInstance | IDetachedTerminalInstance): TerminalChatContribution | null { return instance.getContribution(TerminalChatContribution.ID); } - + /** + * Currently focused chat widget. This is used to track action context since + * 'active terminals' are only tracked for non-detached terminal instanecs. + */ + static activeChatWidget?: TerminalChatContribution; private _chatWidget: Lazy | undefined; private _lastLayoutDimensions: IDimension | undefined; @@ -42,7 +46,7 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon widgetManager: TerminalWidgetManager, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService private _configurationService: IConfigurationService, - @ITerminalService terminalService: ITerminalService + @ITerminalService private readonly _terminalService: ITerminalService ) { super(); if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { @@ -64,7 +68,16 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon } this._chatWidget = new Lazy(() => { const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); - + chatWidget.focusTracker.onDidFocus(() => { + TerminalChatContribution.activeChatWidget = this; + if (!isDetachedTerminalInstance(this._instance)) { + this._terminalService.setActiveInstance(this._instance); + } + }); + chatWidget.focusTracker.onDidBlur(() => { + TerminalChatContribution.activeChatWidget = undefined; + this._instance.resetScrollbarVisibility(); + }); if (!this._instance.domElement) { throw new Error('FindWidget expected terminal DOM to be initialized'); } @@ -99,7 +112,8 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), ), run: (_xterm, _accessor, activeInstance) => { - TerminalChatContribution.get(activeInstance)?.chatWidget?.reveal(); + const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + contr?.chatWidget?.reveal(); } }); @@ -118,7 +132,8 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), ), run: (_xterm, _accessor, activeInstance) => { - TerminalChatContribution.get(activeInstance)?.chatWidget?.hide(); + const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + contr?.chatWidget?.hide(); } }); @@ -137,7 +152,8 @@ registerActiveXtermAction({ group: 'navigation', }, run: (_xterm, _accessor, activeInstance) => { - TerminalChatContribution.get(activeInstance)?.chatWidget?.acceptInput(); + const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + contr?.chatWidget?.acceptInput(); } }); @@ -154,6 +170,7 @@ registerActiveXtermAction({ group: 'navigation', }, run: (_xterm, _accessor, activeInstance) => { - TerminalChatContribution.get(activeInstance)?.chatWidget?.cancel(); + const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + contr?.chatWidget?.cancel(); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 4f0c787fc6b..de48c32fcf3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IFocusTracker, trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -18,6 +19,7 @@ export class TerminalChatWidget extends Disposable { private _widgetContainer: HTMLElement; private _chatWidgetFocused: IContextKey; private _chatWidgetVisible: IContextKey; + private readonly _focusTracker: IFocusTracker; constructor( private readonly _container: HTMLElement, private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, @@ -44,6 +46,7 @@ export class TerminalChatWidget extends Disposable { })); this._widget.render(this._widgetContainer); this._register(this._widget.onDidFocus(() => this._chatWidgetFocused.set(true))); + this._focusTracker = this._register(trackFocus(this._widgetContainer)); } reveal(): void { this._widgetContainer.classList.remove('hide'); @@ -70,4 +73,7 @@ export class TerminalChatWidget extends Disposable { layout(width: number): void { this._widget?.layout(100, width < 300 ? 300 : width); } + public get focusTracker(): IFocusTracker { + return this._focusTracker; + } } From 10e7518fbe0d4e9da230165470ed87558a70b86d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 09:57:23 -0800 Subject: [PATCH 007/228] WIP inline chat widget integration --- .../chat/browser/media/chat.css | 13 ++++ .../browser/terminal.chat.contribution.ts | 1 + .../chat/browser/terminalChatWidget.ts | 72 +++++++++++++------ 3 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/media/chat.css diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/chat.css new file mode 100644 index 00000000000..e81dc6b4752 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/chat.css @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.terminal-inline-chat { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 100; +} 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 c0ee3d62f1b..8881c8f664a 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,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/chat'; import { IDimension } from 'vs/base/browser/dom'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 4f0c787fc6b..76482c5c987 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -4,61 +4,91 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; import { IDetachedTerminalInstance, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; export class TerminalChatWidget extends Disposable { - private _widget: ChatWidget; + private _widget!: ChatWidget; private _scopedInstantiationService: IInstantiationService; private _widgetContainer: HTMLElement; private _chatWidgetFocused: IContextKey; private _chatWidgetVisible: IContextKey; + + private readonly _inlineChatWidget: InlineChatWidget; + constructor( private readonly _container: HTMLElement, private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService) { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); - this._scopedInstantiationService = this._instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); + this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); this._chatWidgetFocused = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); this._chatWidgetVisible = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); this._widgetContainer = document.createElement('div'); - this._widgetContainer.classList.add('terminal-chat-widget'); + this._widgetContainer.classList.add('terminal-inline-chat'); this._container.appendChild(this._widgetContainer); - this._widget = this._register(this._scopedInstantiationService.createInstance( - ChatWidget, - { viewId: 'terminal' }, - { supportsFileReferences: false, renderStyle: 'compact' }, + // this._widget = this._register(this._scopedInstantiationService.createInstance( + // ChatWidget, + // { viewId: 'terminal' }, + // { supportsFileReferences: false, renderStyle: 'compact' }, + // { + // listForeground: editorForeground, + // listBackground: editorBackground, + // inputEditorBackground: inputBackground, + // resultEditorBackground: editorBackground + // })); + // this._widget.render(this._widgetContainer); + // this._register(this._widget.onDidFocus(() => this._chatWidgetFocused.set(true))); + + const fakeParentEditorElement = document.createElement('div'); + + // const editorConstructionOptions = this.inputEditorOptions.getEditorConstructionOptions(); + // this.setPlaceholderFontStyles(editorConstructionOptions.fontFamily!, editorConstructionOptions.fontSize!, editorConstructionOptions.lineHeight!); + + const fakeParentEditor = this._scopedInstantiationService.createInstance( + CodeEditorWidget, + fakeParentEditorElement, + {}, + { isSimpleWidget: true } + ); + + this._inlineChatWidget = this._scopedInstantiationService.createInstance( + InlineChatWidget, + fakeParentEditor, { - listForeground: editorForeground, - listBackground: editorBackground, - inputEditorBackground: inputBackground, - resultEditorBackground: editorBackground - })); - this._widget.render(this._widgetContainer); - this._register(this._widget.onDidFocus(() => this._chatWidgetFocused.set(true))); + menuId: MENU_CELL_CHAT_INPUT, + widgetMenuId: MENU_CELL_CHAT_WIDGET, + statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, + feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK + } + ); + + this._widgetContainer.appendChild(this._inlineChatWidget.domNode); } reveal(): void { this._widgetContainer.classList.remove('hide'); - this._widget.setVisible(true); + // this._widget.setVisible(true); this._chatWidgetFocused.set(true); this._chatWidgetVisible.set(true); - this._widget.setInput('@terminal'); - this._widget.setInputPlaceholder('Request a terminal command'); - this._widget.focusInput(); + // this._widget.setInput('@terminal'); + // this._widget.setInputPlaceholder('Request a terminal command'); + // this._widget.focusInput(); } hide(): void { this._widgetContainer.classList.add('hide'); this._chatWidgetFocused.set(false); this._chatWidgetVisible.set(false); - this._widget.clear(); + // this._widget.clear(); this._instance.focus(); } cancel(): void { From 73e7e46e8056b36e74118d23304d65c8c959819a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:17:35 -0800 Subject: [PATCH 008/228] More styled inline chat --- .../contrib/inlineChat/browser/inlineChat.css | 150 +++++++++--------- .../{chat.css => terminalChatWidget.css} | 5 +- .../browser/terminal.chat.contribution.ts | 1 - .../chat/browser/terminalChatWidget.ts | 13 +- 4 files changed, 85 insertions(+), 84 deletions(-) rename src/vs/workbench/contrib/terminalContrib/chat/browser/media/{chat.css => terminalChatWidget.css} (86%) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index ee46f6df25e..862f0183354 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -11,7 +11,7 @@ background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat { +.inline-chat { color: inherit; padding: 6px; margin-top: 6px; @@ -23,11 +23,11 @@ /* body */ -.monaco-editor .inline-chat .body { +.inline-chat .body { display: flex; } -.monaco-editor .inline-chat .body .content { +.inline-chat .body .content { display: flex; box-sizing: border-box; outline: 1px solid var(--vscode-inlineChatInput-border); @@ -35,11 +35,11 @@ border-radius: 2px; } -.monaco-editor .inline-chat .body .content.synthetic-focus { +.inline-chat .body .content.synthetic-focus { outline: 1px solid var(--vscode-inlineChatInput-focusBorder); } -.monaco-editor .inline-chat .body .content .input { +.inline-chat .body .content .input { display: flex; align-items: center; justify-content: space-between; @@ -48,11 +48,11 @@ cursor: text; } -.monaco-editor .inline-chat .body .content .input .monaco-editor-background { +.inline-chat .body .content .input .monaco-editor-background { background-color: var(--vscode-inlineChatInput-background); } -.monaco-editor .inline-chat .body .content .input .editor-placeholder { +.inline-chat .body .content .input .editor-placeholder { position: absolute; z-index: 1; color: var(--vscode-inlineChatInput-placeholderForeground); @@ -61,14 +61,14 @@ text-overflow: ellipsis; } -.monaco-editor .inline-chat .body .content .input .editor-placeholder.hidden { +.inline-chat .body .content .input .editor-placeholder.hidden { display: none; } -.monaco-editor .inline-chat .body .content .input .editor-container { +.inline-chat .body .content .input .editor-container { vertical-align: middle; } -.monaco-editor .inline-chat .body .toolbar { +.inline-chat .body .toolbar { display: flex; flex-direction: column; align-self: stretch; @@ -78,47 +78,47 @@ background: var(--vscode-inlineChatInput-background); } -.monaco-editor .inline-chat .body .toolbar .actions-container { +.inline-chat .body .toolbar .actions-container { display: flex; flex-direction: row; gap: 4px; } -.monaco-editor .inline-chat .body > .widget-toolbar { +.inline-chat .body > .widget-toolbar { padding-left: 4px; } /* progress bit */ -.monaco-editor .inline-chat .progress { +.inline-chat .progress { position: relative; width: calc(100% - 18px); left: 19px; } /* UGLY - fighting against workbench styles */ -.monaco-workbench .part.editor > .content .monaco-editor .inline-chat .progress .monaco-progress-container { +.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { top: 0; } /* status */ -.monaco-editor .inline-chat .status { +.inline-chat .status { margin-top: 4px; display: flex; justify-content: space-between; align-items: center; } -.monaco-editor .inline-chat .status.actions { +.inline-chat .status.actions { margin-top: 4px; } -.monaco-editor .inline-chat .status .actions.hidden { +.inline-chat .status .actions.hidden { display: none; } -.monaco-editor .inline-chat .status .label { +.inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); font-size: 11px; @@ -126,60 +126,60 @@ display: inline-flex; } -.monaco-editor .inline-chat .status .label.hidden { +.inline-chat .status .label.hidden { display: none; } -.monaco-editor .inline-chat .status .label.info { +.inline-chat .status .label.info { margin-right: auto; padding-left: 2px; } -.monaco-editor .inline-chat .status .label.info > .codicon { +.inline-chat .status .label.info > .codicon { padding: 0 5px; font-size: 12px; line-height: 18px; } -.monaco-editor .inline-chat .status .label.status { +.inline-chat .status .label.status { padding-left: 10px; padding-right: 4px; margin-left: auto; } -.monaco-editor .inline-chat .status .label .slash-command-pill CODE { +.inline-chat .status .label .slash-command-pill CODE { border-radius: 3px; padding: 0 1px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); } -.monaco-editor .inline-chat .detectedIntent { +.inline-chat .detectedIntent { color: var(--vscode-descriptionForeground); padding: 5px 0px 5px 5px; } -.monaco-editor .inline-chat .detectedIntent.hidden { +.inline-chat .detectedIntent.hidden { display: none; } -.monaco-editor .inline-chat .detectedIntent .slash-command-pill CODE { +.inline-chat .detectedIntent .slash-command-pill CODE { border-radius: 3px; padding: 0 1px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); } -.monaco-editor .inline-chat .detectedIntent .slash-command-pill a { +.inline-chat .detectedIntent .slash-command-pill a { color: var(--vscode-textLink-foreground); cursor: pointer; } -/* .monaco-editor .inline-chat .markdownMessage .message * { +/* .inline-chat .markdownMessage .message * { margin: unset; } -.monaco-editor .inline-chat .markdownMessage .message code { +.inline-chat .markdownMessage .message code { font-family: var(--monaco-monospace-font); font-size: 12px; color: var(--vscode-textPreformat-foreground); @@ -189,7 +189,7 @@ } */ -.monaco-editor .inline-chat .chatMessage .chatMessageContent .value { +.inline-chat .chatMessage .chatMessageContent .value { -webkit-line-clamp: initial; -webkit-box-orient: vertical; overflow: hidden; @@ -198,130 +198,130 @@ user-select: text; } -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="cropped"] .value { +.inline-chat .chatMessage .chatMessageContent[state="cropped"] .value { -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); } -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="expanded"] .value { +.inline-chat .chatMessage .chatMessageContent[state="expanded"] .value { -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); } -.monaco-editor .inline-chat .followUps { +.inline-chat .followUps { padding: 5px 5px; } -.monaco-editor .inline-chat .followUps .interactive-session-followups .monaco-button { +.inline-chat .followUps .interactive-session-followups .monaco-button { display: block; color: var(--vscode-textLink-foreground); font-size: 12px; } -.monaco-editor .inline-chat .followUps.hidden { +.inline-chat .followUps.hidden { display: none; } -.monaco-editor .inline-chat .chatMessage { +.inline-chat .chatMessage { padding: 8px 3px; } -.monaco-editor .inline-chat .chatMessage .chatMessageContent { +.inline-chat .chatMessage .chatMessageContent { padding: 2px 2px; } -.monaco-editor .inline-chat .chatMessage.hidden { +.inline-chat .chatMessage.hidden { display: none; } -.monaco-editor .inline-chat .status .label A { +.inline-chat .status .label A { color: var(--vscode-textLink-foreground); cursor: pointer; } -.monaco-editor .inline-chat .status .label.error { +.inline-chat .status .label.error { color: var(--vscode-errorForeground); } -.monaco-editor .inline-chat .status .label.warn { +.inline-chat .status .label.warn { color: var(--vscode-editorWarning-foreground); } -.monaco-editor .inline-chat .status .actions { +.inline-chat .status .actions { display: flex; } -.monaco-editor .inline-chat .status .actions > .monaco-button, -.monaco-editor .inline-chat .status .actions > .monaco-button-dropdown { +.inline-chat .status .actions > .monaco-button, +.inline-chat .status .actions > .monaco-button-dropdown { margin-right: 6px; } -.monaco-editor .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { +.inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { display: flex; align-items: center; padding: 0 4px; } -.monaco-editor .inline-chat .status .actions > .monaco-button.codicon { +.inline-chat .status .actions > .monaco-button.codicon { display: flex; } -.monaco-editor .inline-chat .status .actions > .monaco-button.codicon::before { +.inline-chat .status .actions > .monaco-button.codicon::before { align-self: center; } -.monaco-editor .inline-chat .status .actions .monaco-text-button { +.inline-chat .status .actions .monaco-text-button { padding: 2px 4px; white-space: nowrap; } -.monaco-editor .inline-chat .status .monaco-toolbar .action-item { +.inline-chat .status .monaco-toolbar .action-item { padding: 0 2px; } /* TODO@jrieken not needed? */ -.monaco-editor .inline-chat .status .monaco-toolbar .action-label.checked { +.inline-chat .status .monaco-toolbar .action-label.checked { color: var(--vscode-inputOption-activeForeground); background-color: var(--vscode-inputOption-activeBackground); outline: 1px solid var(--vscode-inputOption-activeBorder); } -.monaco-editor .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { +.inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { background-color: var(--vscode-button-hoverBackground); } /* preview */ -.monaco-editor .inline-chat .preview { +.inline-chat .preview { display: none; } -.monaco-editor .inline-chat .previewDiff, -.monaco-editor .inline-chat .previewCreate { +.inline-chat .previewDiff, +.inline-chat .previewCreate { display: inherit; border: 1px solid var(--vscode-inlineChat-border); border-radius: 2px; margin: 6px 0px; } -.monaco-editor .inline-chat .previewCreateTitle { +.inline-chat .previewCreateTitle { padding-top: 6px; } -.monaco-editor .inline-chat .previewDiff.hidden, -.monaco-editor .inline-chat .previewCreate.hidden, -.monaco-editor .inline-chat .previewCreateTitle.hidden { +.inline-chat .previewDiff.hidden, +.inline-chat .previewCreate.hidden, +.inline-chat .previewCreateTitle.hidden { display: none; } -.monaco-editor .inline-chat-toolbar { +.inline-chat-toolbar { display: flex; } -.monaco-editor .inline-chat-toolbar > .monaco-button{ +.inline-chat-toolbar > .monaco-button{ margin-right: 6px; } -.monaco-editor .inline-chat-toolbar .action-label.checked { +.inline-chat-toolbar .action-label.checked { color: var(--vscode-inputOption-activeForeground); background-color: var(--vscode-inputOption-activeBackground); outline: 1px solid var(--vscode-inputOption-activeBorder); @@ -329,65 +329,65 @@ /* decoration styles */ -.monaco-editor .inline-chat-inserted-range { +.inline-chat-inserted-range { background-color: var(--vscode-inlineChatDiff-inserted); } -.monaco-editor .inline-chat-inserted-range-linehighlight { +.inline-chat-inserted-range-linehighlight { background-color: var(--vscode-diffEditor-insertedLineBackground); } -.monaco-editor .inline-chat-original-zone2 { +.inline-chat-original-zone2 { background-color: var(--vscode-diffEditor-removedLineBackground); opacity: 0.8; } -.monaco-editor .inline-chat-lines-inserted-range { +.inline-chat-lines-inserted-range { background-color: var(--vscode-diffEditor-insertedTextBackground); } -.monaco-editor .inline-chat-block-selection { +.inline-chat-block-selection { background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat-slash-command { +.inline-chat-slash-command { opacity: 0; } -.monaco-editor .inline-chat-slash-command-detail { +.inline-chat-slash-command-detail { opacity: 0.5; } /* diff zone */ -.monaco-editor .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, -.monaco-editor .inline-chat-diff-widget .monaco-diff-editor .monaco-editor .margin-view-overlays { +.inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, +.inline-chat-diff-widget .monaco-diff-editor .monaco-editor .margin-view-overlays { background-color: var(--vscode-inlineChat-regionHighlight); } /* create zone */ -.monaco-editor .inline-chat-newfile-widget { +.inline-chat-newfile-widget { background-color: var(--vscode-inlineChat-regionHighlight); } -.monaco-editor .inline-chat-newfile-widget .title { +.inline-chat-newfile-widget .title { display: flex; align-items: center; justify-content: space-between; } -.monaco-editor .inline-chat-newfile-widget .title .detail { +.inline-chat-newfile-widget .title .detail { margin-left: 4px; } -.monaco-editor .inline-chat-newfile-widget .buttonbar-widget { +.inline-chat-newfile-widget .buttonbar-widget { display: flex; margin-left: auto; margin-right: 8px; } -.monaco-editor .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { +.inline-chat-newfile-widget .buttonbar-widget > .monaco-button { display: inline-flex; white-space: nowrap; margin-left: 4px; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css similarity index 86% rename from src/vs/workbench/contrib/terminalContrib/chat/browser/media/chat.css rename to src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index e81dc6b4752..5c8676bbe6e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -6,8 +6,9 @@ .terminal-inline-chat { position: absolute; left: 0; - top: 0; bottom: 0; - right: 0; z-index: 100; } +/* .terminal-inline-chat .inline-chat .body { + display: flex; +} */ 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 c210f1c4494..b9fab74a3a2 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,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/chat'; import { IDimension } from 'vs/base/browser/dom'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index d8c1d5180c7..7c67b161aba 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -3,20 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IFocusTracker, trackFocus } from 'vs/base/browser/dom'; +import 'vs/css!./media/terminalChatWidget'; +import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; import { IDetachedTerminalInstance, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; export class TerminalChatWidget extends Disposable { - private _widget!: ChatWidget; private _scopedInstantiationService: IInstantiationService; private _widgetContainer: HTMLElement; private _chatWidgetFocused: IContextKey; @@ -81,6 +80,8 @@ export class TerminalChatWidget extends Disposable { this._focusTracker = this._register(trackFocus(this._widgetContainer)); } reveal(): void { + this._inlineChatWidget.layout(new Dimension(400, 150)); + this._widgetContainer.classList.remove('hide'); // this._widget.setVisible(true); this._chatWidgetFocused.set(true); @@ -97,13 +98,13 @@ export class TerminalChatWidget extends Disposable { this._instance.focus(); } cancel(): void { - this._widget?.clear(); + // this._widget?.clear(); } acceptInput(): void { - this._widget?.acceptInput(); + // this._widget?.acceptInput(); } layout(width: number): void { - this._widget?.layout(100, width < 300 ? 300 : width); + // this._widget?.layout(100, width < 300 ? 300 : width); } public get focusTracker(): IFocusTracker { return this._focusTracker; From 22670a1031a0af3df9cc13f267eaa3dd16626bb2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:21:09 -0800 Subject: [PATCH 009/228] Add temp placeholders --- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 7c67b161aba..041ec3b9573 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -14,6 +14,7 @@ import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inline import { MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; import { IDetachedTerminalInstance, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { localize } from 'vs/nls'; export class TerminalChatWidget extends Disposable { private _scopedInstantiationService: IInstantiationService; @@ -74,6 +75,8 @@ export class TerminalChatWidget extends Disposable { feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK } ); + this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); + this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated code may be incorrect")); this._widgetContainer.appendChild(this._inlineChatWidget.domNode); From 792fc7a419483cf0b1bd6a496e4b68307574d479 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 9 Feb 2024 12:24:13 -0600 Subject: [PATCH 010/228] fix focus issues --- src/vs/workbench/contrib/terminal/browser/media/terminal.css | 2 +- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 8fc9e6be0eb..8fcb3c71172 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -572,6 +572,6 @@ top: 10px; } -.monaco-workbench .terminal-chat-widget.hide { +.monaco-workbench .terminal-inline-chat.hide { visibility: hidden; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 041ec3b9573..89c2518dd8e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -92,6 +92,7 @@ export class TerminalChatWidget extends Disposable { // this._widget.setInput('@terminal'); // this._widget.setInputPlaceholder('Request a terminal command'); // this._widget.focusInput(); + this._inlineChatWidget.focus(); } hide(): void { this._widgetContainer.classList.add('hide'); From dfed5c7b09b6ff0dd6d91de17875c8278e66e3d2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:31:03 -0800 Subject: [PATCH 011/228] Remove detached terminal support --- .../browser/terminal.chat.contribution.ts | 34 +++++++++++++------ .../chat/browser/terminalChatWidget.ts | 14 ++++---- 2 files changed, 31 insertions(+), 17 deletions(-) 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 b9fab74a3a2..e54914120bd 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,31 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { IDimension } from 'vs/base/browser/dom'; +import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize2 } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { ITerminalProcessInfo, ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; -import { Codicon } from 'vs/base/common/codicons'; -import { MenuId } from 'vs/platform/actions/common/actions'; import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; -import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class TerminalChatContribution extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; - static get(instance: ITerminalInstance | IDetachedTerminalInstance): TerminalChatContribution | null { + static get(instance: ITerminalInstance): TerminalChatContribution | null { return instance.getContribution(TerminalChatContribution.ID); } /** @@ -41,8 +41,8 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } constructor( - private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, - processManager: ITerminalProcessManager | ITerminalProcessInfo, + private readonly _instance: ITerminalInstance, + processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService private _configurationService: IConfigurationService, @@ -96,7 +96,7 @@ export class TerminalChatContribution extends Disposable implements ITerminalCon this._chatWidget?.rawValue?.dispose(); } } -registerTerminalContribution(TerminalChatContribution.ID, TerminalChatContribution, true); +registerTerminalContribution(TerminalChatContribution.ID, TerminalChatContribution, false); registerActiveXtermAction({ id: TerminalCommandId.FocusChat, @@ -112,6 +112,9 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), ), run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); contr?.chatWidget?.reveal(); } @@ -132,6 +135,9 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), ), run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); contr?.chatWidget?.hide(); } @@ -152,6 +158,9 @@ registerActiveXtermAction({ group: 'navigation', }, run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); contr?.chatWidget?.acceptInput(); } @@ -170,6 +179,9 @@ registerActiveXtermAction({ group: 'navigation', }, run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); contr?.chatWidget?.cancel(); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 89c2518dd8e..3a99cd5ebef 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -12,7 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; -import { IDetachedTerminalInstance, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { localize } from 'vs/nls'; @@ -28,10 +28,10 @@ export class TerminalChatWidget extends Disposable { constructor( private readonly _container: HTMLElement, - private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, - + private readonly _instance: ITerminalInstance, @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService) { + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); @@ -61,7 +61,9 @@ export class TerminalChatWidget extends Disposable { const fakeParentEditor = this._scopedInstantiationService.createInstance( CodeEditorWidget, fakeParentEditorElement, - {}, + { + + }, { isSimpleWidget: true } ); @@ -76,7 +78,7 @@ export class TerminalChatWidget extends Disposable { } ); this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); - this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated code may be incorrect")); + this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); this._widgetContainer.appendChild(this._inlineChatWidget.domNode); From cfeb85db2be149ab3c0e5a6e466f02876acf5330 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:49:57 -0800 Subject: [PATCH 012/228] Anchor term chat to bottom --- .../terminalContrib/chat/browser/media/terminalChatWidget.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index 5c8676bbe6e..633b6778dfe 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -8,6 +8,7 @@ left: 0; bottom: 0; z-index: 100; + height: auto !important; } /* .terminal-inline-chat .inline-chat .body { display: flex; From 0a8994cf3ac712a7ee9e5fa1ddbf82a70c8ce891 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 9 Feb 2024 12:53:06 -0600 Subject: [PATCH 013/228] use terminal menu ID --- src/vs/platform/actions/common/actions.ts | 1 + .../actions/voiceChatActions.ts | 6 ++++++ .../chat/browser/terminalChatWidget.ts | 20 +++++++++---------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 376627398ac..ec2810e91fb 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -208,6 +208,7 @@ export class MenuId { static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); static readonly ChatMessageTitle = new MenuId('ChatMessageTitle'); static readonly ChatExecute = new MenuId('ChatExecute'); + static readonly TerminalChat = new MenuId('TerminalChat'); static readonly ChatInputSide = new MenuId('ChatInputSide'); static readonly AccessibleView = new MenuId('AccessibleView'); static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar'); 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 3b605a096d5..75f3ab3c832 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -475,6 +475,12 @@ export class StartVoiceChatAction extends Action2 { when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate()), group: 'main', order: -1 + }, + { + id: MenuId.TerminalChat, + when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate()), + group: 'main', + order: -1 }] }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 3a99cd5ebef..de24d36312d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -11,10 +11,11 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; -import { MENU_CELL_CHAT_INPUT, MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; -import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDetachedTerminalInstance, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { localize } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS, MENU_CELL_CHAT_WIDGET_FEEDBACK } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; export class TerminalChatWidget extends Disposable { private _scopedInstantiationService: IInstantiationService; @@ -28,10 +29,10 @@ export class TerminalChatWidget extends Disposable { constructor( private readonly _container: HTMLElement, - private readonly _instance: ITerminalInstance, + private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, + @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService - ) { + @IContextKeyService private readonly _contextKeyService: IContextKeyService) { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); @@ -61,9 +62,7 @@ export class TerminalChatWidget extends Disposable { const fakeParentEditor = this._scopedInstantiationService.createInstance( CodeEditorWidget, fakeParentEditorElement, - { - - }, + {}, { isSimpleWidget: true } ); @@ -71,15 +70,14 @@ export class TerminalChatWidget extends Disposable { InlineChatWidget, fakeParentEditor, { - menuId: MENU_CELL_CHAT_INPUT, + menuId: MenuId.TerminalChat, widgetMenuId: MENU_CELL_CHAT_WIDGET, statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK } ); this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); - this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); - + this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated code may be incorrect")); this._widgetContainer.appendChild(this._inlineChatWidget.domNode); this._focusTracker = this._register(trackFocus(this._widgetContainer)); From 9e07b3ae1e411647d86531822b31178e044a4df5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:54:57 -0800 Subject: [PATCH 014/228] Remove detached terminal support --- .../terminalContrib/chat/browser/terminalChatWidget.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index de24d36312d..bef5d94f3d8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -11,7 +11,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; -import { IDetachedTerminalInstance, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; @@ -29,7 +29,7 @@ export class TerminalChatWidget extends Disposable { constructor( private readonly _container: HTMLElement, - private readonly _instance: ITerminalInstance | IDetachedTerminalInstance, + private readonly _instance: ITerminalInstance, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService) { From 5e3bd936bcb55db758af7de1346549eff0d3f2f5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:59:31 -0800 Subject: [PATCH 015/228] Fix panel background overriding input's --- src/vs/workbench/browser/parts/panel/media/panelpart.css | 6 +++--- .../terminalContrib/chat/browser/terminalChatWidget.ts | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 4ccb53b0c80..7b17465b2d9 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -35,9 +35,9 @@ border-right-width: 0; /* no border when main editor area is hiden */ } -.monaco-workbench .part.panel > .content .monaco-editor, -.monaco-workbench .part.panel > .content .monaco-editor .margin, -.monaco-workbench .part.panel > .content .monaco-editor .monaco-editor-background { +.monaco-workbench .part.panel > .content .monaco-editor:not(.ignore-panel-bg), +.monaco-workbench .part.panel > .content .monaco-editor:not(.ignore-panel-bg) .margin, +.monaco-workbench .part.panel > .content .monaco-editor:not(.ignore-panel-bg) .monaco-editor-background { background-color: var(--vscode-panel-background); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index bef5d94f3d8..fc85c9a5a50 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -62,7 +62,9 @@ export class TerminalChatWidget extends Disposable { const fakeParentEditor = this._scopedInstantiationService.createInstance( CodeEditorWidget, fakeParentEditorElement, - {}, + { + extraEditorClassName: 'ignore-panel-bg' + }, { isSimpleWidget: true } ); From 2611151ba8966ba0c52c27ea0885a6cadd39c99a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 11:10:10 -0800 Subject: [PATCH 016/228] Add wip make request button --- .../contrib/terminal/common/terminal.ts | 2 +- .../browser/terminal.chat.contribution.ts | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 0579628b521..9a11ee3252c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -502,7 +502,7 @@ export const enum TerminalCommandId { FontZoomReset = 'workbench.action.terminal.fontZoomReset', FocusChat = 'workbench.action.terminal.focusChat', HideChat = 'workbench.action.terminal.hideChat', - SubmitChat = 'workbench.action.terminal.submitChat', + MakeChatRequest = 'workbench.action.terminal.submitChat', CancelChat = 'workbench.action.terminal.cancelChat', // Developer commands 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 e54914120bd..6c44b62619c 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 @@ -100,7 +100,7 @@ registerTerminalContribution(TerminalChatContribution.ID, TerminalChatContributi registerActiveXtermAction({ id: TerminalCommandId.FocusChat, - title: localize2('workbench.action.terminal.focusChat', 'Terminal: Focus Chat'), + title: localize2('workbench.action.terminal.focusChat', 'Focus Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), @@ -122,7 +122,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalCommandId.HideChat, - title: localize2('workbench.action.terminal.hideChat', 'Terminal: Hide Chat'), + title: localize2('workbench.action.terminal.hideChat', 'Hide Chat'), keybinding: { primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], @@ -144,18 +144,28 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.SubmitChat, - title: localize2('workbench.action.terminal.submitChat', 'Terminal: Submit Chat'), + id: TerminalCommandId.MakeChatRequest, + title: localize2('workbench.action.terminal.submitChat', 'Make Chat Request'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.chatInputHasText ), icon: Codicon.send, + keybinding: { + when: TerminalContextKeys.chatSessionInProgress, + // TODO: + // when: CTX_INLINE_CHAT_FOCUSED, + weight: KeybindingWeight.EditorCore + 7, + primary: KeyCode.Enter + }, menu: { - id: MenuId.ChatExecute, + id: MenuId.TerminalChat, + group: 'main', + order: 1, when: TerminalContextKeys.chatSessionInProgress.negate(), - group: 'navigation', + // TODO: + // when: CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.isEqualTo(false) }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { From a8cda8716fb80b41170aeb251b99a26b276e2bfc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 9 Feb 2024 11:10:25 -0800 Subject: [PATCH 017/228] Fix command name --- src/vs/workbench/contrib/terminal/common/terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 9a11ee3252c..5ed9e674af2 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -502,7 +502,7 @@ export const enum TerminalCommandId { FontZoomReset = 'workbench.action.terminal.fontZoomReset', FocusChat = 'workbench.action.terminal.focusChat', HideChat = 'workbench.action.terminal.hideChat', - MakeChatRequest = 'workbench.action.terminal.submitChat', + MakeChatRequest = 'workbench.action.terminal.makeChatRequest', CancelChat = 'workbench.action.terminal.cancelChat', // Developer commands From b79d48c296a18057450205fe65029dabebb08ddc Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 12 Feb 2024 13:52:59 -0600 Subject: [PATCH 018/228] wip make request action --- .../chat/browser/terminal.chat.contribution.ts | 6 +++--- .../chat/browser/terminalChatWidget.ts | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) 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 6c44b62619c..c2bbe8aa74a 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 @@ -149,11 +149,11 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalContextKeys.chatInputHasText + // TerminalContextKeys.chatInputHasText ), icon: Codicon.send, keybinding: { - when: TerminalContextKeys.chatSessionInProgress, + when: TerminalContextKeys.chatSessionInProgress.negate(), // TODO: // when: CTX_INLINE_CHAT_FOCUSED, weight: KeybindingWeight.EditorCore + 7, @@ -163,7 +163,7 @@ registerActiveXtermAction({ id: MenuId.TerminalChat, group: 'main', order: 1, - when: TerminalContextKeys.chatSessionInProgress.negate(), + // when: TerminalContextKeys.chatSessionInProgress.negate(), // TODO: // when: CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.isEqualTo(false) }, diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index fc85c9a5a50..1467a9a86ef 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -16,6 +16,9 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS, MENU_CELL_CHAT_WIDGET_FEEDBACK } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class TerminalChatWidget extends Disposable { private _scopedInstantiationService: IInstantiationService; @@ -27,12 +30,15 @@ export class TerminalChatWidget extends Disposable { private readonly _focusTracker: IFocusTracker; + private _chatModel: ChatModel | undefined; + constructor( private readonly _container: HTMLElement, private readonly _instance: ITerminalInstance, @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService) { + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IChatService private readonly _chatService: IChatService) { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); @@ -41,6 +47,7 @@ export class TerminalChatWidget extends Disposable { this._widgetContainer = document.createElement('div'); this._widgetContainer.classList.add('terminal-inline-chat'); this._container.appendChild(this._widgetContainer); + // this._widget = this._register(this._scopedInstantiationService.createInstance( // ChatWidget, // { viewId: 'terminal' }, @@ -108,6 +115,14 @@ export class TerminalChatWidget extends Disposable { } acceptInput(): void { // this._widget?.acceptInput(); + this._chatModel ??= this._chatService.startSession('terminal', CancellationToken.None); + + // if (!this._model) { + // throw new Error('Could not start chat session'); + // } + this._chatService?.sendRequest(this._chatModel?.sessionId!, this._inlineChatWidget.value); + this._inlineChatWidget.value = ''; + // this.widget.setModel(this.model, { inputValue: this._currentQuery }); } layout(width: number): void { // this._widget?.layout(100, width < 300 ? 300 : width); From a0d97ae9449d8665a531d8dd1dd58c4fe24332fe Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 12 Feb 2024 15:49:32 -0600 Subject: [PATCH 019/228] contrib => controller --- .../terminal/common/terminalContextKey.ts | 6 +- .../browser/terminal.chat.contribution.ts | 101 ++------------- .../chat/browser/terminalChatController.ts | 119 ++++++++++++++++++ .../chat/browser/terminalChatWidget.ts | 5 +- 4 files changed, 136 insertions(+), 95 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 1de53d61930..78fd7df6712 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -41,7 +41,7 @@ export const enum TerminalContextKeyStrings { TerminalShellIntegrationEnabled = 'terminalShellIntegrationEnabled', ChatFocus = 'terminalChatFocus', ChatVisible = 'terminalChatVisible', - ChatSessionInProgress = 'terminalChatSessionInProgress', + ChatActiveRequest = 'terminalChatActiveRequest', ChatInputHasText = 'terminalChatInputHasText', } @@ -170,8 +170,8 @@ export namespace TerminalContextKeys { /** Whether the chat widget is visible */ export const chatVisible = new RawContextKey(TerminalContextKeyStrings.ChatVisible, false, localize('chatVisibleContextKey', "Whether the chat view is visible.")); - /** Whether a chat session is in progress */ - export const chatSessionInProgress = new RawContextKey(TerminalContextKeyStrings.ChatSessionInProgress, false, localize('chatSessionInProgressContextKey', "Whether a chat session is in progress.")); + /** Whether there is an active chat request */ + export const chatRequestActive = new RawContextKey(TerminalContextKeyStrings.ChatActiveRequest, false, localize('chatRequestActiveContextKey', "Whether there is an active chat request.")); /** Whether the chat input has text */ export const chatInputHasText = new RawContextKey(TerminalContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); 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 c2bbe8aa74a..1d909e6fc30 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,100 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; -import { IDimension } from 'vs/base/browser/dom'; import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Lazy } from 'vs/base/common/lazy'; -import { Disposable } from 'vs/base/common/lifecycle'; import { localize2 } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; -import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { ITerminalProcessManager, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; -export class TerminalChatContribution extends Disposable implements ITerminalContribution { - static readonly ID = 'terminal.Chat'; - - static get(instance: ITerminalInstance): TerminalChatContribution | null { - return instance.getContribution(TerminalChatContribution.ID); - } - /** - * Currently focused chat widget. This is used to track action context since - * 'active terminals' are only tracked for non-detached terminal instanecs. - */ - static activeChatWidget?: TerminalChatContribution; - private _chatWidget: Lazy | undefined; - private _lastLayoutDimensions: IDimension | undefined; - - get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } - - constructor( - private readonly _instance: ITerminalInstance, - processManager: ITerminalProcessManager, - widgetManager: TerminalWidgetManager, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private _configurationService: IConfigurationService, - @ITerminalService private readonly _terminalService: ITerminalService - ) { - super(); - if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { - return; - } - } - - layout(_xterm: IXtermTerminal & { raw: RawXtermTerminal }, dimension: IDimension): void { - if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { - return; - } - this._lastLayoutDimensions = dimension; - this._chatWidget?.rawValue?.layout(dimension.width); - } - - xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { - if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { - return; - } - this._chatWidget = new Lazy(() => { - const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); - chatWidget.focusTracker.onDidFocus(() => { - TerminalChatContribution.activeChatWidget = this; - if (!isDetachedTerminalInstance(this._instance)) { - this._terminalService.setActiveInstance(this._instance); - } - }); - chatWidget.focusTracker.onDidBlur(() => { - TerminalChatContribution.activeChatWidget = undefined; - this._instance.resetScrollbarVisibility(); - }); - if (!this._instance.domElement) { - throw new Error('FindWidget expected terminal DOM to be initialized'); - } - - // this._instance.domElement?.appendChild(chatWidget.getDomNode()); - if (this._lastLayoutDimensions) { - chatWidget.layout(this._lastLayoutDimensions.width); - } - - return chatWidget; - }); - } - - override dispose() { - super.dispose(); - this._chatWidget?.rawValue?.dispose(); - } -} -registerTerminalContribution(TerminalChatContribution.ID, TerminalChatContribution, false); +registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); registerActiveXtermAction({ id: TerminalCommandId.FocusChat, @@ -115,7 +36,7 @@ registerActiveXtermAction({ if (isDetachedTerminalInstance(activeInstance)) { return; } - const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); contr?.chatWidget?.reveal(); } }); @@ -138,7 +59,7 @@ registerActiveXtermAction({ if (isDetachedTerminalInstance(activeInstance)) { return; } - const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); contr?.chatWidget?.hide(); } }); @@ -153,7 +74,7 @@ registerActiveXtermAction({ ), icon: Codicon.send, keybinding: { - when: TerminalContextKeys.chatSessionInProgress.negate(), + when: TerminalContextKeys.chatRequestActive.negate(), // TODO: // when: CTX_INLINE_CHAT_FOCUSED, weight: KeybindingWeight.EditorCore + 7, @@ -171,8 +92,8 @@ registerActiveXtermAction({ if (isDetachedTerminalInstance(activeInstance)) { return; } - const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); - contr?.chatWidget?.acceptInput(); + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptInput(); } }); @@ -181,7 +102,7 @@ registerActiveXtermAction({ title: localize2('workbench.action.terminal.cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatSessionInProgress, + TerminalContextKeys.chatRequestActive, ), icon: Codicon.debugStop, menu: { @@ -192,7 +113,7 @@ registerActiveXtermAction({ if (isDetachedTerminalInstance(activeInstance)) { return; } - const contr = TerminalChatContribution.activeChatWidget || TerminalChatContribution.get(activeInstance); + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); contr?.chatWidget?.cancel(); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts new file mode 100644 index 00000000000..ae9ded44aec --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; +import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDimension } from 'vs/base/browser/dom'; +import { Lazy } from 'vs/base/common/lazy'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; +import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; +import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; +import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; + +export class TerminalChatController extends Disposable implements ITerminalContribution { + static readonly ID = 'terminal.Chat'; + + static get(instance: ITerminalInstance): TerminalChatController | null { + return instance.getContribution(TerminalChatController.ID); + } + /** + * Currently focused chat widget. This is used to track action context since + * 'active terminals' are only tracked for non-detached terminal instanecs. + */ + static activeChatWidget?: TerminalChatController; + private _chatWidget: Lazy | undefined; + private _lastLayoutDimensions: IDimension | undefined; + + get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } + + // private _sessionCtor: CancelablePromise | undefined; + private _activeSession?: Session; + // private readonly _ctxHasActiveRequest: IContextKey; + // private _isVisible: boolean = false; + // private _strategy: EditStrategy | undefined; + + // private _inlineChatListener: IDisposable | undefined; + // private _toolbar: MenuWorkbenchToolBar | undefined; + // private readonly _ctxLastResponseType: IContextKey; + // private _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); + + constructor( + private readonly _instance: ITerminalInstance, + processManager: ITerminalProcessManager, + widgetManager: TerminalWidgetManager, + @IConfigurationService private _configurationService: IConfigurationService, + @ITerminalService private readonly _terminalService: ITerminalService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, + // @IContextKeyService private readonly _contextKeyService: IContextKeyService, + // @IInstantiationService private readonly _instantiationService: IInstantiationService, + // @ICommandService private readonly _commandService: ICommandService, + // @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService + ) { + super(); + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + // this._ctxHasActiveRequest = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); + // this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService); + } + + layout(_xterm: IXtermTerminal & { raw: RawXtermTerminal }, dimension: IDimension): void { + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + this._lastLayoutDimensions = dimension; + this._chatWidget?.rawValue?.layout(dimension.width); + } + + xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { + return; + } + this._chatWidget = new Lazy(() => { + const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); + chatWidget.focusTracker.onDidFocus(() => { + TerminalChatController.activeChatWidget = this; + if (!isDetachedTerminalInstance(this._instance)) { + this._terminalService.setActiveInstance(this._instance); + } + }); + chatWidget.focusTracker.onDidBlur(() => { + TerminalChatController.activeChatWidget = undefined; + this._instance.resetScrollbarVisibility(); + }); + if (!this._instance.domElement) { + throw new Error('FindWidget expected terminal DOM to be initialized'); + } + + // this._instance.domElement?.appendChild(chatWidget.getDomNode()); + if (this._lastLayoutDimensions) { + chatWidget.layout(this._lastLayoutDimensions.width); + } + + return chatWidget; + }); + } + + async acceptInput(): Promise { + // TODO: create session, deal with response + this._chatWidget?.rawValue?.acceptInput(); + } + + reveal(): void { + this._chatWidget?.rawValue?.reveal(); + } + + override dispose() { + super.dispose(); + this._chatWidget?.rawValue?.dispose(); + } +} + diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 1467a9a86ef..14a6ca567b4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -115,12 +115,13 @@ export class TerminalChatWidget extends Disposable { } acceptInput(): void { // this._widget?.acceptInput(); - this._chatModel ??= this._chatService.startSession('terminal', CancellationToken.None); + // this._chatModel ??= this._chatService.startSession('terminal', CancellationToken.None); // if (!this._model) { // throw new Error('Could not start chat session'); // } - this._chatService?.sendRequest(this._chatModel?.sessionId!, this._inlineChatWidget.value); + // this._chatService?.sendRequest(this._chatModel?.sessionId!, this._inlineChatWidget.value); + this._inlineChatWidget.value = ''; // this.widget.setModel(this.model, { inputValue: this._currentQuery }); } From fd0784486582c2cff44a8c4bb28fa01b38904473 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 12 Feb 2024 16:27:38 -0600 Subject: [PATCH 020/228] wip make request action --- .../chat/browser/terminalChatController.ts | 20 +++++++++- .../chat/browser/terminalChatWidget.ts | 39 ++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index ae9ded44aec..5392e1787dd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -14,8 +14,8 @@ import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/wid import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; export class TerminalChatController extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; @@ -51,7 +51,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IConfigurationService private _configurationService: IConfigurationService, @ITerminalService private readonly _terminalService: ITerminalService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, + @IChatAgentService private readonly _chatAgentService: IChatAgentService // @IContextKeyService private readonly _contextKeyService: IContextKeyService, // @IInstantiationService private readonly _instantiationService: IInstantiationService, // @ICommandService private readonly _commandService: ICommandService, @@ -104,6 +104,22 @@ export class TerminalChatController extends Disposable implements ITerminalContr async acceptInput(): Promise { // TODO: create session, deal with response + // this._activeSession = new Session(EditMode.Live, , this._instance); + // const initVariableData: IChatRequestVariableData = { message: getPromptText(parsedRequest.parts), variables: {} }; + // request = model.addRequest(parsedRequest, initVariableData, agent, agentSlashCommandPart?.command); + // const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, token); + // const requestProps: IChatAgentRequest = { + // sessionId: 'sessionId', + // requestId: 'fake', + // agentId: 'terminal', + // message: this._chatWidget?.rawValue?.getValue() || '', + // // variables: variableData.variables, + // // command: agentSlashCommandPart?.command.name, + // // variables2: asVariablesData2(parsedRequest, variableData) + // }; + // const agentResult = await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, undefined, token); + // const rawResult = agentResult; + // const agentOrCommandFollowups = this._chatAgentService.getFollowups('terminal', agentResult, followupsCancelToken); this._chatWidget?.rawValue?.acceptInput(); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 14a6ca567b4..ed78f6292e7 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -16,8 +16,9 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; import { MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS, MENU_CELL_CHAT_WIDGET_FEEDBACK } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CancellationToken } from 'vs/base/common/cancellation'; export class TerminalChatWidget extends Disposable { @@ -30,7 +31,6 @@ export class TerminalChatWidget extends Disposable { private readonly _focusTracker: IFocusTracker; - private _chatModel: ChatModel | undefined; constructor( private readonly _container: HTMLElement, @@ -38,7 +38,7 @@ export class TerminalChatWidget extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IChatService private readonly _chatService: IChatService) { + @IChatAgentService private readonly _chatAgentService: IChatAgentService) { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); @@ -113,7 +113,7 @@ export class TerminalChatWidget extends Disposable { cancel(): void { // this._widget?.clear(); } - acceptInput(): void { + async acceptInput(): Promise { // this._widget?.acceptInput(); // this._chatModel ??= this._chatService.startSession('terminal', CancellationToken.None); @@ -121,9 +121,36 @@ export class TerminalChatWidget extends Disposable { // throw new Error('Could not start chat session'); // } // this._chatService?.sendRequest(this._chatModel?.sessionId!, this._inlineChatWidget.value); + // this._activeSession = new Session(EditMode.Live, , this._instance); + // const initVariableData: IChatRequestVariableData = { message: getPromptText(parsedRequest.parts), variables: {} }; + // request = model.addRequest(parsedRequest, initVariableData, agent, agentSlashCommandPart?.command); + // const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, token); + const progressCallback = (progress: IChatProgress) => { + // if (token.isCancellationRequested) { + // return; + // } + console.log(progress); + // gotProgress = true; + if (progress.kind === 'content' || progress.kind === 'markdownContent') { + // this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${typeof progress.content === 'string' ? progress.content.length : progress.content.value.length} chars`); + } else { + // this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progress)}`); + } + + // model.acceptResponseProgress(request, progress); + }; + const requestProps: IChatAgentRequest = { + sessionId: generateUuid(), + requestId: generateUuid(), + agentId: 'terminal', + message: this._inlineChatWidget.value || '', + variables: new Map() as any, + variables2: {} as any + }; + const agentResult = await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], CancellationToken.None); + console.log(agentResult); this._inlineChatWidget.value = ''; - // this.widget.setModel(this.model, { inputValue: this._currentQuery }); } layout(width: number): void { // this._widget?.layout(100, width < 300 ? 300 : width); From 760ee5b9976bc9ee647e5aa5ecfa26c42780fab4 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 10:22:14 -0600 Subject: [PATCH 021/228] wip --- .../chat/browser/terminalChatController.ts | 65 ++++++++++++------- .../chat/browser/terminalChatWidget.ts | 64 ++++++------------ 2 files changed, 63 insertions(+), 66 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 5392e1787dd..8308105d9f5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -14,8 +14,11 @@ import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/wid import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { generateUuid } from 'vs/base/common/uuid'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; export class TerminalChatController extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; @@ -34,7 +37,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } // private _sessionCtor: CancelablePromise | undefined; - private _activeSession?: Session; + // private _activeSession?: Session; // private readonly _ctxHasActiveRequest: IContextKey; // private _isVisible: boolean = false; // private _strategy: EditStrategy | undefined; @@ -51,7 +54,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IConfigurationService private _configurationService: IConfigurationService, @ITerminalService private readonly _terminalService: ITerminalService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService + @IChatAgentService private readonly _chatAgentService: IChatAgentService, // @IContextKeyService private readonly _contextKeyService: IContextKeyService, // @IInstantiationService private readonly _instantiationService: IInstantiationService, // @ICommandService private readonly _commandService: ICommandService, @@ -93,7 +96,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr throw new Error('FindWidget expected terminal DOM to be initialized'); } - // this._instance.domElement?.appendChild(chatWidget.getDomNode()); if (this._lastLayoutDimensions) { chatWidget.layout(this._lastLayoutDimensions.width); } @@ -103,24 +105,41 @@ export class TerminalChatController extends Disposable implements ITerminalContr } async acceptInput(): Promise { - // TODO: create session, deal with response - // this._activeSession = new Session(EditMode.Live, , this._instance); - // const initVariableData: IChatRequestVariableData = { message: getPromptText(parsedRequest.parts), variables: {} }; - // request = model.addRequest(parsedRequest, initVariableData, agent, agentSlashCommandPart?.command); - // const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, token); - // const requestProps: IChatAgentRequest = { - // sessionId: 'sessionId', - // requestId: 'fake', - // agentId: 'terminal', - // message: this._chatWidget?.rawValue?.getValue() || '', - // // variables: variableData.variables, - // // command: agentSlashCommandPart?.command.name, - // // variables2: asVariablesData2(parsedRequest, variableData) - // }; - // const agentResult = await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, undefined, token); - // const rawResult = agentResult; - // const agentOrCommandFollowups = this._chatAgentService.getFollowups('terminal', agentResult, followupsCancelToken); - this._chatWidget?.rawValue?.acceptInput(); + let message = ''; + const progressCallback = (progress: IChatProgress) => { + // if (token.isCancellationRequested) { + // return; + // } + + + // gotProgress = true; + + if (progress.kind === 'content' || progress.kind === 'markdownContent') { + // this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${typeof progress.content === 'string' ? progress.content.length : progress.content.value.length} chars`); + message += progress.content; + } else { + // this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progress)}`); + } + + // model.acceptResponseProgress(request, progress); + }; + const resolvedVariables: Record = {}; + + const requestProps: IChatAgentRequest = { + sessionId: generateUuid(), + requestId: generateUuid(), + agentId: 'terminal', + message: this._chatWidget?.rawValue?.input() || '', + variables: resolvedVariables, + variables2: { message: this._chatWidget?.rawValue?.input() || '', variables: [] } + }; + // TODO: use token + const agentResult = await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], CancellationToken.None); + console.log(agentResult); + console.log(message); + alert(message); + this._chatWidget?.rawValue?.setValue(); + // TODO: accessibility announcement, help dialog } reveal(): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index ed78f6292e7..ee0a778cc0f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -15,11 +15,8 @@ import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/termina import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; -import { MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_STATUS, MENU_CELL_CHAT_WIDGET_FEEDBACK } from 'vs/workbench/contrib/notebook/browser/view/cellParts/chat/cellChatController'; -import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; export class TerminalChatWidget extends Disposable { private _scopedInstantiationService: IInstantiationService; @@ -113,45 +110,26 @@ export class TerminalChatWidget extends Disposable { cancel(): void { // this._widget?.clear(); } - async acceptInput(): Promise { - // this._widget?.acceptInput(); - // this._chatModel ??= this._chatService.startSession('terminal', CancellationToken.None); - - // if (!this._model) { - // throw new Error('Could not start chat session'); - // } - // this._chatService?.sendRequest(this._chatModel?.sessionId!, this._inlineChatWidget.value); - // this._activeSession = new Session(EditMode.Live, , this._instance); - // const initVariableData: IChatRequestVariableData = { message: getPromptText(parsedRequest.parts), variables: {} }; - // request = model.addRequest(parsedRequest, initVariableData, agent, agentSlashCommandPart?.command); - // const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, token); - const progressCallback = (progress: IChatProgress) => { - // if (token.isCancellationRequested) { - // return; - // } - console.log(progress); - // gotProgress = true; - - if (progress.kind === 'content' || progress.kind === 'markdownContent') { - // this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${typeof progress.content === 'string' ? progress.content.length : progress.content.value.length} chars`); - } else { - // this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progress)}`); - } - - // model.acceptResponseProgress(request, progress); - }; - const requestProps: IChatAgentRequest = { - sessionId: generateUuid(), - requestId: generateUuid(), - agentId: 'terminal', - message: this._inlineChatWidget.value || '', - variables: new Map() as any, - variables2: {} as any - }; - const agentResult = await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], CancellationToken.None); - console.log(agentResult); - this._inlineChatWidget.value = ''; + input(): string { + return this._inlineChatWidget.value; } + setValue(value?: string) { + this._inlineChatWidget.value = value ?? ''; + } + // async acceptInput(): Promise { + // // this._widget?.acceptInput(); + // // this._chatModel ??= this._chatService.startSession('terminal', CancellationToken.None); + + // // if (!this._model) { + // // throw new Error('Could not start chat session'); + // // } + // // this._chatService?.sendRequest(this._chatModel?.sessionId!, this._inlineChatWidget.value); + // // this._activeSession = new Session(EditMode.Live, , this._instance); + // // const initVariableData: IChatRequestVariableData = { message: getPromptText(parsedRequest.parts), variables: {} }; + // // request = model.addRequest(parsedRequest, initVariableData, agent, agentSlashCommandPart?.command); + // // const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, token); + // this._inlineChatWidget.value = ''; + // } layout(width: number): void { // this._widget?.layout(100, width < 300 ? 300 : width); } From b998b6432bd6c080a26b59ccd8a03f69b06a0169 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 08:30:12 -0800 Subject: [PATCH 022/228] Add menus for terminal chat to the terminalContrib --- src/vs/platform/actions/common/actions.ts | 1 - .../chat/browser/terminal.chat.contribution.ts | 11 +++++++++-- .../chat/browser/terminalChat.ts | 12 ++++++++++++ .../chat/browser/terminalChatWidget.ts | 17 ++++++++--------- 4 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 3102938a3be..0b3015ddf75 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -211,7 +211,6 @@ export class MenuId { static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); static readonly ChatMessageTitle = new MenuId('ChatMessageTitle'); static readonly ChatExecute = new MenuId('ChatExecute'); - static readonly TerminalChat = new MenuId('TerminalChat'); static readonly ChatInputSide = new MenuId('ChatInputSide'); static readonly AccessibleView = new MenuId('AccessibleView'); static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar'); 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 1d909e6fc30..fd88e4c0982 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 @@ -15,6 +15,7 @@ import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); @@ -43,13 +44,19 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalCommandId.HideChat, - title: localize2('workbench.action.terminal.hideChat', 'Hide Chat'), + title: localize2('workbench.action.terminal.closeChat', 'Close Chat'), keybinding: { primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.and(TerminalContextKeys.chatFocused, TerminalContextKeys.chatVisible), weight: KeybindingWeight.WorkbenchContrib }, + icon: Codicon.close, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'main', + order: 2 + }, f1: true, precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -81,7 +88,7 @@ registerActiveXtermAction({ primary: KeyCode.Enter }, menu: { - id: MenuId.TerminalChat, + id: MENU_TERMINAL_CHAT_INPUT, group: 'main', order: 1, // when: TerminalContextKeys.chatSessionInProgress.negate(), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts new file mode 100644 index 00000000000..8b74aae7aeb --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MenuId } from 'vs/platform/actions/common/actions'; + +export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput'); +export const MENU_TERMINAL_CHAT_WIDGET = MenuId.for('terminalChatWidget'); +export const MENU_TERMINAL_CHAT_WIDGET_STATUS = MenuId.for('terminalChatWidget.status'); +export const MENU_TERMINAL_CHAT_WIDGET_FEEDBACK = MenuId.for('terminalChatWidget.feedback'); +export const MENU_TERMINAL_CHAT_WIDGET_TOOLBAR = MenuId.for('terminalChatWidget.toolbar'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index ee0a778cc0f..7f30c3bca53 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -3,20 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/terminalChatWidget'; import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; +import 'vs/css!./media/terminalChatWidget'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { localize } from 'vs/nls'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { MENU_CELL_CHAT_WIDGET, MENU_CELL_CHAT_WIDGET_FEEDBACK, MENU_CELL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/notebook/browser/controller/chat/notebookChatContext'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; export class TerminalChatWidget extends Disposable { private _scopedInstantiationService: IInstantiationService; @@ -76,10 +75,10 @@ export class TerminalChatWidget extends Disposable { InlineChatWidget, fakeParentEditor, { - menuId: MenuId.TerminalChat, - widgetMenuId: MENU_CELL_CHAT_WIDGET, - statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK + menuId: MENU_TERMINAL_CHAT_INPUT, + widgetMenuId: MENU_TERMINAL_CHAT_WIDGET, + statusMenuId: MENU_TERMINAL_CHAT_WIDGET_STATUS, + feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK } ); this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); From e4e9562df987263f6315e4104a7e968bf45ef87a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 08:33:18 -0800 Subject: [PATCH 023/228] Clean up chat widget --- .../chat/browser/terminalChatWidget.ts | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 7f30c3bca53..330be0b35a5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -31,10 +31,10 @@ export class TerminalChatWidget extends Disposable { constructor( private readonly _container: HTMLElement, private readonly _instance: ITerminalInstance, - @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService) { + @IChatAgentService private readonly _chatAgentService: IChatAgentService + ) { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); @@ -44,24 +44,9 @@ export class TerminalChatWidget extends Disposable { this._widgetContainer.classList.add('terminal-inline-chat'); this._container.appendChild(this._widgetContainer); - // this._widget = this._register(this._scopedInstantiationService.createInstance( - // ChatWidget, - // { viewId: 'terminal' }, - // { supportsFileReferences: false, renderStyle: 'compact' }, - // { - // listForeground: editorForeground, - // listBackground: editorBackground, - // inputEditorBackground: inputBackground, - // resultEditorBackground: editorBackground - // })); - // this._widget.render(this._widgetContainer); - // this._register(this._widget.onDidFocus(() => this._chatWidgetFocused.set(true))); - + // The inline chat widget requires a parent editor that it bases the diff view on, since the + // terminal doesn't use that feature we can just pass in an unattached editor instance. const fakeParentEditorElement = document.createElement('div'); - - // const editorConstructionOptions = this.inputEditorOptions.getEditorConstructionOptions(); - // this.setPlaceholderFontStyles(editorConstructionOptions.fontFamily!, editorConstructionOptions.fontSize!, editorConstructionOptions.lineHeight!); - const fakeParentEditor = this._scopedInstantiationService.createInstance( CodeEditorWidget, fakeParentEditorElement, @@ -91,23 +76,18 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.layout(new Dimension(400, 150)); this._widgetContainer.classList.remove('hide'); - // this._widget.setVisible(true); this._chatWidgetFocused.set(true); this._chatWidgetVisible.set(true); - // this._widget.setInput('@terminal'); - // this._widget.setInputPlaceholder('Request a terminal command'); - // this._widget.focusInput(); this._inlineChatWidget.focus(); } hide(): void { this._widgetContainer.classList.add('hide'); this._chatWidgetFocused.set(false); this._chatWidgetVisible.set(false); - // this._widget.clear(); this._instance.focus(); } cancel(): void { - // this._widget?.clear(); + // TODO: Impl } input(): string { return this._inlineChatWidget.value; From 32b3e7ed0e0fb868f3b2ede89ffa94b898f7a4c1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 08:45:29 -0800 Subject: [PATCH 024/228] Standardize chat command ns, add feedback placeholders --- .../contrib/terminal/common/terminal.ts | 11 +-- .../browser/terminal.chat.contribution.ts | 70 +++++++++++++++++-- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 5ed9e674af2..f2167db7f17 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -500,10 +500,13 @@ export const enum TerminalCommandId { FontZoomIn = 'workbench.action.terminal.fontZoomIn', FontZoomOut = 'workbench.action.terminal.fontZoomOut', FontZoomReset = 'workbench.action.terminal.fontZoomReset', - FocusChat = 'workbench.action.terminal.focusChat', - HideChat = 'workbench.action.terminal.hideChat', - MakeChatRequest = 'workbench.action.terminal.makeChatRequest', - CancelChat = 'workbench.action.terminal.cancelChat', + ChatFocus = 'workbench.action.terminal.chat.focus', + ChatHide = 'workbench.action.terminal.chat.close', + ChatMakeRequest = 'workbench.action.terminal.chat.makeRequest', + ChatCancel = 'workbench.action.terminal.chat.cancel', + ChatFeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', + ChatFeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', + ChatFeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', // Developer commands 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 fd88e4c0982..2972104787f 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 @@ -15,13 +15,13 @@ import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); registerActiveXtermAction({ - id: TerminalCommandId.FocusChat, + id: TerminalCommandId.ChatFocus, title: localize2('workbench.action.terminal.focusChat', 'Focus Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, @@ -43,7 +43,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.HideChat, + id: TerminalCommandId.ChatHide, title: localize2('workbench.action.terminal.closeChat', 'Close Chat'), keybinding: { primary: KeyCode.Escape, @@ -72,7 +72,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.MakeChatRequest, + id: TerminalCommandId.ChatMakeRequest, title: localize2('workbench.action.terminal.submitChat', 'Make Chat Request'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -105,7 +105,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.CancelChat, + id: TerminalCommandId.ChatCancel, title: localize2('workbench.action.terminal.cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -124,3 +124,63 @@ registerActiveXtermAction({ contr?.chatWidget?.cancel(); } }); + +registerActiveXtermAction({ + id: TerminalCommandId.ChatFeedbackHelpful, + title: localize2('feedbackHelpful', 'Helpful'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalContextKeys.chatRequestActive, + ), + icon: Codicon.thumbsup, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 1, + // TODO: Fill in ctx + // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + // TODO: Impl + } +}); + +registerActiveXtermAction({ + id: TerminalCommandId.ChatFeedbackUnhelpful, + title: localize2('feedbackUnhelpful', 'Helpful'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalContextKeys.chatRequestActive, + ), + icon: Codicon.thumbsup, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 2, + // TODO: Fill in ctx + // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + // TODO: Impl + } +}); + +registerActiveXtermAction({ + id: TerminalCommandId.ChatFeedbackReportIssue, + title: localize2('reportIssue', 'Report Issue'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalContextKeys.chatRequestActive, + ), + icon: Codicon.thumbsup, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 3, + // TODO: Fill in ctx + // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + // TODO: Impl + } +}); From 611fd5cf88efac77f8fee0aed18bbef8aaeef768 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 08:46:57 -0800 Subject: [PATCH 025/228] Move chat command IDs into contrib --- .../contrib/terminal/common/terminal.ts | 7 ------- .../chat/browser/terminal.chat.contribution.ts | 17 ++++++++--------- .../chat/browser/terminalChat.ts | 10 ++++++++++ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index f2167db7f17..fc925b646bb 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -500,13 +500,6 @@ export const enum TerminalCommandId { FontZoomIn = 'workbench.action.terminal.fontZoomIn', FontZoomOut = 'workbench.action.terminal.fontZoomOut', FontZoomReset = 'workbench.action.terminal.fontZoomReset', - ChatFocus = 'workbench.action.terminal.chat.focus', - ChatHide = 'workbench.action.terminal.chat.close', - ChatMakeRequest = 'workbench.action.terminal.chat.makeRequest', - ChatCancel = 'workbench.action.terminal.chat.cancel', - ChatFeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', - ChatFeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', - ChatFeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', // Developer commands 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 2972104787f..f8a1dd4b111 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 @@ -13,15 +13,14 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; -import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); registerActiveXtermAction({ - id: TerminalCommandId.ChatFocus, + id: TerminalChatCommandId.Focus, title: localize2('workbench.action.terminal.focusChat', 'Focus Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, @@ -43,7 +42,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.ChatHide, + id: TerminalChatCommandId.Hide, title: localize2('workbench.action.terminal.closeChat', 'Close Chat'), keybinding: { primary: KeyCode.Escape, @@ -72,7 +71,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.ChatMakeRequest, + id: TerminalChatCommandId.MakeRequest, title: localize2('workbench.action.terminal.submitChat', 'Make Chat Request'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -105,7 +104,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.ChatCancel, + id: TerminalChatCommandId.Cancel, title: localize2('workbench.action.terminal.cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -126,7 +125,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.ChatFeedbackHelpful, + id: TerminalChatCommandId.FeedbackHelpful, title: localize2('feedbackHelpful', 'Helpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -146,7 +145,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.ChatFeedbackUnhelpful, + id: TerminalChatCommandId.FeedbackUnhelpful, title: localize2('feedbackUnhelpful', 'Helpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -166,7 +165,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalCommandId.ChatFeedbackReportIssue, + id: TerminalChatCommandId.FeedbackReportIssue, title: localize2('reportIssue', 'Report Issue'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 8b74aae7aeb..431a0e9c715 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -5,6 +5,16 @@ import { MenuId } from 'vs/platform/actions/common/actions'; +export const enum TerminalChatCommandId { + Focus = 'workbench.action.terminal.chat.focus', + Hide = 'workbench.action.terminal.chat.close', + MakeRequest = 'workbench.action.terminal.chat.makeRequest', + Cancel = 'workbench.action.terminal.chat.cancel', + FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', + FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', + FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', +} + export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput'); export const MENU_TERMINAL_CHAT_WIDGET = MenuId.for('terminalChatWidget'); export const MENU_TERMINAL_CHAT_WIDGET_STATUS = MenuId.for('terminalChatWidget.status'); From 430db3ac9e053dada843e609422050da6938cf45 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 08:49:30 -0800 Subject: [PATCH 026/228] Move term chat actions into own file --- .../browser/terminal.chat.contribution.ts | 176 +---------------- .../chat/browser/terminalChatActions.ts | 182 ++++++++++++++++++ 2 files changed, 183 insertions(+), 175 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts 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 f8a1dd4b111..a3a7b55757a 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,183 +3,9 @@ * 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 { localize2 } from 'vs/nls'; -import { MenuId } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); -registerActiveXtermAction({ - id: TerminalChatCommandId.Focus, - title: localize2('workbench.action.terminal.focusChat', 'Focus Chat'), - keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.KeyI, - when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), - weight: KeybindingWeight.WorkbenchContrib - }, - f1: true, - precondition: ContextKeyExpr.and( - ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - ), - run: (_xterm, _accessor, activeInstance) => { - if (isDetachedTerminalInstance(activeInstance)) { - return; - } - const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.chatWidget?.reveal(); - } -}); - -registerActiveXtermAction({ - id: TerminalChatCommandId.Hide, - title: localize2('workbench.action.terminal.closeChat', 'Close Chat'), - keybinding: { - primary: KeyCode.Escape, - secondary: [KeyMod.Shift | KeyCode.Escape], - when: ContextKeyExpr.and(TerminalContextKeys.chatFocused, TerminalContextKeys.chatVisible), - weight: KeybindingWeight.WorkbenchContrib - }, - icon: Codicon.close, - menu: { - id: MENU_TERMINAL_CHAT_WIDGET, - group: 'main', - order: 2 - }, - f1: true, - precondition: ContextKeyExpr.and( - ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - ), - run: (_xterm, _accessor, activeInstance) => { - if (isDetachedTerminalInstance(activeInstance)) { - return; - } - const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.chatWidget?.hide(); - } -}); - -registerActiveXtermAction({ - id: TerminalChatCommandId.MakeRequest, - title: localize2('workbench.action.terminal.submitChat', 'Make Chat Request'), - precondition: ContextKeyExpr.and( - ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - // TerminalContextKeys.chatInputHasText - ), - icon: Codicon.send, - keybinding: { - when: TerminalContextKeys.chatRequestActive.negate(), - // TODO: - // when: CTX_INLINE_CHAT_FOCUSED, - weight: KeybindingWeight.EditorCore + 7, - primary: KeyCode.Enter - }, - menu: { - id: MENU_TERMINAL_CHAT_INPUT, - group: 'main', - order: 1, - // when: TerminalContextKeys.chatSessionInProgress.negate(), - // TODO: - // when: CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.isEqualTo(false) - }, - run: (_xterm, _accessor, activeInstance) => { - if (isDetachedTerminalInstance(activeInstance)) { - return; - } - const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.acceptInput(); - } -}); - -registerActiveXtermAction({ - id: TerminalChatCommandId.Cancel, - title: localize2('workbench.action.terminal.cancelChat', 'Cancel Chat'), - precondition: ContextKeyExpr.and( - ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive, - ), - icon: Codicon.debugStop, - menu: { - id: MenuId.ChatExecute, - group: 'navigation', - }, - run: (_xterm, _accessor, activeInstance) => { - if (isDetachedTerminalInstance(activeInstance)) { - return; - } - const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.chatWidget?.cancel(); - } -}); - -registerActiveXtermAction({ - id: TerminalChatCommandId.FeedbackHelpful, - title: localize2('feedbackHelpful', 'Helpful'), - precondition: ContextKeyExpr.and( - ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive, - ), - icon: Codicon.thumbsup, - menu: { - id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 1, - // TODO: Fill in ctx - // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - run: (_xterm, _accessor, activeInstance) => { - // TODO: Impl - } -}); - -registerActiveXtermAction({ - id: TerminalChatCommandId.FeedbackUnhelpful, - title: localize2('feedbackUnhelpful', 'Helpful'), - precondition: ContextKeyExpr.and( - ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive, - ), - icon: Codicon.thumbsup, - menu: { - id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 2, - // TODO: Fill in ctx - // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - run: (_xterm, _accessor, activeInstance) => { - // TODO: Impl - } -}); - -registerActiveXtermAction({ - id: TerminalChatCommandId.FeedbackReportIssue, - title: localize2('reportIssue', 'Report Issue'), - precondition: ContextKeyExpr.and( - ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive, - ), - icon: Codicon.thumbsup, - menu: { - id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 3, - // TODO: Fill in ctx - // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, - run: (_xterm, _accessor, activeInstance) => { - // TODO: Impl - } -}); +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts new file mode 100644 index 00000000000..161b8515478 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -0,0 +1,182 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize2 } from 'vs/nls'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +registerActiveXtermAction({ + id: TerminalChatCommandId.Focus, + title: localize2('workbench.action.terminal.focusChat', 'Focus Chat'), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.KeyI, + when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), + weight: KeybindingWeight.WorkbenchContrib + }, + f1: true, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.chatWidget?.reveal(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.Hide, + title: localize2('workbench.action.terminal.closeChat', 'Close Chat'), + keybinding: { + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape], + when: ContextKeyExpr.and(TerminalContextKeys.chatFocused, TerminalContextKeys.chatVisible), + weight: KeybindingWeight.WorkbenchContrib + }, + icon: Codicon.close, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'main', + order: 2 + }, + f1: true, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.chatWidget?.hide(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.MakeRequest, + title: localize2('workbench.action.terminal.submitChat', 'Make Chat Request'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + // TerminalContextKeys.chatInputHasText + ), + icon: Codicon.send, + keybinding: { + when: TerminalContextKeys.chatRequestActive.negate(), + // TODO: + // when: CTX_INLINE_CHAT_FOCUSED, + weight: KeybindingWeight.EditorCore + 7, + primary: KeyCode.Enter + }, + menu: { + id: MENU_TERMINAL_CHAT_INPUT, + group: 'main', + order: 1, + // when: TerminalContextKeys.chatSessionInProgress.negate(), + // TODO: + // when: CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.isEqualTo(false) + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptInput(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.Cancel, + title: localize2('workbench.action.terminal.cancelChat', 'Cancel Chat'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalContextKeys.chatRequestActive, + ), + icon: Codicon.debugStop, + menu: { + id: MenuId.ChatExecute, + group: 'navigation', + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.chatWidget?.cancel(); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackHelpful, + title: localize2('feedbackHelpful', 'Helpful'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalContextKeys.chatRequestActive, + ), + icon: Codicon.thumbsup, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 1, + // TODO: Fill in ctx + // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + // TODO: Impl + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackUnhelpful, + title: localize2('feedbackUnhelpful', 'Helpful'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalContextKeys.chatRequestActive, + ), + icon: Codicon.thumbsup, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 2, + // TODO: Fill in ctx + // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + // TODO: Impl + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.FeedbackReportIssue, + title: localize2('reportIssue', 'Report Issue'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + TerminalContextKeys.chatRequestActive, + ), + icon: Codicon.thumbsup, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + group: 'inline', + order: 3, + // TODO: Fill in ctx + // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), + }, + run: (_xterm, _accessor, activeInstance) => { + // TODO: Impl + } +}); From ab50eb72b355613d6f028289e41fccb4fcd9dc91 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:01:19 -0800 Subject: [PATCH 027/228] Fix compile error, add todo for layer breaker --- .../contrib/chat/electron-sandbox/actions/voiceChatActions.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 b41804eb8cc..74ec952ba1c 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -54,6 +54,7 @@ import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ThemeIcon } from 'vs/base/common/themables'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { MENU_TERMINAL_CHAT_INPUT } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; 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.") }); @@ -528,7 +529,8 @@ export class StartVoiceChatAction extends Action2 { order: -1 }, { - id: MenuId.TerminalChat, + // TODO: Fix layer breaker, chat can't depend on terminalContrib + id: MENU_TERMINAL_CHAT_INPUT, when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate()), group: 'main', order: -1 From 283b211fc8544baaa577021b2d507fb966ac1975 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 11:09:21 -0600 Subject: [PATCH 028/228] alert single code block --- .../chat/browser/terminalChatController.ts | 13 ++++++++----- .../chat/browser/terminalChatWidget.ts | 4 +--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 8308105d9f5..c8f3ec69958 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -19,6 +19,7 @@ import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { generateUuid } from 'vs/base/common/uuid'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { marked } from 'vs/base/common/marked/marked'; export class TerminalChatController extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; @@ -133,12 +134,14 @@ export class TerminalChatController extends Disposable implements ITerminalContr variables: resolvedVariables, variables2: { message: this._chatWidget?.rawValue?.input() || '', variables: [] } }; - // TODO: use token - const agentResult = await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], CancellationToken.None); - console.log(agentResult); - console.log(message); - alert(message); this._chatWidget?.rawValue?.setValue(); + + // TODO: use token + await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], CancellationToken.None); + const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); + if (codeBlock) { + alert(codeBlock); + } // TODO: accessibility announcement, help dialog } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 330be0b35a5..4a5dbb55994 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -11,7 +11,6 @@ import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -32,8 +31,7 @@ export class TerminalChatWidget extends Disposable { private readonly _container: HTMLElement, private readonly _instance: ITerminalInstance, @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService + @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); From 582b0bea5a9e5f9090573fa3a2b4dd8587030100 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 11:29:51 -0600 Subject: [PATCH 029/228] add response editor --- .../terminal/browser/media/terminal.css | 4 ++ .../chat/browser/terminalChatController.ts | 6 +-- .../chat/browser/terminalChatWidget.ts | 42 ++++++++++++++++++- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 8fcb3c71172..9053a746f89 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -575,3 +575,7 @@ .monaco-workbench .terminal-inline-chat.hide { visibility: hidden; } + +.monaco-workbench .terminal-inline-chat-response.hide { + visibility: hidden; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index c8f3ec69958..05383e3e7f8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -34,7 +34,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr static activeChatWidget?: TerminalChatController; private _chatWidget: Lazy | undefined; private _lastLayoutDimensions: IDimension | undefined; - + private _requestId: number = 0; get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } // private _sessionCtor: CancelablePromise | undefined; @@ -140,9 +140,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], CancellationToken.None); const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); if (codeBlock) { - alert(codeBlock); + // TODO: check the SR experience + this._chatWidget?.rawValue?.renderResponse(codeBlock, this._requestId++); } - // TODO: accessibility announcement, help dialog } reveal(): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 4a5dbb55994..88f17fe5c58 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -5,12 +5,16 @@ import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/terminalChatWidget'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -23,7 +27,8 @@ export class TerminalChatWidget extends Disposable { private _chatWidgetVisible: IContextKey; private readonly _inlineChatWidget: InlineChatWidget; - + private _responseWidget: CodeEditorWidget | undefined; + private _responseContainer: HTMLElement | undefined; private readonly _focusTracker: IFocusTracker; @@ -31,7 +36,9 @@ export class TerminalChatWidget extends Disposable { private readonly _container: HTMLElement, private readonly _instance: ITerminalInstance, @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, + @IModelService private readonly _modelService: IModelService ) { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); @@ -70,6 +77,33 @@ export class TerminalChatWidget extends Disposable { this._focusTracker = this._register(trackFocus(this._widgetContainer)); } + renderResponse(codeBlock: string, requestId: number): void { + this._chatAccessibilityService.acceptResponse(codeBlock, requestId); + if (!this._responseWidget) { + this._responseContainer = document.createElement('div'); + this._responseContainer.classList.add('terminal-inline-chat-response'); + this._responseWidget = this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseContainer, {}, { isSimpleWidget: true }); + this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: codeBlock })).then((model) => { + if (!model || !this._responseWidget) { + return; + } + this._responseWidget.setModel(model); + this._responseWidget.layout(new Dimension(400, 150)); + this._widgetContainer.prepend(this._responseContainer!); + }); + } else { + this._responseWidget.setValue(codeBlock); + } + this._responseContainer?.classList.remove('hide'); + } + + private async _getTextModel(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing && !existing.isDisposed()) { + return existing; + } + return this._modelService.createModel(resource.fragment, null, resource, false); + } reveal(): void { this._inlineChatWidget.layout(new Dimension(400, 150)); @@ -79,6 +113,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.focus(); } hide(): void { + this._responseContainer?.classList.add('hide'); this._widgetContainer.classList.add('hide'); this._chatWidgetFocused.set(false); this._chatWidgetVisible.set(false); @@ -92,6 +127,9 @@ export class TerminalChatWidget extends Disposable { } setValue(value?: string) { this._inlineChatWidget.value = value ?? ''; + if (!value) { + this._responseContainer?.classList.add('hide'); + } } // async acceptInput(): Promise { // // this._widget?.acceptInput(); From c4519ce005d7e3f1d52a280d022b1cab5df4aa00 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 11:44:13 -0600 Subject: [PATCH 030/228] wip render message when no code block comes through --- .../terminal/browser/media/terminal.css | 4 +++ .../chat/browser/terminalChatController.ts | 5 +++- .../chat/browser/terminalChatWidget.ts | 26 ++++++++++++------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 9053a746f89..ab84f0258b1 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -579,3 +579,7 @@ .monaco-workbench .terminal-inline-chat-response.hide { visibility: hidden; } + +.monaco-workbench .terminal-inline-chat-response.message { + width: 400px !important; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 05383e3e7f8..c4755a08c47 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -139,9 +139,12 @@ export class TerminalChatController extends Disposable implements ITerminalContr // TODO: use token await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], CancellationToken.None); const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); + this._requestId++; if (codeBlock) { // TODO: check the SR experience - this._chatWidget?.rawValue?.renderResponse(codeBlock, this._requestId++); + this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._requestId); + } else { + this._chatWidget?.rawValue?.renderMessage(message, this._requestId); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 88f17fe5c58..476c808dce1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -28,7 +28,7 @@ export class TerminalChatWidget extends Disposable { private readonly _inlineChatWidget: InlineChatWidget; private _responseWidget: CodeEditorWidget | undefined; - private _responseContainer: HTMLElement | undefined; + private _responseElement: HTMLElement; private readonly _focusTracker: IFocusTracker; @@ -49,6 +49,10 @@ export class TerminalChatWidget extends Disposable { this._widgetContainer.classList.add('terminal-inline-chat'); this._container.appendChild(this._widgetContainer); + this._responseElement = document.createElement('div'); + this._responseElement.classList.add('terminal-inline-chat-response'); + this._widgetContainer.prepend(this._responseElement); + // The inline chat widget requires a parent editor that it bases the diff view on, since the // terminal doesn't use that feature we can just pass in an unattached editor instance. const fakeParentEditorElement = document.createElement('div'); @@ -77,24 +81,28 @@ export class TerminalChatWidget extends Disposable { this._focusTracker = this._register(trackFocus(this._widgetContainer)); } - renderResponse(codeBlock: string, requestId: number): void { + renderTerminalCommand(codeBlock: string, requestId: number): void { + this._responseElement.classList.remove('message', 'hide'); this._chatAccessibilityService.acceptResponse(codeBlock, requestId); if (!this._responseWidget) { - this._responseContainer = document.createElement('div'); - this._responseContainer.classList.add('terminal-inline-chat-response'); - this._responseWidget = this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseContainer, {}, { isSimpleWidget: true }); + this._responseWidget = this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, {}, { isSimpleWidget: true }); this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: codeBlock })).then((model) => { if (!model || !this._responseWidget) { return; } this._responseWidget.setModel(model); this._responseWidget.layout(new Dimension(400, 150)); - this._widgetContainer.prepend(this._responseContainer!); }); } else { this._responseWidget.setValue(codeBlock); } - this._responseContainer?.classList.remove('hide'); + } + + renderMessage(message: string, requestId: number): void { + this._responseElement?.classList.remove('hide'); + this._responseElement.classList.add('message'); + this._chatAccessibilityService.acceptResponse(message, requestId); + this._responseElement.textContent = message; } private async _getTextModel(resource: URI): Promise { @@ -113,7 +121,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.focus(); } hide(): void { - this._responseContainer?.classList.add('hide'); + this._responseElement?.classList.add('hide'); this._widgetContainer.classList.add('hide'); this._chatWidgetFocused.set(false); this._chatWidgetVisible.set(false); @@ -128,7 +136,7 @@ export class TerminalChatWidget extends Disposable { setValue(value?: string) { this._inlineChatWidget.value = value ?? ''; if (!value) { - this._responseContainer?.classList.add('hide'); + this._responseElement?.classList.add('hide'); } } // async acceptInput(): Promise { From c9c98f010a12da31afa70b7b60b280260f911bac Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 11:56:08 -0600 Subject: [PATCH 031/228] get chat cancellation action to show up --- .../chat/browser/terminalChatActions.ts | 7 ++--- .../chat/browser/terminalChatController.ts | 29 +++++++++++++++---- .../chat/browser/terminalChatWidget.ts | 2 +- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 161b8515478..b9544ce5feb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -6,7 +6,6 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize2 } from 'vs/nls'; -import { MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; @@ -73,7 +72,7 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - // TerminalContextKeys.chatInputHasText + TerminalContextKeys.chatRequestActive.negate() ), icon: Codicon.send, keybinding: { @@ -109,8 +108,8 @@ registerActiveXtermAction({ ), icon: Codicon.debugStop, menu: { - id: MenuId.ChatExecute, - group: 'navigation', + id: MENU_TERMINAL_CHAT_INPUT, + group: 'main', }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index c4755a08c47..5465c3abed3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -17,9 +17,12 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { generateUuid } from 'vs/base/common/uuid'; -import { CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; import { marked } from 'vs/base/common/marked/marked'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; export class TerminalChatController extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; @@ -39,7 +42,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr // private _sessionCtor: CancelablePromise | undefined; // private _activeSession?: Session; - // private readonly _ctxHasActiveRequest: IContextKey; + private readonly _ctxHasActiveRequest!: IContextKey; + + private _cancellationTokenSource!: CancellationTokenSource; + // private _isVisible: boolean = false; // private _strategy: EditStrategy | undefined; @@ -56,7 +62,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr @ITerminalService private readonly _terminalService: ITerminalService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, - // @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService // @IInstantiationService private readonly _instantiationService: IInstantiationService, // @ICommandService private readonly _commandService: ICommandService, // @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService @@ -65,7 +72,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { return; } - // this._ctxHasActiveRequest = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); + this._ctxHasActiveRequest = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); + this._cancellationTokenSource = new CancellationTokenSource(); // this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService); } @@ -105,8 +113,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr }); } + cancel(): void { + this._cancellationTokenSource.cancel(); + } + async acceptInput(): Promise { let message = ''; + this._chatAccessibilityService.acceptRequest(); + this._ctxHasActiveRequest.set(true); + const cancellationToken = this._cancellationTokenSource.token; const progressCallback = (progress: IChatProgress) => { // if (token.isCancellationRequested) { // return; @@ -137,15 +152,19 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.setValue(); // TODO: use token - await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], CancellationToken.None); + await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], cancellationToken); const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); this._requestId++; + if (cancellationToken.isCancellationRequested) { + return; + } if (codeBlock) { // TODO: check the SR experience this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._requestId); } else { this._chatWidget?.rawValue?.renderMessage(message, this._requestId); } + this._ctxHasActiveRequest.set(false); } reveal(): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 476c808dce1..4c84b3656bc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -31,7 +31,6 @@ export class TerminalChatWidget extends Disposable { private _responseElement: HTMLElement; private readonly _focusTracker: IFocusTracker; - constructor( private readonly _container: HTMLElement, private readonly _instance: ITerminalInstance, @@ -129,6 +128,7 @@ export class TerminalChatWidget extends Disposable { } cancel(): void { // TODO: Impl + this._inlineChatWidget.value = ''; } input(): string { return this._inlineChatWidget.value; From cf634dfa386325d041f05d86cf24a2654defc04a Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 11:57:24 -0600 Subject: [PATCH 032/228] only show if active --- .../terminalContrib/chat/browser/terminalChatActions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index b9544ce5feb..57e3cee5de0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -86,7 +86,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_INPUT, group: 'main', order: 1, - // when: TerminalContextKeys.chatSessionInProgress.negate(), + when: TerminalContextKeys.chatRequestActive.negate(), // TODO: // when: CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.isEqualTo(false) }, @@ -110,6 +110,7 @@ registerActiveXtermAction({ menu: { id: MENU_TERMINAL_CHAT_INPUT, group: 'main', + when: TerminalContextKeys.chatRequestActive, }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { From 7d80e6d9396dc3bf91d8880cd65758a31e3cef81 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 12:02:49 -0600 Subject: [PATCH 033/228] clean up --- .../chat/browser/terminalChatActions.ts | 2 -- .../chat/browser/terminalChatController.ts | 19 +++++-------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 57e3cee5de0..bb1a012026a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -87,8 +87,6 @@ registerActiveXtermAction({ group: 'main', order: 1, when: TerminalContextKeys.chatRequestActive.negate(), - // TODO: - // when: CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.isEqualTo(false) }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 5465c3abed3..a9babf3a5de 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -123,21 +123,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._ctxHasActiveRequest.set(true); const cancellationToken = this._cancellationTokenSource.token; const progressCallback = (progress: IChatProgress) => { - // if (token.isCancellationRequested) { - // return; - // } - - - // gotProgress = true; - - if (progress.kind === 'content' || progress.kind === 'markdownContent') { - // this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${typeof progress.content === 'string' ? progress.content.length : progress.content.value.length} chars`); - message += progress.content; - } else { - // this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progress)}`); + if (cancellationToken.isCancellationRequested) { + return; } - // model.acceptResponseProgress(request, progress); + if (progress.kind === 'content' || progress.kind === 'markdownContent') { + message += progress.content; + } }; const resolvedVariables: Record = {}; @@ -151,7 +143,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr }; this._chatWidget?.rawValue?.setValue(); - // TODO: use token await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], cancellationToken); const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); this._requestId++; From 44d4700aae178618cbb9aa6f888e0b81db5e66f2 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 12:03:37 -0600 Subject: [PATCH 034/228] remove things --- .../chat/browser/terminalChatController.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index a9babf3a5de..22810165660 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -40,20 +40,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr private _requestId: number = 0; get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } - // private _sessionCtor: CancelablePromise | undefined; - // private _activeSession?: Session; private readonly _ctxHasActiveRequest!: IContextKey; private _cancellationTokenSource!: CancellationTokenSource; - // private _isVisible: boolean = false; - // private _strategy: EditStrategy | undefined; - - // private _inlineChatListener: IDisposable | undefined; - // private _toolbar: MenuWorkbenchToolBar | undefined; - // private readonly _ctxLastResponseType: IContextKey; - // private _widgetDisposableStore: DisposableStore = this._register(new DisposableStore()); - constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, @@ -64,9 +54,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService - // @IInstantiationService private readonly _instantiationService: IInstantiationService, - // @ICommandService private readonly _commandService: ICommandService, - // @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService ) { super(); if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { @@ -74,7 +61,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr } this._ctxHasActiveRequest = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); this._cancellationTokenSource = new CancellationTokenSource(); - // this._ctxLastResponseType = CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.bindTo(this._contextKeyService); } layout(_xterm: IXtermTerminal & { raw: RawXtermTerminal }, dimension: IDimension): void { From c4af34eae27344c9bf3030f7c84d2f2e0cf69d8d Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 12:04:44 -0600 Subject: [PATCH 035/228] more clean up --- .../terminalContrib/chat/browser/terminalChatController.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 22810165660..85d7ba44296 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -108,6 +108,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatAccessibilityService.acceptRequest(); this._ctxHasActiveRequest.set(true); const cancellationToken = this._cancellationTokenSource.token; + const agentId = 'terminal'; const progressCallback = (progress: IChatProgress) => { if (cancellationToken.isCancellationRequested) { return; @@ -122,14 +123,14 @@ export class TerminalChatController extends Disposable implements ITerminalContr const requestProps: IChatAgentRequest = { sessionId: generateUuid(), requestId: generateUuid(), - agentId: 'terminal', + agentId, message: this._chatWidget?.rawValue?.input() || '', variables: resolvedVariables, variables2: { message: this._chatWidget?.rawValue?.input() || '', variables: [] } }; this._chatWidget?.rawValue?.setValue(); - await this._chatAgentService.invokeAgent('terminal', requestProps, progressCallback, [], cancellationToken); + await this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, [], cancellationToken); const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); this._requestId++; if (cancellationToken.isCancellationRequested) { From a9cfba287544762341fa7a2e84fce90f568f4cbb Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 12:11:41 -0600 Subject: [PATCH 036/228] catch error --- .../chat/browser/terminalChatController.ts | 11 ++++++++++- .../chat/browser/terminalChatWidget.ts | 18 ++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 85d7ba44296..432d5fd0cbf 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -117,6 +117,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (progress.kind === 'content' || progress.kind === 'markdownContent') { message += progress.content; } + this._chatWidget?.rawValue?.updateProgress(progress); }; const resolvedVariables: Record = {}; @@ -130,7 +131,14 @@ export class TerminalChatController extends Disposable implements ITerminalContr }; this._chatWidget?.rawValue?.setValue(); - await this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, [], cancellationToken); + try { + await this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, [], cancellationToken); + } catch (e) { + // Provider is not ready + this._ctxHasActiveRequest.set(false); + this._chatWidget?.rawValue?.updateProgress(); + return; + } const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); this._requestId++; if (cancellationToken.isCancellationRequested) { @@ -143,6 +151,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.renderMessage(message, this._requestId); } this._ctxHasActiveRequest.set(false); + this._chatWidget?.rawValue?.updateProgress(); } reveal(): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 4c84b3656bc..985fc071cb2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -15,6 +15,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -139,20 +140,9 @@ export class TerminalChatWidget extends Disposable { this._responseElement?.classList.add('hide'); } } - // async acceptInput(): Promise { - // // this._widget?.acceptInput(); - // // this._chatModel ??= this._chatService.startSession('terminal', CancellationToken.None); - - // // if (!this._model) { - // // throw new Error('Could not start chat session'); - // // } - // // this._chatService?.sendRequest(this._chatModel?.sessionId!, this._inlineChatWidget.value); - // // this._activeSession = new Session(EditMode.Live, , this._instance); - // // const initVariableData: IChatRequestVariableData = { message: getPromptText(parsedRequest.parts), variables: {} }; - // // request = model.addRequest(parsedRequest, initVariableData, agent, agentSlashCommandPart?.command); - // // const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, token); - // this._inlineChatWidget.value = ''; - // } + updateProgress(progress?: IChatProgress): void { + this._inlineChatWidget.updateProgress(progress?.kind === 'content' || progress?.kind === 'markdownContent'); + } layout(width: number): void { // this._widget?.layout(100, width < 300 ? 300 : width); } From 1617ebe843cb56d7ff8fdd2c17c3fbaa2de9f7ec Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 12:24:10 -0600 Subject: [PATCH 037/228] ensure terminal agent is registered --- .../contrib/terminal/common/terminalContextKey.ts | 4 ++++ .../chat/browser/terminalChatActions.ts | 4 +++- .../chat/browser/terminalChatController.ts | 11 +++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 78fd7df6712..7f777f94f62 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -43,6 +43,7 @@ export const enum TerminalContextKeyStrings { ChatVisible = 'terminalChatVisible', ChatActiveRequest = 'terminalChatActiveRequest', ChatInputHasText = 'terminalChatInputHasText', + ChatAgentRegistered = 'terminalChatAgentRegistered', } export namespace TerminalContextKeys { @@ -175,4 +176,7 @@ export namespace TerminalContextKeys { /** Whether the chat input has text */ export const chatInputHasText = new RawContextKey(TerminalContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); + + /** Whether the terminal chat agent has been registered */ + export const chatAgentRegistered = new RawContextKey(TerminalContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index bb1a012026a..dcf2aec0617 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -72,7 +72,8 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalContextKeys.chatRequestActive.negate() + TerminalContextKeys.chatRequestActive.negate(), + TerminalContextKeys.chatAgentRegistered ), icon: Codicon.send, keybinding: { @@ -103,6 +104,7 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatRequestActive, + TerminalContextKeys.chatAgentRegistered ), icon: Codicon.debugStop, menu: { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 432d5fd0cbf..b3b9c002aad 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -41,6 +41,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } private readonly _ctxHasActiveRequest!: IContextKey; + private readonly _ctxHasTerminalAgent!: IContextKey; private _cancellationTokenSource!: CancellationTokenSource; @@ -60,6 +61,16 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } this._ctxHasActiveRequest = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); + this._ctxHasTerminalAgent = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); + if (!this._chatAgentService.hasAgent('terminal')) { + this._register(this._chatAgentService.onDidChangeAgents(() => { + if (this._chatAgentService.getAgent('terminal')) { + this._ctxHasTerminalAgent.set(true); + } + })); + } else { + this._ctxHasTerminalAgent.set(true); + } this._cancellationTokenSource = new CancellationTokenSource(); } From d5527a86b26e1586b487dfaadc43001c32b4bd64 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 12:56:50 -0600 Subject: [PATCH 038/228] add view in chat action even though it doesn't work atm --- .../inlineChat/browser/inlineChatActions.ts | 17 +++++++++++++---- .../terminal/browser/media/terminal.css | 2 +- .../chat/browser/terminalChatActions.ts | 8 ++++---- .../chat/browser/terminalChatController.ts | 18 +++++++++++------- .../chat/browser/terminalChatWidget.ts | 11 +++++------ 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 85033c64166..635233002c4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -30,6 +30,9 @@ 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'; +// TODO: fix eslint-disable-next-line local/code-import-patterns +import { MENU_TERMINAL_CHAT_INPUT } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -682,13 +685,19 @@ export class ViewInChatAction extends AbstractInlineChatAction { id: ACTION_VIEW_IN_CHAT, title: localize('viewInChat', 'View in Chat'), icon: Codicon.commentDiscussion, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { + precondition: ContextKeyExpr.or(TerminalContextKeys.chatVisible, CTX_INLINE_CHAT_VISIBLE), + menu: [{ id: MENU_INLINE_CHAT_WIDGET_STATUS, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_VISIBLE), group: '0_main', order: 1 - } + }, + { + id: MENU_TERMINAL_CHAT_INPUT, + when: ContextKeyExpr.and(TerminalContextKeys.chatVisible, CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages)), + group: 'inline', + order: 1 + }] }); } override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index ab84f0258b1..1aaf1516c5e 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -580,6 +580,6 @@ visibility: hidden; } -.monaco-workbench .terminal-inline-chat-response.message { +.monaco-workbench .terminal-inline-chat .chatMessageContent { width: 400px !important; } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index dcf2aec0617..60032254028 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -9,6 +9,7 @@ import { localize2 } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -73,13 +74,12 @@ registerActiveXtermAction({ ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.chatRequestActive.negate(), - TerminalContextKeys.chatAgentRegistered + TerminalContextKeys.chatAgentRegistered, + CTX_INLINE_CHAT_EMPTY.negate() ), icon: Codicon.send, keybinding: { - when: TerminalContextKeys.chatRequestActive.negate(), - // TODO: - // when: CTX_INLINE_CHAT_FOCUSED, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalContextKeys.chatRequestActive.negate()), weight: KeybindingWeight.EditorCore + 7, primary: KeyCode.Enter }, diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index b3b9c002aad..9312e56e19b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -23,6 +23,7 @@ import { marked } from 'vs/base/common/marked/marked'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; +import { CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; export class TerminalChatController extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; @@ -37,11 +38,12 @@ export class TerminalChatController extends Disposable implements ITerminalContr static activeChatWidget?: TerminalChatController; private _chatWidget: Lazy | undefined; private _lastLayoutDimensions: IDimension | undefined; - private _requestId: number = 0; + private _accessibilityRequestId: number = 0; get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } private readonly _ctxHasActiveRequest!: IContextKey; private readonly _ctxHasTerminalAgent!: IContextKey; + private readonly _ctxLastResponseType!: IContextKey; private _cancellationTokenSource!: CancellationTokenSource; @@ -54,7 +56,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService + @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, ) { super(); if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { @@ -62,6 +64,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } this._ctxHasActiveRequest = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); this._ctxHasTerminalAgent = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); + this._ctxLastResponseType = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(this._contextKeyService); if (!this._chatAgentService.hasAgent('terminal')) { this._register(this._chatAgentService.onDidChangeAgents(() => { if (this._chatAgentService.getAgent('terminal')) { @@ -131,10 +134,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.updateProgress(progress); }; const resolvedVariables: Record = {}; - + const requestId = generateUuid(); const requestProps: IChatAgentRequest = { sessionId: generateUuid(), - requestId: generateUuid(), + requestId, agentId, message: this._chatWidget?.rawValue?.input() || '', variables: resolvedVariables, @@ -151,15 +154,16 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); - this._requestId++; + this._accessibilityRequestId++; if (cancellationToken.isCancellationRequested) { return; } if (codeBlock) { // TODO: check the SR experience - this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._requestId); + this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._accessibilityRequestId); } else { - this._chatWidget?.rawValue?.renderMessage(message, this._requestId); + this._chatWidget?.rawValue?.renderMessage(message, this._accessibilityRequestId, requestId); + this._ctxLastResponseType.set(InlineChatResponseTypes.OnlyMessages); } this._ctxHasActiveRequest.set(false); this._chatWidget?.rawValue?.updateProgress(); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 985fc071cb2..761708aec35 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; +import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/terminalChatWidget'; @@ -82,8 +83,8 @@ export class TerminalChatWidget extends Disposable { this._focusTracker = this._register(trackFocus(this._widgetContainer)); } renderTerminalCommand(codeBlock: string, requestId: number): void { - this._responseElement.classList.remove('message', 'hide'); this._chatAccessibilityService.acceptResponse(codeBlock, requestId); + this._responseElement.classList.remove('hide'); if (!this._responseWidget) { this._responseWidget = this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, {}, { isSimpleWidget: true }); this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: codeBlock })).then((model) => { @@ -98,11 +99,9 @@ export class TerminalChatWidget extends Disposable { } } - renderMessage(message: string, requestId: number): void { - this._responseElement?.classList.remove('hide'); - this._responseElement.classList.add('message'); - this._chatAccessibilityService.acceptResponse(message, requestId); - this._responseElement.textContent = message; + renderMessage(message: string, accessibilityRequestId: number, requestId: string): void { + this._inlineChatWidget.updateChatMessage({ message: new MarkdownString(message), requestId, providerId: 'terminal' }); + this._chatAccessibilityService.acceptResponse(message, accessibilityRequestId); } private async _getTextModel(resource: URI): Promise { From 9af101d1807a52d95714f19621c86425b342ba01 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 12:59:40 -0600 Subject: [PATCH 039/228] revert View in Chat action since it doesn't work --- .../inlineChat/browser/inlineChatActions.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 635233002c4..85033c64166 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -30,9 +30,6 @@ 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'; -// TODO: fix eslint-disable-next-line local/code-import-patterns -import { MENU_TERMINAL_CHAT_INPUT } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -685,19 +682,13 @@ export class ViewInChatAction extends AbstractInlineChatAction { id: ACTION_VIEW_IN_CHAT, title: localize('viewInChat', 'View in Chat'), icon: Codicon.commentDiscussion, - precondition: ContextKeyExpr.or(TerminalContextKeys.chatVisible, CTX_INLINE_CHAT_VISIBLE), - menu: [{ + precondition: CTX_INLINE_CHAT_VISIBLE, + menu: { id: MENU_INLINE_CHAT_WIDGET_STATUS, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_VISIBLE), + when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), group: '0_main', order: 1 - }, - { - id: MENU_TERMINAL_CHAT_INPUT, - when: ContextKeyExpr.and(TerminalContextKeys.chatVisible, CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages)), - group: 'inline', - order: 1 - }] + } }); } override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { From 7ad358bdb5ee2688a7df65fbd1381fb8ebffb0e7 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 13:14:43 -0600 Subject: [PATCH 040/228] add accept command action --- .../chat/browser/terminalChat.ts | 1 + .../chat/browser/terminalChatActions.ts | 36 ++++++++++++++++++- .../chat/browser/terminalChatController.ts | 5 +++ .../chat/browser/terminalChatWidget.ts | 13 +++++-- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 431a0e9c715..882baf8c26b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -13,6 +13,7 @@ export const enum TerminalChatCommandId { FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', + AcceptCommand = 'workbench.action.terminal.chat.acceptCommand', } export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 60032254028..73ebf9631a9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -9,7 +9,7 @@ import { localize2 } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -67,6 +67,40 @@ registerActiveXtermAction({ } }); + + + +registerActiveXtermAction({ + id: TerminalChatCommandId.AcceptCommand, + title: localize2('workbench.action.terminal.acceptCommand', 'Accept Command'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalContextKeys.chatRequestActive.negate(), + TerminalContextKeys.chatAgentRegistered, + CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyEdits) + ), + icon: Codicon.check, + keybinding: { + when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalContextKeys.chatRequestActive.negate()), + weight: KeybindingWeight.EditorCore + 7, + primary: KeyCode.Enter + }, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'main', + order: 0, + when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyEdits), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptCommand(); + } +}); + registerActiveXtermAction({ id: TerminalChatCommandId.MakeRequest, title: localize2('workbench.action.terminal.submitChat', 'Make Chat Request'), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 9312e56e19b..0a943b362fd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -161,6 +161,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (codeBlock) { // TODO: check the SR experience this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._accessibilityRequestId); + this._ctxLastResponseType.set(InlineChatResponseTypes.OnlyEdits); } else { this._chatWidget?.rawValue?.renderMessage(message, this._accessibilityRequestId, requestId); this._ctxLastResponseType.set(InlineChatResponseTypes.OnlyMessages); @@ -169,6 +170,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.updateProgress(); } + acceptCommand(): void { + this._chatWidget?.rawValue?.acceptCommand(); + } + reveal(): void { this._chatWidget?.rawValue?.reveal(); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 761708aec35..0ccea8a61cd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -91,8 +91,10 @@ export class TerminalChatWidget extends Disposable { if (!model || !this._responseWidget) { return; } + this._responseWidget.layout(new Dimension(400, 0)); this._responseWidget.setModel(model); - this._responseWidget.layout(new Dimension(400, 150)); + const height = this._responseWidget.getContentHeight(); + this._responseWidget.layout(new Dimension(400, height)); }); } else { this._responseWidget.setValue(codeBlock); @@ -113,7 +115,6 @@ export class TerminalChatWidget extends Disposable { } reveal(): void { this._inlineChatWidget.layout(new Dimension(400, 150)); - this._widgetContainer.classList.remove('hide'); this._chatWidgetFocused.set(true); this._chatWidgetVisible.set(true); @@ -139,6 +140,14 @@ export class TerminalChatWidget extends Disposable { this._responseElement?.classList.add('hide'); } } + acceptCommand(): void { + const value = this._responseWidget?.getValue(); + if (!value) { + return; + } + this._instance.sendText(value, false, true); + this.hide(); + } updateProgress(progress?: IChatProgress): void { this._inlineChatWidget.updateProgress(progress?.kind === 'content' || progress?.kind === 'markdownContent'); } From 9992d7da51e1d97858721263038a82e6c80149cd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 11:18:10 -0800 Subject: [PATCH 041/228] Fix terminal voice menu ids --- .../chat/electron-sandbox/actions/voiceChatActions.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 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 74ec952ba1c..e41ae049b6e 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -54,7 +54,6 @@ import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ThemeIcon } from 'vs/base/common/themables'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { MENU_TERMINAL_CHAT_INPUT } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; 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.") }); @@ -529,8 +528,7 @@ export class StartVoiceChatAction extends Action2 { order: -1 }, { - // TODO: Fix layer breaker, chat can't depend on terminalContrib - id: MENU_TERMINAL_CHAT_INPUT, + id: MenuId.for('terminalChatInput'), when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate()), group: 'main', order: -1 @@ -580,6 +578,11 @@ export class InstallVoiceChatAction extends Action2 { when: HasSpeechProvider.negate(), group: 'main', order: -1 + }, { + id: MenuId.for('terminalChatInput'), + when: HasSpeechProvider.negate(), + group: 'main', + order: -1 }] }); } From 702535987ca16b9ba1ed8d86443a1d24a5400538 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 13:29:56 -0600 Subject: [PATCH 042/228] add accept command keybinding --- .../terminal/common/terminalContextKey.ts | 4 +++ .../chat/browser/terminalChatActions.ts | 8 ++--- .../chat/browser/terminalChatController.ts | 2 +- .../chat/browser/terminalChatWidget.ts | 35 ++++++++++++------- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 7f777f94f62..8ea1469a4b1 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -44,6 +44,7 @@ export const enum TerminalContextKeyStrings { ChatActiveRequest = 'terminalChatActiveRequest', ChatInputHasText = 'terminalChatInputHasText', ChatAgentRegistered = 'terminalChatAgentRegistered', + ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', } export namespace TerminalContextKeys { @@ -179,4 +180,7 @@ export namespace TerminalContextKeys { /** Whether the terminal chat agent has been registered */ export const chatAgentRegistered = new RawContextKey(TerminalContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); + + /** Whether the chat response editor is focused */ + export const chatResponseEditorFocused = new RawContextKey(TerminalContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 73ebf9631a9..37cf45c77fc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -78,19 +78,19 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.chatRequestActive.negate(), TerminalContextKeys.chatAgentRegistered, - CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyEdits) + CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty) ), icon: Codicon.check, keybinding: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.chatResponseEditorFocused, TerminalContextKeys.chatRequestActive.negate()), weight: KeybindingWeight.EditorCore + 7, - primary: KeyCode.Enter + primary: KeyMod.CtrlCmd | KeyCode.Enter, }, menu: { id: MENU_TERMINAL_CHAT_WIDGET, group: 'main', order: 0, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyEdits), + when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 0a943b362fd..00b117d6661 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -65,6 +65,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._ctxHasActiveRequest = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); this._ctxHasTerminalAgent = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); this._ctxLastResponseType = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(this._contextKeyService); + if (!this._chatAgentService.hasAgent('terminal')) { this._register(this._chatAgentService.onDidChangeAgents(() => { if (this._chatAgentService.getAgent('terminal')) { @@ -161,7 +162,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (codeBlock) { // TODO: check the SR experience this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._accessibilityRequestId); - this._ctxLastResponseType.set(InlineChatResponseTypes.OnlyEdits); } else { this._chatWidget?.rawValue?.renderMessage(message, this._accessibilityRequestId, requestId); this._ctxLastResponseType.set(InlineChatResponseTypes.OnlyMessages); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 0ccea8a61cd..41332e575fb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -23,15 +23,16 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; export class TerminalChatWidget extends Disposable { - private _scopedInstantiationService: IInstantiationService; - private _widgetContainer: HTMLElement; - private _chatWidgetFocused: IContextKey; - private _chatWidgetVisible: IContextKey; + private readonly _scopedInstantiationService: IInstantiationService; + private readonly _widgetContainer: HTMLElement; + private readonly _ctxChatWidgetFocused: IContextKey; + private readonly _ctxChatWidgetVisible: IContextKey; + private readonly _ctxResponseEditorFocused!: IContextKey; private readonly _inlineChatWidget: InlineChatWidget; - private _responseWidget: CodeEditorWidget | undefined; - private _responseElement: HTMLElement; + private readonly _responseElement: HTMLElement; private readonly _focusTracker: IFocusTracker; + private _responseWidget: CodeEditorWidget | undefined; constructor( private readonly _container: HTMLElement, @@ -44,8 +45,10 @@ export class TerminalChatWidget extends Disposable { super(); const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); - this._chatWidgetFocused = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); - this._chatWidgetVisible = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); + this._ctxChatWidgetFocused = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); + this._ctxChatWidgetVisible = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); + this._ctxResponseEditorFocused = TerminalContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); + this._widgetContainer = document.createElement('div'); this._widgetContainer.classList.add('terminal-inline-chat'); this._container.appendChild(this._widgetContainer); @@ -86,7 +89,7 @@ export class TerminalChatWidget extends Disposable { this._chatAccessibilityService.acceptResponse(codeBlock, requestId); this._responseElement.classList.remove('hide'); if (!this._responseWidget) { - this._responseWidget = this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, {}, { isSimpleWidget: true }); + this._responseWidget = this._register(this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, {}, { isSimpleWidget: true })); this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: codeBlock })).then((model) => { if (!model || !this._responseWidget) { return; @@ -96,6 +99,12 @@ export class TerminalChatWidget extends Disposable { const height = this._responseWidget.getContentHeight(); this._responseWidget.layout(new Dimension(400, height)); }); + this._register(this._responseWidget.onDidFocusEditorText(() => { + this._ctxResponseEditorFocused.set(true); + })); + this._register(this._responseWidget.onDidBlurEditorText(() => { + this._ctxResponseEditorFocused.set(false); + })); } else { this._responseWidget.setValue(codeBlock); } @@ -116,15 +125,15 @@ export class TerminalChatWidget extends Disposable { reveal(): void { this._inlineChatWidget.layout(new Dimension(400, 150)); this._widgetContainer.classList.remove('hide'); - this._chatWidgetFocused.set(true); - this._chatWidgetVisible.set(true); + this._ctxChatWidgetFocused.set(true); + this._ctxChatWidgetVisible.set(true); this._inlineChatWidget.focus(); } hide(): void { this._responseElement?.classList.add('hide'); this._widgetContainer.classList.add('hide'); - this._chatWidgetFocused.set(false); - this._chatWidgetVisible.set(false); + this._ctxChatWidgetFocused.set(false); + this._ctxChatWidgetVisible.set(false); this._instance.focus(); } cancel(): void { From b93cd296b072da5ae170af15695e00d865c449f8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 12:15:54 -0800 Subject: [PATCH 043/228] Working terminal voice chat --- .../actions/media/voiceChatActions.css | 9 ++- .../actions/voiceChatActions.ts | 77 ++++++++++++++++--- .../electron-sandbox/chat.contribution.ts | 3 +- .../chat/browser/terminalChatController.ts | 52 +++++++++++++ .../chat/browser/terminalChatWidget.ts | 8 ++ 5 files changed, 136 insertions(+), 13 deletions(-) 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 7a43cef7d12..87b1d68f988 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 @@ -7,7 +7,8 @@ * Replace with "microphone" icon. */ .monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, -.monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { +.monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, +.monaco-workbench .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: "\ec1c"; } @@ -15,7 +16,8 @@ * Clear animation styles when reduced motion is enabled. */ .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), -.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { +.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), +.monaco-workbench.reduce-motion .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { animation: none; } @@ -23,6 +25,7 @@ * Replace with "stop" icon when reduced motion is enabled. */ .monaco-workbench.reduce-motion .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, -.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { +.monaco-workbench.reduce-motion .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, +.monaco-workbench.reduce-motion .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { content: "\ead7"; } 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 e41ae049b6e..aeb85706fc6 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -54,18 +54,23 @@ import { IVoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ThemeIcon } from 'vs/base/common/themables'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +// TODO: The chat needs to move into contrib/terminal/ as we don't want anything importing from terminalContrib/ +// eslint-disable-next-line local/code-import-patterns +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; 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.") }); 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_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 CanVoiceChat = ContextKeyExpr.and(CONTEXT_PROVIDER_EXISTS, HasSpeechProvider); -type VoiceChatSessionContext = 'inline' | 'quick' | 'view' | 'editor'; +type VoiceChatSessionContext = 'inline' | 'terminal' | 'quick' | 'view' | 'editor'; interface IVoiceChatSessionController { @@ -89,8 +94,9 @@ class VoiceChatSessionControllerFactory { 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: 'inline' | 'quick' | 'view' | 'focused'): Promise; - static async create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | '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 { const chatWidgetService = accessor.get(IChatWidgetService); const chatService = accessor.get(IChatService); const viewsService = accessor.get(IViewsService); @@ -98,6 +104,7 @@ class VoiceChatSessionControllerFactory { 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') { @@ -132,6 +139,15 @@ class VoiceChatSessionControllerFactory { return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); } } + + // 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); + } + } } // View Chat @@ -156,6 +172,17 @@ class VoiceChatSessionControllerFactory { } } + // 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(); @@ -224,6 +251,20 @@ class VoiceChatSessionControllerFactory { clearInputPlaceholder: () => inlineChat.resetPlaceholder() }; } + + private static doCreateForTerminalChat(terminalChat: TerminalChatController): IVoiceChatSessionController { + return { + context: 'terminal', + onDidAcceptInput: terminalChat.onDidAcceptInput, + onDidCancelInput: terminalChat.onDidCancelInput, + focusInput: () => terminalChat.focus(), + acceptInput: () => terminalChat.acceptInput(), + updateInput: text => terminalChat.updateInput(text, false), + getInput: () => terminalChat.getInput(), + setInputPlaceholder: text => terminalChat.setPlaceholder(text), + clearInputPlaceholder: () => terminalChat.resetPlaceholder() + }; + } } interface IVoiceChatSession { @@ -255,6 +296,7 @@ class VoiceChatSessions { 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); @@ -345,6 +387,9 @@ class VoiceChatSessions { case 'inline': this.inlineVoiceChatInProgressKey.set(true); break; + case 'terminal': + this.terminalVoiceChatInProgressKey.set(true); + break; case 'quick': this.quickVoiceChatInProgressKey.set(true); break; @@ -387,6 +432,7 @@ class VoiceChatSessions { this.quickVoiceChatInProgressKey.set(false); this.inlineVoiceChatInProgressKey.set(false); + this.terminalVoiceChatInProgressKey.set(false); this.voiceChatInViewInProgressKey.set(false); this.voiceChatInEditorInProgressKey.set(false); } @@ -510,7 +556,8 @@ export class StartVoiceChatAction extends Action2 { 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_INLINE_VOICE_CHAT_IN_PROGRESS.negate(), + CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate() ), primary: KeyMod.CtrlCmd | KeyCode.KeyI }, @@ -529,7 +576,7 @@ export class StartVoiceChatAction extends Action2 { }, { id: MenuId.for('terminalChatInput'), - when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate()), + when: ContextKeyExpr.and(HasSpeechProvider, CONTEXT_TERMINAL_VOICE_CHAT_IN_PROGRESS.negate()), group: 'main', order: -1 }] @@ -629,7 +676,7 @@ class BaseStopListeningAction extends Action2 { constructor( desc: { id: string; icon?: ThemeIcon; f1?: boolean }, - private readonly target: 'inline' | 'quick' | 'view' | 'editor' | undefined, + private readonly target: 'inline' | 'terminal' | 'quick' | 'view' | 'editor' | undefined, context: RawContextKey, menu: MenuId | undefined, group: 'navigation' | 'main' = 'navigation' @@ -703,6 +750,15 @@ export class StopListeningInInlineChatAction extends BaseStopListeningAction { } } +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'), 'main'); + } +} + export class StopListeningAndSubmitAction extends Action2 { static readonly ID = 'workbench.action.chat.stopListeningAndSubmit'; @@ -745,7 +801,8 @@ registerThemingParticipant((theme, collector) => { // 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), - .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { color: ${activeRecordingColor}; outline: 1px solid ${activeRecordingColor}; outline-offset: -1px; @@ -754,7 +811,8 @@ registerThemingParticipant((theme, collector) => { } .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, - .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before, + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::before { position: absolute; outline: 1px solid ${activeRecordingColor}; outline-offset: 2px; @@ -764,7 +822,8 @@ registerThemingParticipant((theme, collector) => { } .monaco-workbench:not(.reduce-motion) .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after, - .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { + .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after, + .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled)::after { content: ''; position: absolute; outline: 1px solid ${activeRecordingDimmedColor}; 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 2f69e1b13f8..02383b6b3f3 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, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction } from 'vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions'; +import { InlineVoiceChatAction, QuickVoiceChatAction, StartVoiceChatAction, StopListeningInInlineChatAction, StopListeningInQuickChatAction, StopListeningInChatEditorAction, StopListeningInChatViewAction, VoiceChatInChatViewAction, StopListeningAction, StopListeningAndSubmitAction, KeywordActivationContribution, InstallVoiceChatAction, StopListeningInTerminalChatAction } 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'; @@ -21,5 +21,6 @@ registerAction2(StopListeningInChatViewAction); registerAction2(StopListeningInChatEditorAction); registerAction2(StopListeningInQuickChatAction); registerAction2(StopListeningInInlineChatAction); +registerAction2(StopListeningInTerminalChatAction); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 00b117d6661..86287257f8f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -24,6 +24,18 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { Emitter, Event } from 'vs/base/common/event'; + +const enum Message { + NONE = 0, + ACCEPT_SESSION = 1 << 0, + CANCEL_SESSION = 1 << 1, + PAUSE_SESSION = 1 << 2, + CANCEL_REQUEST = 1 << 3, + CANCEL_INPUT = 1 << 4, + ACCEPT_INPUT = 1 << 5, + RERUN_INPUT = 1 << 6, +} export class TerminalChatController extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.Chat'; @@ -47,6 +59,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr private _cancellationTokenSource!: CancellationTokenSource; + private _messages = this._store.add(new Emitter()); + + 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); + constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, @@ -118,6 +135,18 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._cancellationTokenSource.cancel(); } + setPlaceholder(text: string): void { + // TODO: Impl + // this._forcedPlaceholder = text; + // this._updatePlaceholder(); + } + + resetPlaceholder(): void { + // TODO: Impl + // this._forcedPlaceholder = undefined; + // this._updatePlaceholder(); + } + async acceptInput(): Promise { let message = ''; this._chatAccessibilityService.acceptRequest(); @@ -168,6 +197,29 @@ export class TerminalChatController extends Disposable implements ITerminalContr } this._ctxHasActiveRequest.set(false); this._chatWidget?.rawValue?.updateProgress(); + this._messages.fire(Message.ACCEPT_INPUT); + } + + updateInput(text: string, selectAll = true): void { + const widget = this._chatWidget?.rawValue?.inlineChatWidget; + if (widget) { + widget.value = text; + if (selectAll) { + widget.selectAll(); + } + } + } + + getInput(): string { + return this._chatWidget?.rawValue?.input() ?? ''; + } + + focus(): void { + this._chatWidget?.rawValue?.focus(); + } + + hasFocus(): boolean { + return !!this._chatWidget?.rawValue?.hasFocus(); } acceptCommand(): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 41332e575fb..6374e1cc991 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -34,6 +34,8 @@ export class TerminalChatWidget extends Disposable { private readonly _focusTracker: IFocusTracker; private _responseWidget: CodeEditorWidget | undefined; + public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } + constructor( private readonly _container: HTMLElement, private readonly _instance: ITerminalInstance, @@ -140,6 +142,12 @@ export class TerminalChatWidget extends Disposable { // TODO: Impl this._inlineChatWidget.value = ''; } + focus(): void { + this._inlineChatWidget.focus(); + } + hasFocus(): boolean { + return this._inlineChatWidget.hasFocus(); + } input(): string { return this._inlineChatWidget.value; } From 3960db9ce3b00189765b88601fefa1913ffdd1e9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 12:19:10 -0800 Subject: [PATCH 044/228] Add voice placeholder --- .../chat/browser/terminalChatController.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 86287257f8f..f3d7ef85326 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -135,16 +135,29 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._cancellationTokenSource.cancel(); } + private _forcedPlaceholder: string | undefined = undefined; + + private _updatePlaceholder(): void { + const inlineChatWidget = this._chatWidget?.rawValue?.inlineChatWidget; + if (inlineChatWidget) { + inlineChatWidget.placeholder = this._getPlaceholderText(); + } + } + + private _getPlaceholderText(): string { + return this._forcedPlaceholder ?? ''; + // TODO: Pass through session placeholder + // return this._forcedPlaceholder ?? this._session?.session.placeholder ?? ''; + } + setPlaceholder(text: string): void { - // TODO: Impl - // this._forcedPlaceholder = text; - // this._updatePlaceholder(); + this._forcedPlaceholder = text; + this._updatePlaceholder(); } resetPlaceholder(): void { - // TODO: Impl - // this._forcedPlaceholder = undefined; - // this._updatePlaceholder(); + this._forcedPlaceholder = undefined; + this._updatePlaceholder(); } async acceptInput(): Promise { From bf1bb656259791cf84051759a1d264409b5239c0 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 14:50:05 -0600 Subject: [PATCH 045/228] clear on hide --- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 6374e1cc991..0e8b82043ea 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -134,6 +134,8 @@ export class TerminalChatWidget extends Disposable { hide(): void { this._responseElement?.classList.add('hide'); this._widgetContainer.classList.add('hide'); + this._inlineChatWidget.value = ''; + this._responseWidget?.setValue(''); this._ctxChatWidgetFocused.set(false); this._ctxChatWidgetVisible.set(false); this._instance.focus(); From 83f8ee26b6c832c68f7ba37f7b3f083bc2b76292 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 14:53:59 -0600 Subject: [PATCH 046/228] rm unused --- .../chat/browser/terminalChatController.ts | 14 -------------- .../chat/browser/terminalChatWidget.ts | 11 ++--------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index f3d7ef85326..83adeff265d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -6,7 +6,6 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IDimension } from 'vs/base/browser/dom'; import { Lazy } from 'vs/base/common/lazy'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; @@ -49,7 +48,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr */ static activeChatWidget?: TerminalChatController; private _chatWidget: Lazy | undefined; - private _lastLayoutDimensions: IDimension | undefined; private _accessibilityRequestId: number = 0; get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } @@ -95,14 +93,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._cancellationTokenSource = new CancellationTokenSource(); } - layout(_xterm: IXtermTerminal & { raw: RawXtermTerminal }, dimension: IDimension): void { - if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { - return; - } - this._lastLayoutDimensions = dimension; - this._chatWidget?.rawValue?.layout(dimension.width); - } - xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { return; @@ -123,10 +113,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr throw new Error('FindWidget expected terminal DOM to be initialized'); } - if (this._lastLayoutDimensions) { - chatWidget.layout(this._lastLayoutDimensions.width); - } - return chatWidget; }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 0e8b82043ea..61aa2c4a409 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -92,6 +92,8 @@ export class TerminalChatWidget extends Disposable { this._responseElement.classList.remove('hide'); if (!this._responseWidget) { this._responseWidget = this._register(this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, {}, { isSimpleWidget: true })); + this._register(this._responseWidget.onDidFocusEditorText(() => this._ctxResponseEditorFocused.set(true))); + this._register(this._responseWidget.onDidBlurEditorText(() => this._ctxResponseEditorFocused.set(false))); this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: codeBlock })).then((model) => { if (!model || !this._responseWidget) { return; @@ -101,12 +103,6 @@ export class TerminalChatWidget extends Disposable { const height = this._responseWidget.getContentHeight(); this._responseWidget.layout(new Dimension(400, height)); }); - this._register(this._responseWidget.onDidFocusEditorText(() => { - this._ctxResponseEditorFocused.set(true); - })); - this._register(this._responseWidget.onDidBlurEditorText(() => { - this._ctxResponseEditorFocused.set(false); - })); } else { this._responseWidget.setValue(codeBlock); } @@ -170,9 +166,6 @@ export class TerminalChatWidget extends Disposable { updateProgress(progress?: IChatProgress): void { this._inlineChatWidget.updateProgress(progress?.kind === 'content' || progress?.kind === 'markdownContent'); } - layout(width: number): void { - // this._widget?.layout(100, width < 300 ? 300 : width); - } public get focusTracker(): IFocusTracker { return this._focusTracker; } From 1d262b3146aa7f3c254d28fc0144f9ae08bf9717 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 15:05:59 -0600 Subject: [PATCH 047/228] update variables after rob's change --- .../terminalContrib/chat/browser/terminalChatController.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 83adeff265d..9c9cb0a2ff8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -17,7 +17,6 @@ import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/ import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { generateUuid } from 'vs/base/common/uuid'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; import { marked } from 'vs/base/common/marked/marked'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -162,15 +161,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr } this._chatWidget?.rawValue?.updateProgress(progress); }; - const resolvedVariables: Record = {}; const requestId = generateUuid(); const requestProps: IChatAgentRequest = { sessionId: generateUuid(), requestId, agentId, message: this._chatWidget?.rawValue?.input() || '', - variables: resolvedVariables, - variables2: { message: this._chatWidget?.rawValue?.input() || '', variables: [] } + variables: { variables: [] }, }; this._chatWidget?.rawValue?.setValue(); From cd261754e852064d15516f6321a65ac17c906a3f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:06:30 -0800 Subject: [PATCH 048/228] Simplify response editor presentation --- .../chat/browser/terminalChatWidget.ts | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 61aa2c4a409..0c174ed3555 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -91,7 +91,41 @@ export class TerminalChatWidget extends Disposable { this._chatAccessibilityService.acceptResponse(codeBlock, requestId); this._responseElement.classList.remove('hide'); if (!this._responseWidget) { - this._responseWidget = this._register(this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, {}, { isSimpleWidget: true })); + this._responseWidget = this._register(this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, { + padding: { top: 2, bottom: 2 }, + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + hideCursorInOverviewRuler: true, + selectOnLineNumbers: false, + selectionHighlight: false, + scrollbar: { + useShadows: false, + vertical: 'hidden', + horizontal: 'auto', + alwaysConsumeMouseWheel: false + }, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + dragAndDrop: false, + revealHorizontalRightPadding: 5, + minimap: { enabled: false }, + guides: { indentation: false }, + rulers: [], + renderWhitespace: 'none', + dropIntoEditor: { enabled: true }, + quickSuggestions: false, + suggest: { + showIcons: false, + showSnippets: false, + showWords: true, + showStatusBar: false, + }, + }, { isSimpleWidget: true })); this._register(this._responseWidget.onDidFocusEditorText(() => this._ctxResponseEditorFocused.set(true))); this._register(this._responseWidget.onDidBlurEditorText(() => this._ctxResponseEditorFocused.set(false))); this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: codeBlock })).then((model) => { From 84fb0821a9347a1b8c563f29259962e758ca6e87 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 15:56:53 -0600 Subject: [PATCH 049/228] extract code content, language --- .../terminalContrib/chat/browser/terminalChatController.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 9c9cb0a2ff8..f7edc4b5fd6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -179,7 +179,12 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.updateProgress(); return; } - const codeBlock = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw.replaceAll('```', ''); + const firstCodeBlockContent = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw; + const regex = /```(?\w+)\n(?[\s\S]*?)```/g; + const match = regex.exec(firstCodeBlockContent); + const codeBlock = match?.groups?.content; + // TODO: map to editor known language, set editor language + // const language = match?.groups?.language; this._accessibilityRequestId++; if (cancellationToken.isCancellationRequested) { return; From fa111adf63ac35b6b8921db9ece8cb7880a7831c Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 13 Feb 2024 16:08:25 -0600 Subject: [PATCH 050/228] add language support --- .../chat/browser/terminalChatController.ts | 5 ++--- .../chat/browser/terminalChatWidget.ts | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index f7edc4b5fd6..221c010600b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -183,15 +183,14 @@ export class TerminalChatController extends Disposable implements ITerminalContr const regex = /```(?\w+)\n(?[\s\S]*?)```/g; const match = regex.exec(firstCodeBlockContent); const codeBlock = match?.groups?.content; - // TODO: map to editor known language, set editor language - // const language = match?.groups?.language; + const shellType = match?.groups?.language; this._accessibilityRequestId++; if (cancellationToken.isCancellationRequested) { return; } if (codeBlock) { // TODO: check the SR experience - this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._accessibilityRequestId); + this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._accessibilityRequestId, shellType); } else { this._chatWidget?.rawValue?.renderMessage(message, this._accessibilityRequestId, requestId); this._ctxLastResponseType.set(InlineChatResponseTypes.OnlyMessages); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 0c174ed3555..abb8807cdd9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -87,7 +87,7 @@ export class TerminalChatWidget extends Disposable { this._focusTracker = this._register(trackFocus(this._widgetContainer)); } - renderTerminalCommand(codeBlock: string, requestId: number): void { + renderTerminalCommand(codeBlock: string, requestId: number, shellType?: string): void { this._chatAccessibilityService.acceptResponse(codeBlock, requestId); this._responseElement.classList.remove('hide'); if (!this._responseWidget) { @@ -140,6 +140,20 @@ export class TerminalChatWidget extends Disposable { } else { this._responseWidget.setValue(codeBlock); } + this._responseWidget.getModel()?.setLanguage(this._getLanguageFromShell(shellType)); + } + + private _getLanguageFromShell(shell?: string): string { + switch (shell) { + case 'sh': + case 'bash': + case 'zsh': + return 'shellscript'; + case 'pwsh': + return 'powershell'; + default: + return 'plaintext'; + } } renderMessage(message: string, accessibilityRequestId: number, requestId: string): void { From 16f1f4bfc1cb918a397c5c4cd4a1904d88ec983b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:29:44 -0800 Subject: [PATCH 051/228] Add shell language id fallback --- .../terminalContrib/chat/browser/terminalChatWidget.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index abb8807cdd9..c3efebdc5ca 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -9,6 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/terminalChatWidget'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; @@ -42,6 +43,7 @@ export class TerminalChatWidget extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, + @ILanguageService private readonly _languageService: ILanguageService, @IModelService private readonly _modelService: IModelService ) { super(); @@ -145,9 +147,13 @@ export class TerminalChatWidget extends Disposable { private _getLanguageFromShell(shell?: string): string { switch (shell) { - case 'sh': - case 'bash': + case 'fish': + return this._languageService.isRegisteredLanguageId('fish') ? 'fish' : 'shellscript'; case 'zsh': + return this._languageService.isRegisteredLanguageId('zsh') ? 'zsh' : 'shellscript'; + case 'bash': + return this._languageService.isRegisteredLanguageId('bash') ? 'bash' : 'shellscript'; + case 'sh': return 'shellscript'; case 'pwsh': return 'powershell'; From 8112339b211fcdb807e4e7f9b57f39142fbe289b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:49:37 -0800 Subject: [PATCH 052/228] Improve progress feedback --- .../chat/browser/terminalChatController.ts | 17 +++++++++++++---- .../chat/browser/terminalChatWidget.ts | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 221c010600b..36db8ad2ab9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -23,6 +23,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Emitter, Event } from 'vs/base/common/event'; +import { localize } from 'vs/nls'; const enum Message { NONE = 0, @@ -172,12 +173,20 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.setValue(); try { - await this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, [], cancellationToken); + const task = this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, [], cancellationToken); + this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); + this._chatWidget?.rawValue?.inlineChatWidget.updateFollowUps(undefined); + this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(true); + this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(localize('thinking', "Thinking\u2026")); + // TODO: this._zone.value.widget.updateInfo(!this._session.lastExchange ? localize('thinking', "Thinking\u2026") : ''); + await task; } catch (e) { - // Provider is not ready + + } finally { this._ctxHasActiveRequest.set(false); - this._chatWidget?.rawValue?.updateProgress(); - return; + this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); + this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); } const firstCodeBlockContent = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw; const regex = /```(?\w+)\n(?[\s\S]*?)```/g; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index c3efebdc5ca..402f8638558 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -84,7 +84,7 @@ export class TerminalChatWidget extends Disposable { } ); this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); - this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated code may be incorrect")); + this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); this._widgetContainer.appendChild(this._inlineChatWidget.domNode); this._focusTracker = this._register(trackFocus(this._widgetContainer)); From 3c306e9b87596190decd7749f68b00b0acc52525 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:52:43 -0800 Subject: [PATCH 053/228] Fix feedback icons --- .../chat/browser/terminalChatActions.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 37cf45c77fc..a62ed944245 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -162,6 +162,7 @@ registerActiveXtermAction({ ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatRequestActive, ), + // TODO: toggled: CTX_INLINE_CHAT_LAST_FEEDBACK.isEqualTo('helpful'), icon: Codicon.thumbsup, menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, @@ -177,12 +178,13 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.FeedbackUnhelpful, - title: localize2('feedbackUnhelpful', 'Helpful'), + title: localize2('feedbackUnhelpful', 'Unhelpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatRequestActive, ), - icon: Codicon.thumbsup, + // TODO: toggled: CTX_INLINE_CHAT_LAST_FEEDBACK.isEqualTo('unhelpful'), + icon: Codicon.thumbsdown, menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', @@ -202,14 +204,19 @@ registerActiveXtermAction({ ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatRequestActive, ), - icon: Codicon.thumbsup, - menu: { + // TODO: precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), + icon: Codicon.report, + menu: [/*{ + // TODO: Enable this id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - group: 'inline', - order: 3, - // TODO: Fill in ctx - // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), - }, + when: ContextKeyExpr.and(CTX_TERMINAL_CHAT_SUPPORT_ISSUE_REPORTING, CTX_TERMINAL_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), + group: '2_feedback', + order: 3 + }, */{ + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'config', + order: 3 + }], run: (_xterm, _accessor, activeInstance) => { // TODO: Impl } From 3119ae83f7fc220079e3a8ef9b42d254fc0ead34 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:57:38 -0800 Subject: [PATCH 054/228] Move accept to status menu --- .../terminalContrib/chat/browser/terminalChatActions.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index a62ed944245..348d5bee159 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -13,7 +13,7 @@ import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONS import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerActiveXtermAction({ @@ -72,7 +72,8 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.AcceptCommand, - title: localize2('workbench.action.terminal.acceptCommand', 'Accept Command'), + title: localize2('acceptCommand', 'Accept Command'), + shortTitle: localize2('accept', 'Accept'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -87,8 +88,8 @@ registerActiveXtermAction({ primary: KeyMod.CtrlCmd | KeyCode.Enter, }, menu: { - id: MENU_TERMINAL_CHAT_WIDGET, - group: 'main', + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', order: 0, when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), }, From 802e74686aab506468f23b1097034c72a22c057c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:58:43 -0800 Subject: [PATCH 055/228] Simplify localize id --- .../terminalContrib/chat/browser/terminalChatActions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 348d5bee159..d0205008d74 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -18,7 +18,7 @@ import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/cha registerActiveXtermAction({ id: TerminalChatCommandId.Focus, - title: localize2('workbench.action.terminal.focusChat', 'Focus Chat'), + title: localize2('focusChat', 'Focus Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), @@ -40,7 +40,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.Hide, - title: localize2('workbench.action.terminal.closeChat', 'Close Chat'), + title: localize2('closeChat', 'Close Chat'), keybinding: { primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], @@ -104,7 +104,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.MakeRequest, - title: localize2('workbench.action.terminal.submitChat', 'Make Chat Request'), + title: localize2('makeChatRequest', 'Make Chat Request'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -135,7 +135,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.Cancel, - title: localize2('workbench.action.terminal.cancelChat', 'Cancel Chat'), + title: localize2('cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatRequestActive, From ee47ee25bb67793e707129ed258b219419152216 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 14 Feb 2024 12:18:38 -0600 Subject: [PATCH 056/228] get accept action to show up, add view in chat action --- .../inlineChat/browser/inlineChatWidget.ts | 2 +- .../chat/browser/terminalChat.ts | 1 + .../chat/browser/terminalChatActions.ts | 29 ++++++++++++- .../chat/browser/terminalChatController.ts | 41 ++++++++++++++----- .../chat/browser/terminalChatWidget.ts | 1 + 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index ca909ba98e0..537c7b8951f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -388,7 +388,7 @@ export class InlineChatWidget { buttonConfigProvider: action => { if (action.id === ACTION_REGENERATE_RESPONSE) { return { showIcon: true, showLabel: false, isSecondary: true }; - } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { + } else if ([ACTION_VIEW_IN_CHAT, ACTION_ACCEPT_CHANGES, 'workbench.action.terminal.chat.acceptCommand', 'workbench.action.terminal.chat.viewInChat'].includes(action.id)) { return { isSecondary: false }; } else { return { isSecondary: true }; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 882baf8c26b..ddea1c2a53b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -14,6 +14,7 @@ export const enum TerminalChatCommandId { FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', AcceptCommand = 'workbench.action.terminal.chat.acceptCommand', + ViewInChat = 'workbench.action.terminal.chat.viewInChat', } export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index d0205008d74..d6ba18a272e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -91,7 +91,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 0, - when: CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), TerminalContextKeys.chatRequestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -102,6 +102,32 @@ registerActiveXtermAction({ } }); +registerActiveXtermAction({ + id: TerminalChatCommandId.ViewInChat, + title: localize2('viewInChat', 'View in Chat'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalContextKeys.chatRequestActive.negate(), + TerminalContextKeys.chatAgentRegistered, + CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages) + ), + icon: Codicon.commentDiscussion, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), TerminalContextKeys.chatRequestActive.negate()), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.viewInChat(); + } +}); + registerActiveXtermAction({ id: TerminalChatCommandId.MakeRequest, title: localize2('makeChatRequest', 'Make Chat Request'), @@ -222,3 +248,4 @@ registerActiveXtermAction({ // TODO: Impl } }); + diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 36db8ad2ab9..da56f4d4041 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -14,13 +14,13 @@ import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/te import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { generateUuid } from 'vs/base/common/uuid'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { marked } from 'vs/base/common/marked/marked'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Emitter, Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; @@ -62,6 +62,9 @@ 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); + private _lastInput: string | undefined; + private _lastResponseContent: string | undefined; + constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, @@ -72,6 +75,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, + @IChatService private readonly _chatService: IChatService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService ) { super(); if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { @@ -147,18 +152,22 @@ export class TerminalChatController extends Disposable implements ITerminalContr } async acceptInput(): Promise { - let message = ''; + this._lastInput = this._chatWidget?.rawValue?.input(); + if (!this._lastInput) { + return; + } this._chatAccessibilityService.acceptRequest(); this._ctxHasActiveRequest.set(true); const cancellationToken = this._cancellationTokenSource.token; const agentId = 'terminal'; + let responseContent = ''; const progressCallback = (progress: IChatProgress) => { if (cancellationToken.isCancellationRequested) { return; } if (progress.kind === 'content' || progress.kind === 'markdownContent') { - message += progress.content; + responseContent += progress.content; } this._chatWidget?.rawValue?.updateProgress(progress); }; @@ -167,11 +176,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr sessionId: generateUuid(), requestId, agentId, - message: this._chatWidget?.rawValue?.input() || '', + message: this._lastInput, variables: { variables: [] }, }; - this._chatWidget?.rawValue?.setValue(); - try { const task = this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, [], cancellationToken); this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); @@ -188,7 +195,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); } - const firstCodeBlockContent = marked.lexer(message).filter(token => token.type === 'code')?.[0]?.raw; + this._lastResponseContent = responseContent; + const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; const regex = /```(?\w+)\n(?[\s\S]*?)```/g; const match = regex.exec(firstCodeBlockContent); const codeBlock = match?.groups?.content; @@ -200,12 +208,12 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (codeBlock) { // TODO: check the SR experience this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._accessibilityRequestId, shellType); + this._ctxLastResponseType.set(InlineChatResponseTypes.Empty); } else { - this._chatWidget?.rawValue?.renderMessage(message, this._accessibilityRequestId, requestId); + this._chatWidget?.rawValue?.renderMessage(responseContent, this._accessibilityRequestId, requestId); this._ctxLastResponseType.set(InlineChatResponseTypes.OnlyMessages); } - this._ctxHasActiveRequest.set(false); - this._chatWidget?.rawValue?.updateProgress(); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); } @@ -239,6 +247,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.reveal(); } + async viewInChat(): Promise { + if (!this._lastInput || !this._lastResponseContent) { + return; + } + const widget = await this._chatWidgetService.revealViewForProvider('copilot'); + if (widget && widget.viewModel) { + this._chatService.addCompleteRequest(widget.viewModel.sessionId, this._lastInput, undefined, { message: this._lastResponseContent }); + widget.focusLastMessage(); + } + } + override dispose() { super.dispose(); this._chatWidget?.rawValue?.dispose(); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 402f8638558..b8c73ef1ffb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -163,6 +163,7 @@ export class TerminalChatWidget extends Disposable { } renderMessage(message: string, accessibilityRequestId: number, requestId: string): void { + this._responseElement.classList.add('hide'); this._inlineChatWidget.updateChatMessage({ message: new MarkdownString(message), requestId, providerId: 'terminal' }); this._chatAccessibilityService.acceptResponse(message, accessibilityRequestId); } From bfc4df0453af87e35090ddb39e1a992c3fcf1eaa Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 14 Feb 2024 12:24:03 -0600 Subject: [PATCH 057/228] add show/hide commands for editor --- .../chat/browser/terminalChatWidget.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index b8c73ef1ffb..e522765f213 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -91,7 +91,7 @@ export class TerminalChatWidget extends Disposable { } renderTerminalCommand(codeBlock: string, requestId: number, shellType?: string): void { this._chatAccessibilityService.acceptResponse(codeBlock, requestId); - this._responseElement.classList.remove('hide'); + this.showTerminalCommandEditor(); if (!this._responseWidget) { this._responseWidget = this._register(this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, { padding: { top: 2, bottom: 2 }, @@ -163,7 +163,7 @@ export class TerminalChatWidget extends Disposable { } renderMessage(message: string, accessibilityRequestId: number, requestId: string): void { - this._responseElement.classList.add('hide'); + this.hideTerminalCommandEditor(); this._inlineChatWidget.updateChatMessage({ message: new MarkdownString(message), requestId, providerId: 'terminal' }); this._chatAccessibilityService.acceptResponse(message, accessibilityRequestId); } @@ -183,7 +183,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.focus(); } hide(): void { - this._responseElement?.classList.add('hide'); + this.hideTerminalCommandEditor(); this._widgetContainer.classList.add('hide'); this._inlineChatWidget.value = ''; this._responseWidget?.setValue(''); @@ -207,7 +207,7 @@ export class TerminalChatWidget extends Disposable { setValue(value?: string) { this._inlineChatWidget.value = value ?? ''; if (!value) { - this._responseElement?.classList.add('hide'); + this.hideTerminalCommandEditor(); } } acceptCommand(): void { @@ -224,4 +224,10 @@ export class TerminalChatWidget extends Disposable { public get focusTracker(): IFocusTracker { return this._focusTracker; } + hideTerminalCommandEditor(): void { + this._responseElement.classList.add('hide'); + } + showTerminalCommandEditor(): void { + this._responseElement.classList.remove('hide'); + } } From 2b7e5e52d276a902db01ee05e7c9e157e6a0a265 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 14 Feb 2024 12:36:27 -0600 Subject: [PATCH 058/228] rename and reorder things in terminalChatWidget --- .../chat/browser/terminalChatWidget.ts | 108 +++++++++--------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index e522765f213..49eef7120fa 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -24,21 +24,24 @@ import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/termin import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; export class TerminalChatWidget extends Disposable { - private readonly _scopedInstantiationService: IInstantiationService; - private readonly _widgetContainer: HTMLElement; - private readonly _ctxChatWidgetFocused: IContextKey; - private readonly _ctxChatWidgetVisible: IContextKey; - private readonly _ctxResponseEditorFocused!: IContextKey; + + private readonly _container: HTMLElement; private readonly _inlineChatWidget: InlineChatWidget; - private readonly _responseElement: HTMLElement; - private readonly _focusTracker: IFocusTracker; - private _responseWidget: CodeEditorWidget | undefined; - public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } + private readonly _terminalCommandWidgetContainer: HTMLElement; + private _terminalCommandWidget: CodeEditorWidget | undefined; + + private readonly _focusTracker: IFocusTracker; + + private readonly _scopedInstantiationService: IInstantiationService; + private readonly _focusedContextKey: IContextKey; + private readonly _visibleContextKey: IContextKey; + private readonly _responseEditorFocusedContextKey!: IContextKey; + constructor( - private readonly _container: HTMLElement, + terminalElement: HTMLElement, private readonly _instance: ITerminalInstance, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -47,19 +50,19 @@ export class TerminalChatWidget extends Disposable { @IModelService private readonly _modelService: IModelService ) { super(); - const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._container)); + const scopedContextKeyService = this._register(this._contextKeyService.createScoped(terminalElement)); this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); - this._ctxChatWidgetFocused = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); - this._ctxChatWidgetVisible = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); - this._ctxResponseEditorFocused = TerminalContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); + this._focusedContextKey = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); + this._visibleContextKey = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); + this._responseEditorFocusedContextKey = TerminalContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); - this._widgetContainer = document.createElement('div'); - this._widgetContainer.classList.add('terminal-inline-chat'); - this._container.appendChild(this._widgetContainer); + this._container = document.createElement('div'); + this._container.classList.add('terminal-inline-chat'); + terminalElement.appendChild(this._container); - this._responseElement = document.createElement('div'); - this._responseElement.classList.add('terminal-inline-chat-response'); - this._widgetContainer.prepend(this._responseElement); + this._terminalCommandWidgetContainer = document.createElement('div'); + this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); + this._container.prepend(this._terminalCommandWidgetContainer); // The inline chat widget requires a parent editor that it bases the diff view on, since the // terminal doesn't use that feature we can just pass in an unattached editor instance. @@ -85,15 +88,16 @@ export class TerminalChatWidget extends Disposable { ); this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); - this._widgetContainer.appendChild(this._inlineChatWidget.domNode); + this._container.appendChild(this._inlineChatWidget.domNode); - this._focusTracker = this._register(trackFocus(this._widgetContainer)); + this._focusTracker = this._register(trackFocus(this._container)); } - renderTerminalCommand(codeBlock: string, requestId: number, shellType?: string): void { - this._chatAccessibilityService.acceptResponse(codeBlock, requestId); - this.showTerminalCommandEditor(); - if (!this._responseWidget) { - this._responseWidget = this._register(this._scopedInstantiationService.createInstance(CodeEditorWidget, this._responseElement, { + + renderTerminalCommand(command: string, requestId: number, shellType?: string): void { + this._chatAccessibilityService.acceptResponse(command, requestId); + this.showTerminalCommandWidget(); + if (!this._terminalCommandWidget) { + this._terminalCommandWidget = this._register(this._scopedInstantiationService.createInstance(CodeEditorWidget, this._terminalCommandWidgetContainer, { padding: { top: 2, bottom: 2 }, overviewRulerLanes: 0, glyphMargin: false, @@ -128,21 +132,21 @@ export class TerminalChatWidget extends Disposable { showStatusBar: false, }, }, { isSimpleWidget: true })); - this._register(this._responseWidget.onDidFocusEditorText(() => this._ctxResponseEditorFocused.set(true))); - this._register(this._responseWidget.onDidBlurEditorText(() => this._ctxResponseEditorFocused.set(false))); - this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: codeBlock })).then((model) => { - if (!model || !this._responseWidget) { + this._register(this._terminalCommandWidget.onDidFocusEditorText(() => this._responseEditorFocusedContextKey.set(true))); + this._register(this._terminalCommandWidget.onDidBlurEditorText(() => this._responseEditorFocusedContextKey.set(false))); + this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: command })).then((model) => { + if (!model || !this._terminalCommandWidget) { return; } - this._responseWidget.layout(new Dimension(400, 0)); - this._responseWidget.setModel(model); - const height = this._responseWidget.getContentHeight(); - this._responseWidget.layout(new Dimension(400, height)); + this._terminalCommandWidget.layout(new Dimension(400, 0)); + this._terminalCommandWidget.setModel(model); + const height = this._terminalCommandWidget.getContentHeight(); + this._terminalCommandWidget.layout(new Dimension(400, height)); }); } else { - this._responseWidget.setValue(codeBlock); + this._terminalCommandWidget.setValue(command); } - this._responseWidget.getModel()?.setLanguage(this._getLanguageFromShell(shellType)); + this._terminalCommandWidget.getModel()?.setLanguage(this._getLanguageFromShell(shellType)); } private _getLanguageFromShell(shell?: string): string { @@ -163,7 +167,7 @@ export class TerminalChatWidget extends Disposable { } renderMessage(message: string, accessibilityRequestId: number, requestId: string): void { - this.hideTerminalCommandEditor(); + this.hideTerminalCommandWidget(); this._inlineChatWidget.updateChatMessage({ message: new MarkdownString(message), requestId, providerId: 'terminal' }); this._chatAccessibilityService.acceptResponse(message, accessibilityRequestId); } @@ -177,18 +181,18 @@ export class TerminalChatWidget extends Disposable { } reveal(): void { this._inlineChatWidget.layout(new Dimension(400, 150)); - this._widgetContainer.classList.remove('hide'); - this._ctxChatWidgetFocused.set(true); - this._ctxChatWidgetVisible.set(true); + this._container.classList.remove('hide'); + this._focusedContextKey.set(true); + this._visibleContextKey.set(true); this._inlineChatWidget.focus(); } hide(): void { - this.hideTerminalCommandEditor(); - this._widgetContainer.classList.add('hide'); + this.hideTerminalCommandWidget(); + this._container.classList.add('hide'); this._inlineChatWidget.value = ''; - this._responseWidget?.setValue(''); - this._ctxChatWidgetFocused.set(false); - this._ctxChatWidgetVisible.set(false); + this._terminalCommandWidget?.setValue(''); + this._focusedContextKey.set(false); + this._visibleContextKey.set(false); this._instance.focus(); } cancel(): void { @@ -207,11 +211,11 @@ export class TerminalChatWidget extends Disposable { setValue(value?: string) { this._inlineChatWidget.value = value ?? ''; if (!value) { - this.hideTerminalCommandEditor(); + this.hideTerminalCommandWidget(); } } acceptCommand(): void { - const value = this._responseWidget?.getValue(); + const value = this._terminalCommandWidget?.getValue(); if (!value) { return; } @@ -224,10 +228,10 @@ export class TerminalChatWidget extends Disposable { public get focusTracker(): IFocusTracker { return this._focusTracker; } - hideTerminalCommandEditor(): void { - this._responseElement.classList.add('hide'); + hideTerminalCommandWidget(): void { + this._terminalCommandWidgetContainer.classList.add('hide'); } - showTerminalCommandEditor(): void { - this._responseElement.classList.remove('hide'); + showTerminalCommandWidget(): void { + this._terminalCommandWidgetContainer.classList.remove('hide'); } } From 1565341d4496a71da7860467d3ea32a72103dbab Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 14 Feb 2024 12:50:49 -0600 Subject: [PATCH 059/228] reset everything on hide of widget --- .../terminalContrib/chat/browser/terminalChatController.ts | 1 - .../terminalContrib/chat/browser/terminalChatWidget.ts | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index da56f4d4041..a5d73cd459a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -169,7 +169,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (progress.kind === 'content' || progress.kind === 'markdownContent') { responseContent += progress.content; } - this._chatWidget?.rawValue?.updateProgress(progress); }; const requestId = generateUuid(); const requestProps: IChatAgentRequest = { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 49eef7120fa..a485dd40c4f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -191,6 +191,10 @@ export class TerminalChatWidget extends Disposable { this._container.classList.add('hide'); this._inlineChatWidget.value = ''; this._terminalCommandWidget?.setValue(''); + this._inlineChatWidget.updateChatMessage(undefined); + this._inlineChatWidget.updateFollowUps(undefined); + this._inlineChatWidget.updateProgress(false); + this._inlineChatWidget.updateToolbar(false); this._focusedContextKey.set(false); this._visibleContextKey.set(false); this._instance.focus(); From 55aca1a766b3223d5bd2fa164cbb17ff54615b9f Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 14 Feb 2024 13:06:47 -0600 Subject: [PATCH 060/228] more clean up --- .../terminal/common/terminalContextKey.ts | 1 + .../chat/browser/terminalChatController.ts | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 8ea1469a4b1..a7805bfac46 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -45,6 +45,7 @@ export const enum TerminalContextKeyStrings { ChatInputHasText = 'terminalChatInputHasText', ChatAgentRegistered = 'terminalChatAgentRegistered', ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', + ChatLastResponseType = 'terminalChatLastResponseType' } export namespace TerminalContextKeys { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index a5d73cd459a..5c105fee353 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -21,9 +21,9 @@ import { marked } from 'vs/base/common/marked/marked'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { Emitter, Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; +import { CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; const enum Message { NONE = 0, @@ -48,22 +48,25 @@ export class TerminalChatController extends Disposable implements ITerminalContr */ static activeChatWidget?: TerminalChatController; private _chatWidget: Lazy | undefined; - private _accessibilityRequestId: number = 0; get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; } - private readonly _ctxHasActiveRequest!: IContextKey; - private readonly _ctxHasTerminalAgent!: IContextKey; - private readonly _ctxLastResponseType!: IContextKey; + private readonly _requestActiveContextKey!: IContextKey; + private readonly _terminalAgentRegisteredContextKey!: IContextKey; + private readonly _lastResponseTypeContextKey!: IContextKey; private _cancellationTokenSource!: CancellationTokenSource; + private _accessibilityRequestId: number = 0; + private _messages = this._store.add(new Emitter()); + private _lastInput: string | undefined; + private _lastResponseContent: string | undefined; + 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); - private _lastInput: string | undefined; - private _lastResponseContent: string | undefined; + private _terminalAgentId = 'terminal'; constructor( private readonly _instance: ITerminalInstance, @@ -82,18 +85,18 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { return; } - this._ctxHasActiveRequest = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); - this._ctxHasTerminalAgent = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); - this._ctxLastResponseType = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(this._contextKeyService); + this._requestActiveContextKey = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); + this._terminalAgentRegisteredContextKey = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); + this._lastResponseTypeContextKey = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(this._contextKeyService); - if (!this._chatAgentService.hasAgent('terminal')) { + if (!this._chatAgentService.hasAgent(this._terminalAgentId)) { this._register(this._chatAgentService.onDidChangeAgents(() => { - if (this._chatAgentService.getAgent('terminal')) { - this._ctxHasTerminalAgent.set(true); + if (this._chatAgentService.getAgent(this._terminalAgentId)) { + this._terminalAgentRegisteredContextKey.set(true); } })); } else { - this._ctxHasTerminalAgent.set(true); + this._terminalAgentRegisteredContextKey.set(true); } this._cancellationTokenSource = new CancellationTokenSource(); } @@ -157,9 +160,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } this._chatAccessibilityService.acceptRequest(); - this._ctxHasActiveRequest.set(true); + this._requestActiveContextKey.set(true); const cancellationToken = this._cancellationTokenSource.token; - const agentId = 'terminal'; let responseContent = ''; const progressCallback = (progress: IChatProgress) => { if (cancellationToken.isCancellationRequested) { @@ -174,12 +176,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr const requestProps: IChatAgentRequest = { sessionId: generateUuid(), requestId, - agentId, + agentId: this._terminalAgentId, message: this._lastInput, + // TODO: ? variables: { variables: [] }, }; try { - const task = this._chatAgentService.invokeAgent(agentId, requestProps, progressCallback, [], cancellationToken); + const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, [], cancellationToken); this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); this._chatWidget?.rawValue?.inlineChatWidget.updateFollowUps(undefined); this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(true); @@ -189,7 +192,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } catch (e) { } finally { - this._ctxHasActiveRequest.set(false); + this._requestActiveContextKey.set(false); this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); @@ -205,12 +208,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } if (codeBlock) { - // TODO: check the SR experience this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._accessibilityRequestId, shellType); - this._ctxLastResponseType.set(InlineChatResponseTypes.Empty); + this._lastResponseTypeContextKey.set(InlineChatResponseTypes.Empty); } else { this._chatWidget?.rawValue?.renderMessage(responseContent, this._accessibilityRequestId, requestId); - this._ctxLastResponseType.set(InlineChatResponseTypes.OnlyMessages); + this._lastResponseTypeContextKey.set(InlineChatResponseTypes.OnlyMessages); } this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); From 57857d6546a713f47a0639a55db18ea4224edf54 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 14 Feb 2024 13:22:39 -0600 Subject: [PATCH 061/228] rm unused --- src/vs/workbench/contrib/terminal/common/terminalContextKey.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index a7805bfac46..8ea1469a4b1 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -45,7 +45,6 @@ export const enum TerminalContextKeyStrings { ChatInputHasText = 'terminalChatInputHasText', ChatAgentRegistered = 'terminalChatAgentRegistered', ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', - ChatLastResponseType = 'terminalChatLastResponseType' } export namespace TerminalContextKeys { From a9b4e6fe4446bd2e90c5507e0d32f08e196b292c Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 14 Feb 2024 13:43:46 -0600 Subject: [PATCH 062/228] add accessible view --- .../browser/accessibilityConfiguration.ts | 8 +++- .../terminal/common/terminalContextKey.ts | 3 ++ .../browser/terminal.chat.contribution.ts | 6 +++ .../browser/terminalChatAccessibleView.ts | 41 +++++++++++++++++++ .../chat/browser/terminalChatController.ts | 1 + 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index afd920ac1f9..a5d30356fed 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -52,7 +52,8 @@ export const enum AccessibilityVerbositySettingId { Hover = 'accessibility.verbosity.hover', Notification = 'accessibility.verbosity.notification', EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint', - Comments = 'accessibility.verbosity.comments' + Comments = 'accessibility.verbosity.comments', + TerminalInlineChat = 'accessibility.verbosity.terminalInlineChat' } export const enum AccessibleViewProviderId { @@ -62,6 +63,7 @@ export const enum AccessibleViewProviderId { Chat = 'panelChat', InlineChat = 'inlineChat', InlineCompletions = 'inlineCompletions', + TerminalInlineChat = 'terminalInlineChat', KeybindingsEditor = 'keybindingsEditor', Notebook = 'notebook', Editor = 'editor', @@ -168,6 +170,10 @@ const configuration: IConfigurationNode = { description: localize('verbosity.comments', 'Provide information about actions that can be taken in the comment widget or in a file which contains comments.'), ...baseVerbosityProperty }, + [AccessibilityVerbositySettingId.TerminalInlineChat]: { + description: localize('verbosity.terminalInlineChat', 'Provide information about actions that can be taken in the terminal inline chat widget.'), + ...baseVerbosityProperty + }, [AccessibilityAlertSettingId.Save]: { 'markdownDescription': localize('announcement.save', "Indicates when a file is saved. Also see {0}.", '`#audioCues.save#`'), 'enum': ['userGesture', 'always', 'never'], diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 8ea1469a4b1..5ac4cdca5ac 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -183,4 +183,7 @@ export namespace TerminalContextKeys { /** Whether the chat response editor is focused */ export const chatResponseEditorFocused = new RawContextKey(TerminalContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); + + /** Whether the chat response editor is focused */ + export const chatResponseMessageFocused = new RawContextKey(TerminalContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseMessageFocusedContextKey', "Whether the chat response message is focused.")); } 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 a3a7b55757a..eebaa8b3b8a 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 @@ -5,7 +5,13 @@ import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; +const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(TerminalInlineChatAccessibleViewContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts new file mode 100644 index 00000000000..949a2d8cc51 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +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 { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +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.TerminalInlineChat, + verbositySettingKey: AccessibilityVerbositySettingId.TerminalInlineChat, + provideContent(): string { return responseContent; }, + onClose() { + controller.focus(); + }, + + options: { type: AccessibleViewType.View } + }); + return true; + }, ContextKeyExpr.and(TerminalContextKeys.chatFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 5c105fee353..515d7b895e8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -62,6 +62,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr private _lastInput: string | undefined; private _lastResponseContent: string | undefined; + get lastResponseContent(): string | undefined { return this._lastResponseContent; } 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); From d881ddbae52bec5cdc5e87351d6c3f8f7fa5c67f Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 14 Feb 2024 15:30:36 -0600 Subject: [PATCH 063/228] Add accessibility help dialog --- .../browser/accessibilityConfiguration.ts | 5 -- .../inlineChat/browser/inlineChatActions.ts | 3 +- .../terminal/common/terminalContextKey.ts | 3 - .../browser/terminal.chat.contribution.ts | 2 + .../chat/browser/terminalChat.ts | 2 +- .../browser/terminalChatAccessibilityHelp.ts | 64 +++++++++++++++++++ .../browser/terminalChatAccessibleView.ts | 3 +- .../chat/browser/terminalChatActions.ts | 14 ++-- 8 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index a5d30356fed..bba5a1f648e 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -53,7 +53,6 @@ export const enum AccessibilityVerbositySettingId { Notification = 'accessibility.verbosity.notification', EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint', Comments = 'accessibility.verbosity.comments', - TerminalInlineChat = 'accessibility.verbosity.terminalInlineChat' } export const enum AccessibleViewProviderId { @@ -170,10 +169,6 @@ const configuration: IConfigurationNode = { description: localize('verbosity.comments', 'Provide information about actions that can be taken in the comment widget or in a file which contains comments.'), ...baseVerbosityProperty }, - [AccessibilityVerbositySettingId.TerminalInlineChat]: { - description: localize('verbosity.terminalInlineChat', 'Provide information about actions that can be taken in the terminal inline chat widget.'), - ...baseVerbosityProperty - }, [AccessibilityAlertSettingId.Save]: { 'markdownDescription': localize('announcement.save', "Indicates when a file is saved. Also see {0}.", '`#audioCues.save#`'), 'enum': ['userGesture', 'always', 'never'], diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 85033c64166..02cf92cfa93 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -30,6 +30,7 @@ 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 { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -745,6 +746,6 @@ export class InlineAccessibilityHelpContribution extends Disposable { return; } runAccessibilityHelpAction(accessor, codeEditor, 'inlineChat'); - }, ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED))); + }, ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED), TerminalContextKeys.chatFocused.negate()))); } } diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 5ac4cdca5ac..8ea1469a4b1 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -183,7 +183,4 @@ export namespace TerminalContextKeys { /** Whether the chat response editor is focused */ export const chatResponseEditorFocused = new RawContextKey(TerminalContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); - - /** Whether the chat response editor is focused */ - export const chatResponseMessageFocused = new RawContextKey(TerminalContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseMessageFocusedContextKey', "Whether the chat response message is focused.")); } 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 eebaa8b3b8a..70804842c3e 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 @@ -13,5 +13,7 @@ import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; +import { TerminalInlineChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(TerminalInlineChatAccessibleViewContribution, LifecyclePhase.Eventually); +workbenchContributionsRegistry.registerWorkbenchContribution(TerminalInlineChatAccessibilityHelpContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index ddea1c2a53b..f75c9c3d9a7 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -6,7 +6,7 @@ import { MenuId } from 'vs/platform/actions/common/actions'; export const enum TerminalChatCommandId { - Focus = 'workbench.action.terminal.chat.focus', + Start = 'workbench.action.terminal.chat.start', Hide = 'workbench.action.terminal.chat.close', MakeRequest = 'workbench.action.terminal.chat.makeRequest', Cancel = 'workbench.action.terminal.chat.cancel', diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts new file mode 100644 index 00000000000..5bbe76db85f --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +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 { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; + +export class TerminalInlineChatAccessibilityHelpContribution extends Disposable { + constructor() { + super(); + this._register(AccessibilityHelpAction.addImplementation(106, 'terminalInlineChat', accessor => { + const terminalService = accessor.get(ITerminalService); + const accessibleViewService = accessor.get(IAccessibleViewService); + const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; + if (controller === undefined) { + return false; + } + const helpContent = getAccessibilityHelpText(accessor); + accessibleViewService.show({ + id: AccessibleViewProviderId.TerminalInlineChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, + provideContent(): string { return helpContent; }, + onClose() { + controller.focus(); + }, + options: { type: AccessibleViewType.Help } + }); + return true; + }, ContextKeyExpr.or(TerminalContextKeys.chatFocused, TerminalContextKeys.chatResponseEditorFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + } +} + + +export function getAccessibilityHelpText(accessor: ServicesAccessor): string { + const keybindingService = accessor.get(IKeybindingService); + const content = []; + const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); + const acceptCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.AcceptCommand)?.getAriaLabel(); + const makeRequestKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.MakeRequest)?.getAriaLabel(); + //TODO: using this instead of the terminal command bc by definition the inline terminal chat is focused when this dialog is invoked. + const startChatKeybinding = keybindingService.lookupKeybinding('inlineChat.start')?.getAriaLabel(); + content.push(localize('inlineChat.overview', "Inline chat occurs within a terminal. It is useful for suggesting terminal commands. Keep in mind that AI generated code may be incorrect.")); + content.push(localize('inlineChat.access', "It can be activated using the command: Terminal: Start Chat ({0}), which will focus the input box.", startChatKeybinding)); + content.push(makeRequestKeybinding ? localize('inlineChat.input', "The input box is where the user can type a request and can make the request ({0}). The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.", makeRequestKeybinding) : localize('inlineChat.inputNoKb', "The input box is where the user can type a request and can make the request by tabbing to the Make Request button, which is not currently triggerable via keybindings. The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.")); + content.push(localize('inlineChat.results', "A result may contain a terminal command or just a message. In either case, the result will be announced.")); + content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponseMessage', 'If just a message comes back, it can be inspected 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.inspectTerminalCommand', 'If a terminal command comes back, it can be inspected in an editor reached via Shift+Tab.')); + content.push(acceptCommandKeybinding ? localize('inlineChat.acceptCommand', 'With focus in the command editor, the Terminal: Accept Chat Command ({0}) action.', acceptCommandKeybinding) : localize('inlineChat.acceptCommandNoKb', 'Accept a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); + 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'); +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index 949a2d8cc51..8c146698482 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -27,12 +27,11 @@ export class TerminalInlineChatAccessibleViewContribution extends Disposable { const responseContent = controller.lastResponseContent; accessibleViewService.show({ id: AccessibleViewProviderId.TerminalInlineChat, - verbositySettingKey: AccessibilityVerbositySettingId.TerminalInlineChat, + verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, provideContent(): string { return responseContent; }, onClose() { controller.focus(); }, - options: { type: AccessibleViewType.View } }); return true; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index d6ba18a272e..641737229dc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -17,8 +17,8 @@ import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerActiveXtermAction({ - id: TerminalChatCommandId.Focus, - title: localize2('focusChat', 'Focus Chat'), + id: TerminalChatCommandId.Start, + title: localize2('startChat', 'Terminal: Start Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), @@ -40,7 +40,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.Hide, - title: localize2('closeChat', 'Close Chat'), + title: localize2('closeChat', 'Terminal: Close Chat'), keybinding: { primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], @@ -72,7 +72,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.AcceptCommand, - title: localize2('acceptCommand', 'Accept Command'), + title: localize2('acceptCommand', 'Terminal: Accept Chat Command'), shortTitle: localize2('accept', 'Accept'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -104,7 +104,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.ViewInChat, - title: localize2('viewInChat', 'View in Chat'), + title: localize2('viewInChat', 'Terminal: View in Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -130,7 +130,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.MakeRequest, - title: localize2('makeChatRequest', 'Make Chat Request'), + title: localize2('makeChatRequest', 'Terminal: Make Chat Request'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -161,7 +161,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.Cancel, - title: localize2('cancelChat', 'Cancel Chat'), + title: localize2('cancelChat', 'Terminal: Cancel Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatRequestActive, From b01c303b90fe931c2f4e816172fa8fdaf13ee963 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 10:50:32 -0600 Subject: [PATCH 064/228] add session support, get cancellation to work --- .../inlineChat/browser/inlineChatSession.ts | 9 +- .../chat/browser/terminalChatActions.ts | 4 +- .../chat/browser/terminalChatController.ts | 107 ++++++++++++++---- .../chat/browser/terminalChatWidget.ts | 17 +-- 4 files changed, 96 insertions(+), 41 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 693fde05bab..678c8177976 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -316,7 +316,7 @@ export class SessionExchange { constructor( readonly prompt: SessionPrompt, - readonly response: ReplyResponse | EmptyResponse | ErrorResponse + readonly response: ReplyResponse | EmptyResponse | ErrorResponse | TerminalResponse ) { } } @@ -324,6 +324,13 @@ export class EmptyResponse { } +export class TerminalResponse { + readonly message: string; + constructor(message: string) { + this.message = message; + } +} + export class ErrorResponse { readonly message: string; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 641737229dc..7aef66f5631 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -178,7 +178,7 @@ registerActiveXtermAction({ return; } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.chatWidget?.cancel(); + contr?.cancel(); } }); @@ -234,7 +234,7 @@ registerActiveXtermAction({ // TODO: precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), icon: Codicon.report, menu: [/*{ - // TODO: Enable this + // TODO: Enable id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, when: ContextKeyExpr.and(CTX_TERMINAL_CHAT_SUPPORT_ISSUE_REPORTING, CTX_TERMINAL_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), group: '2_feedback', diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 515d7b895e8..944fba8dbba 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -16,14 +16,24 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { generateUuid } from 'vs/base/common/uuid'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { marked } from 'vs/base/common/marked/marked'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { Emitter, Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; -import { CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_RESPONSE_TYPES, EditMode, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { EmptyResponse, Session, SessionExchange, SessionPrompt, TerminalResponse } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { assertType } from 'vs/base/common/types'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModel } from 'vs/editor/common/model'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; const enum Message { NONE = 0, @@ -54,15 +64,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _terminalAgentRegisteredContextKey!: IContextKey; private readonly _lastResponseTypeContextKey!: IContextKey; - private _cancellationTokenSource!: CancellationTokenSource; + private _scopedInstantiationService: IInstantiationService | undefined; private _accessibilityRequestId: number = 0; private _messages = this._store.add(new Emitter()); - private _lastInput: string | undefined; - private _lastResponseContent: string | undefined; - get lastResponseContent(): string | undefined { return this._lastResponseContent; } + private _activeSession?: Session; + + private _fakeEditor: CodeEditorWidget | undefined; + + get lastResponseContent(): string | undefined { return (this._activeSession?.lastExchange?.response as TerminalResponse).message; } 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); @@ -80,7 +92,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatService private readonly _chatService: IChatService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, + @IModelService private readonly _modelService: IModelService ) { super(); if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { @@ -99,7 +113,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr } else { this._terminalAgentRegisteredContextKey.set(true); } - this._cancellationTokenSource = new CancellationTokenSource(); } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { @@ -107,7 +120,25 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } this._chatWidget = new Lazy(() => { - const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); + const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._instance.domElement!)); + this._scopedInstantiationService = this._instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); + // The inline chat widget requires a parent editor that it bases the diff view on, since the + // terminal doesn't use that feature we can just pass in an unattached editor instance. + const fakeParentEditorElement = document.createElement('div'); + this._fakeEditor = this._scopedInstantiationService.createInstance( + CodeEditorWidget, + fakeParentEditorElement, + { + extraEditorClassName: 'ignore-panel-bg' + }, + { isSimpleWidget: true } + ); + + const path = `terminal-chat-input-${this._instance.instanceId}`; + const inputUri = URI.from({ path: path, scheme: Schemas.untitled, fragment: '' }); + const result: ITextModel = this._modelService.createModel('', null, inputUri, false); + this._fakeEditor.setModel(result); + const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._fakeEditor!, this._instance); chatWidget.focusTracker.onDidFocus(() => { TerminalChatController.activeChatWidget = this; if (!isDetachedTerminalInstance(this._instance)) { @@ -127,7 +158,28 @@ export class TerminalChatController extends Disposable implements ITerminalContr } cancel(): void { - this._cancellationTokenSource.cancel(); + if (this._activeSession) { + this._inlineChatSessionService.releaseSession(this._activeSession); + this._activeSession = 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; } private _forcedPlaceholder: string | undefined = undefined; @@ -156,13 +208,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr } async acceptInput(): Promise { - this._lastInput = this._chatWidget?.rawValue?.input(); - if (!this._lastInput) { - return; - } this._chatAccessibilityService.acceptRequest(); this._requestActiveContextKey.set(true); - const cancellationToken = this._cancellationTokenSource.token; + const cancellationToken = new CancellationTokenSource().token; + if (this._fakeEditor?.hasModel()) { + await this._startSession(this._fakeEditor, cancellationToken); + } + assertType(this._activeSession); + const inputValue = this.chatWidget!.inlineChatWidget.value; + this._activeSession!.addInput(new SessionPrompt(inputValue)); let responseContent = ''; const progressCallback = (progress: IChatProgress) => { if (cancellationToken.isCancellationRequested) { @@ -178,10 +232,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr sessionId: generateUuid(), requestId, agentId: this._terminalAgentId, - message: this._lastInput, + message: inputValue, // TODO: ? variables: { variables: [] }, }; + let response: EmptyResponse | TerminalResponse = EmptyResponse; try { const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, [], cancellationToken); this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); @@ -191,14 +246,16 @@ export class TerminalChatController extends Disposable implements ITerminalContr // TODO: this._zone.value.widget.updateInfo(!this._session.lastExchange ? localize('thinking', "Thinking\u2026") : ''); await task; } catch (e) { - + response = e; } finally { this._requestActiveContextKey.set(false); this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + if (response === EmptyResponse) { + response = new TerminalResponse(responseContent); + } } - this._lastResponseContent = responseContent; const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; const regex = /```(?\w+)\n(?[\s\S]*?)```/g; const match = regex.exec(firstCodeBlockContent); @@ -215,6 +272,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.renderMessage(responseContent, this._accessibilityRequestId, requestId); this._lastResponseTypeContextKey.set(InlineChatResponseTypes.OnlyMessages); } + if (this._activeSession?.lastInput) { + this._activeSession.addExchange(new SessionExchange(this._activeSession.lastInput, response)); + } this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); } @@ -250,12 +310,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr } async viewInChat(): Promise { - if (!this._lastInput || !this._lastResponseContent) { - return; - } const widget = await this._chatWidgetService.revealViewForProvider('copilot'); if (widget && widget.viewModel) { - this._chatService.addCompleteRequest(widget.viewModel.sessionId, this._lastInput, undefined, { message: this._lastResponseContent }); + const request = this._activeSession?.lastExchange; + const input = request?.prompt.value; + const response = request?.response as TerminalResponse; + if (!input || !response) { + return; + } + this._chatService.addCompleteRequest(widget.viewModel.sessionId, input, undefined, response); widget.focusLastMessage(); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index a485dd40c4f..bbe822e01a6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -42,6 +42,7 @@ export class TerminalChatWidget extends Disposable { constructor( terminalElement: HTMLElement, + fakeParentEditor: CodeEditorWidget, private readonly _instance: ITerminalInstance, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -64,18 +65,6 @@ export class TerminalChatWidget extends Disposable { this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); this._container.prepend(this._terminalCommandWidgetContainer); - // The inline chat widget requires a parent editor that it bases the diff view on, since the - // terminal doesn't use that feature we can just pass in an unattached editor instance. - const fakeParentEditorElement = document.createElement('div'); - const fakeParentEditor = this._scopedInstantiationService.createInstance( - CodeEditorWidget, - fakeParentEditorElement, - { - extraEditorClassName: 'ignore-panel-bg' - }, - { isSimpleWidget: true } - ); - this._inlineChatWidget = this._scopedInstantiationService.createInstance( InlineChatWidget, fakeParentEditor, @@ -199,10 +188,6 @@ export class TerminalChatWidget extends Disposable { this._visibleContextKey.set(false); this._instance.focus(); } - cancel(): void { - // TODO: Impl - this._inlineChatWidget.value = ''; - } focus(): void { this._inlineChatWidget.focus(); } From 216b941cd96e4022e50b7c92d446d090c64379ae Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 10:28:12 -0800 Subject: [PATCH 065/228] Move to registerWorkbenchContribution2 --- .../chat/browser/terminal.chat.contribution.ts | 16 +++++++--------- .../browser/terminalChatAccessibilityHelp.ts | 1 + 2 files changed, 8 insertions(+), 9 deletions(-) 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 70804842c3e..0c2dba50035 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,17 +3,15 @@ * 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 { TerminalInlineChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; +import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; + +import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); -import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { TerminalInlineChatAccessibleViewContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView'; -import { TerminalInlineChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; -const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchContributionsRegistry.registerWorkbenchContribution(TerminalInlineChatAccessibleViewContribution, LifecyclePhase.Eventually); -workbenchContributionsRegistry.registerWorkbenchContribution(TerminalInlineChatAccessibilityHelpContribution, LifecyclePhase.Eventually); +registerWorkbenchContribution2(TerminalInlineChatAccessibleViewContribution.ID, TerminalInlineChatAccessibleViewContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(TerminalInlineChatAccessibilityHelpContribution.ID, TerminalInlineChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index 5bbe76db85f..2fe29ea5d8a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -18,6 +18,7 @@ import { TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; export class TerminalInlineChatAccessibilityHelpContribution extends Disposable { + static ID: 'terminalInlineChatAccessibilityHelpContribution'; constructor() { super(); this._register(AccessibilityHelpAction.addImplementation(106, 'terminalInlineChat', accessor => { From 58f5274002e29d30dba54da2ac53a4dc8c5a0b3b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 10:36:59 -0800 Subject: [PATCH 066/228] Remove scoped services, they were causing an error and breaking language highlighting --- .../chat/browser/terminalChatController.ts | 58 +++++++++---------- .../chat/browser/terminalChatWidget.ts | 12 ++-- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 944fba8dbba..86c17c99d22 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -3,37 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; -import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { Lazy } from 'vs/base/common/lazy'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as RawXtermTerminal } from '@xterm/xterm'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Lazy } from 'vs/base/common/lazy'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { marked } from 'vs/base/common/marked/marked'; +import { Schemas } from 'vs/base/common/network'; +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/codeEditorWidget'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { marked } from 'vs/base/common/marked/marked'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { Emitter, Event } from 'vs/base/common/event'; -import { localize } from 'vs/nls'; -import { CTX_INLINE_CHAT_RESPONSE_TYPES, EditMode, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { EmptyResponse, Session, SessionExchange, SessionPrompt, TerminalResponse } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { assertType } from 'vs/base/common/types'; -import { IModelService } from 'vs/editor/common/services/model'; -import { ITextModel } from 'vs/editor/common/model'; -import { Schemas } from 'vs/base/common/network'; -import { URI } from 'vs/base/common/uri'; +import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; +import { CTX_INLINE_CHAT_RESPONSE_TYPES, EditMode, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +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'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; const enum Message { NONE = 0, @@ -64,8 +63,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _terminalAgentRegisteredContextKey!: IContextKey; private readonly _lastResponseTypeContextKey!: IContextKey; - private _scopedInstantiationService: IInstantiationService | undefined; - private _accessibilityRequestId: number = 0; private _messages = this._store.add(new Emitter()); @@ -97,6 +94,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IModelService private readonly _modelService: IModelService ) { super(); + if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { return; } @@ -120,12 +118,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } this._chatWidget = new Lazy(() => { - const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._instance.domElement!)); - this._scopedInstantiationService = this._instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); // The inline chat widget requires a parent editor that it bases the diff view on, since the // terminal doesn't use that feature we can just pass in an unattached editor instance. const fakeParentEditorElement = document.createElement('div'); - this._fakeEditor = this._scopedInstantiationService.createInstance( + this._fakeEditor = this._instantiationService.createInstance( CodeEditorWidget, fakeParentEditorElement, { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index bbe822e01a6..67d671aae28 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -15,7 +15,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; @@ -35,7 +34,7 @@ export class TerminalChatWidget extends Disposable { private readonly _focusTracker: IFocusTracker; - private readonly _scopedInstantiationService: IInstantiationService; + // private readonly _scopedInstantiationService: IInstantiationService; private readonly _focusedContextKey: IContextKey; private readonly _visibleContextKey: IContextKey; private readonly _responseEditorFocusedContextKey!: IContextKey; @@ -44,15 +43,14 @@ export class TerminalChatWidget extends Disposable { terminalElement: HTMLElement, fakeParentEditor: CodeEditorWidget, private readonly _instance: ITerminalInstance, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @ILanguageService private readonly _languageService: ILanguageService, @IModelService private readonly _modelService: IModelService ) { super(); - const scopedContextKeyService = this._register(this._contextKeyService.createScoped(terminalElement)); - this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); + this._focusedContextKey = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); this._visibleContextKey = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); this._responseEditorFocusedContextKey = TerminalContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); @@ -65,7 +63,7 @@ export class TerminalChatWidget extends Disposable { this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); this._container.prepend(this._terminalCommandWidgetContainer); - this._inlineChatWidget = this._scopedInstantiationService.createInstance( + this._inlineChatWidget = this._instantiationService.createInstance( InlineChatWidget, fakeParentEditor, { @@ -86,7 +84,7 @@ export class TerminalChatWidget extends Disposable { this._chatAccessibilityService.acceptResponse(command, requestId); this.showTerminalCommandWidget(); if (!this._terminalCommandWidget) { - this._terminalCommandWidget = this._register(this._scopedInstantiationService.createInstance(CodeEditorWidget, this._terminalCommandWidgetContainer, { + this._terminalCommandWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._terminalCommandWidgetContainer, { padding: { top: 2, bottom: 2 }, overviewRulerLanes: 0, glyphMargin: false, From 804a4816393bb289678a0457e8653ce72032d1b7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:00:18 -0800 Subject: [PATCH 067/228] Improve command suggestion presentation --- .../chat/browser/media/terminalChatWidget.css | 23 ++++++++++++++++--- .../chat/browser/terminalChatWidget.ts | 23 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index 633b6778dfe..a916f2dd0d7 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -10,6 +10,23 @@ z-index: 100; height: auto !important; } -/* .terminal-inline-chat .inline-chat .body { - display: flex; -} */ + +.terminal-inline-chat .terminal-inline-chat-response { + border: 1px solid var(--vscode-input-border, transparent); + background-color: var(--vscode-interactive-result-editor-background-color); +} + +.terminal-inline-chat .terminal-inline-chat-response:has(.monaco-editor.focused) { + border-color: var(--vscode-focusBorder, transparent); +} + +.terminal-inline-chat .terminal-inline-chat-response, +.terminal-inline-chat .terminal-inline-chat-response .monaco-editor, +.terminal-inline-chat .terminal-inline-chat-response .monaco-editor .overflow-guard { + border-radius: 4px; +} + +.terminal-inline-chat .terminal-inline-chat-response { + margin: 0 0 8px 0; + padding-left: 8px; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 67d671aae28..9f4f960ee5e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -13,8 +13,10 @@ import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; @@ -34,7 +36,6 @@ export class TerminalChatWidget extends Disposable { private readonly _focusTracker: IFocusTracker; - // private readonly _scopedInstantiationService: IInstantiationService; private readonly _focusedContextKey: IContextKey; private readonly _visibleContextKey: IContextKey; private readonly _responseEditorFocusedContextKey!: IContextKey; @@ -44,6 +45,7 @@ export class TerminalChatWidget extends Disposable { fakeParentEditor: CodeEditorWidget, private readonly _instance: ITerminalInstance, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @ILanguageService private readonly _languageService: ILanguageService, @@ -80,12 +82,25 @@ export class TerminalChatWidget extends Disposable { this._focusTracker = this._register(trackFocus(this._container)); } + + private _getAriaLabel(): string { + const verbose = this._configurationService.getValue(AccessibilityVerbositySettingId.Chat); + if (verbose) { + // TODO: Add verbose description + } + return localize('terminalChatInput', "Terminal Chat Input"); + } + renderTerminalCommand(command: string, requestId: number, shellType?: string): void { this._chatAccessibilityService.acceptResponse(command, requestId); this.showTerminalCommandWidget(); if (!this._terminalCommandWidget) { this._terminalCommandWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._terminalCommandWidgetContainer, { - padding: { top: 2, bottom: 2 }, + readOnly: false, + ariaLabel: this._getAriaLabel(), + fontSize: 13, + lineHeight: 20, + padding: { top: 8, bottom: 8 }, overviewRulerLanes: 0, glyphMargin: false, lineNumbers: 'off', @@ -133,7 +148,9 @@ export class TerminalChatWidget extends Disposable { } else { this._terminalCommandWidget.setValue(command); } - this._terminalCommandWidget.getModel()?.setLanguage(this._getLanguageFromShell(shellType)); + const languageId = this._getLanguageFromShell(shellType); + console.log('languageId', languageId); + this._terminalCommandWidget.getModel()?.setLanguage(languageId); } private _getLanguageFromShell(shell?: string): string { From bf87f8dc1e9afcef04eb8465f221628ee13645d3 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 12:20:22 -0600 Subject: [PATCH 068/228] Revert "add session support, get cancellation to work" This reverts commit b01c303b90fe931c2f4e816172fa8fdaf13ee963. --- .../inlineChat/browser/inlineChatSession.ts | 9 +- .../chat/browser/terminalChatActions.ts | 4 +- .../chat/browser/terminalChatController.ts | 102 +++++++----------- .../chat/browser/terminalChatWidget.ts | 17 ++- 4 files changed, 60 insertions(+), 72 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index 678c8177976..693fde05bab 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -316,7 +316,7 @@ export class SessionExchange { constructor( readonly prompt: SessionPrompt, - readonly response: ReplyResponse | EmptyResponse | ErrorResponse | TerminalResponse + readonly response: ReplyResponse | EmptyResponse | ErrorResponse ) { } } @@ -324,13 +324,6 @@ export class EmptyResponse { } -export class TerminalResponse { - readonly message: string; - constructor(message: string) { - this.message = message; - } -} - export class ErrorResponse { readonly message: string; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 7aef66f5631..641737229dc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -178,7 +178,7 @@ registerActiveXtermAction({ return; } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.cancel(); + contr?.chatWidget?.cancel(); } }); @@ -234,7 +234,7 @@ registerActiveXtermAction({ // TODO: precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), icon: Codicon.report, menu: [/*{ - // TODO: Enable + // TODO: Enable this id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, when: ContextKeyExpr.and(CTX_TERMINAL_CHAT_SUPPORT_ISSUE_REPORTING, CTX_TERMINAL_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), group: '2_feedback', diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 86c17c99d22..54430cd8fee 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -25,14 +25,25 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { EmptyResponse, Session, SessionExchange, SessionPrompt, TerminalResponse } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { CTX_INLINE_CHAT_RESPONSE_TYPES, EditMode, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -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'; +import { generateUuid } from 'vs/base/common/uuid'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { marked } from 'vs/base/common/marked/marked'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { Emitter, Event } from 'vs/base/common/event'; +import { localize } from 'vs/nls'; +import { CTX_INLINE_CHAT_RESPONSE_TYPES, EditMode, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { EmptyResponse, Session, SessionExchange, SessionPrompt, TerminalResponse } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { assertType } from 'vs/base/common/types'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ITextModel } from 'vs/editor/common/model'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; const enum Message { NONE = 0, @@ -63,15 +74,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _terminalAgentRegisteredContextKey!: IContextKey; private readonly _lastResponseTypeContextKey!: IContextKey; + private _scopedInstantiationService: IInstantiationService | undefined; + private _accessibilityRequestId: number = 0; private _messages = this._store.add(new Emitter()); - private _activeSession?: Session; - - private _fakeEditor: CodeEditorWidget | undefined; - - get lastResponseContent(): string | undefined { return (this._activeSession?.lastExchange?.response as TerminalResponse).message; } + private _lastInput: string | undefined; + private _lastResponseContent: string | undefined; + get lastResponseContent(): string | undefined { return this._lastResponseContent; } 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); @@ -89,9 +100,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatService private readonly _chatService: IChatService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, - @IModelService private readonly _modelService: IModelService + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService ) { super(); @@ -111,6 +120,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } else { this._terminalAgentRegisteredContextKey.set(true); } + this._cancellationTokenSource = new CancellationTokenSource(); } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { @@ -118,10 +128,12 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } this._chatWidget = new Lazy(() => { + const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._instance.domElement!)); + this._scopedInstantiationService = this._instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); // The inline chat widget requires a parent editor that it bases the diff view on, since the // terminal doesn't use that feature we can just pass in an unattached editor instance. const fakeParentEditorElement = document.createElement('div'); - this._fakeEditor = this._instantiationService.createInstance( + this._fakeEditor = this._scopedInstantiationService.createInstance( CodeEditorWidget, fakeParentEditorElement, { @@ -154,28 +166,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } cancel(): void { - if (this._activeSession) { - this._inlineChatSessionService.releaseSession(this._activeSession); - this._activeSession = 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._cancellationTokenSource.cancel(); } private _forcedPlaceholder: string | undefined = undefined; @@ -204,15 +195,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr } async acceptInput(): Promise { + this._lastInput = this._chatWidget?.rawValue?.input(); + if (!this._lastInput) { + return; + } this._chatAccessibilityService.acceptRequest(); this._requestActiveContextKey.set(true); - const cancellationToken = new CancellationTokenSource().token; - if (this._fakeEditor?.hasModel()) { - await this._startSession(this._fakeEditor, cancellationToken); - } - assertType(this._activeSession); - const inputValue = this.chatWidget!.inlineChatWidget.value; - this._activeSession!.addInput(new SessionPrompt(inputValue)); + const cancellationToken = this._cancellationTokenSource.token; let responseContent = ''; const progressCallback = (progress: IChatProgress) => { if (cancellationToken.isCancellationRequested) { @@ -228,11 +217,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr sessionId: generateUuid(), requestId, agentId: this._terminalAgentId, - message: inputValue, + message: this._lastInput, // TODO: ? variables: { variables: [] }, }; - let response: EmptyResponse | TerminalResponse = EmptyResponse; try { const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, [], cancellationToken); this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); @@ -242,16 +230,14 @@ export class TerminalChatController extends Disposable implements ITerminalContr // TODO: this._zone.value.widget.updateInfo(!this._session.lastExchange ? localize('thinking', "Thinking\u2026") : ''); await task; } catch (e) { - response = e; + } finally { this._requestActiveContextKey.set(false); this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); - if (response === EmptyResponse) { - response = new TerminalResponse(responseContent); - } } + this._lastResponseContent = responseContent; const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; const regex = /```(?\w+)\n(?[\s\S]*?)```/g; const match = regex.exec(firstCodeBlockContent); @@ -268,9 +254,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.renderMessage(responseContent, this._accessibilityRequestId, requestId); this._lastResponseTypeContextKey.set(InlineChatResponseTypes.OnlyMessages); } - if (this._activeSession?.lastInput) { - this._activeSession.addExchange(new SessionExchange(this._activeSession.lastInput, response)); - } this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); } @@ -306,15 +289,12 @@ export class TerminalChatController extends Disposable implements ITerminalContr } async viewInChat(): Promise { + if (!this._lastInput || !this._lastResponseContent) { + return; + } const widget = await this._chatWidgetService.revealViewForProvider('copilot'); if (widget && widget.viewModel) { - const request = this._activeSession?.lastExchange; - const input = request?.prompt.value; - const response = request?.response as TerminalResponse; - if (!input || !response) { - return; - } - this._chatService.addCompleteRequest(widget.viewModel.sessionId, input, undefined, response); + this._chatService.addCompleteRequest(widget.viewModel.sessionId, this._lastInput, undefined, { message: this._lastResponseContent }); widget.focusLastMessage(); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 9f4f960ee5e..39892d8a381 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -42,7 +42,6 @@ export class TerminalChatWidget extends Disposable { constructor( terminalElement: HTMLElement, - fakeParentEditor: CodeEditorWidget, private readonly _instance: ITerminalInstance, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -65,6 +64,18 @@ export class TerminalChatWidget extends Disposable { this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); this._container.prepend(this._terminalCommandWidgetContainer); + // The inline chat widget requires a parent editor that it bases the diff view on, since the + // terminal doesn't use that feature we can just pass in an unattached editor instance. + const fakeParentEditorElement = document.createElement('div'); + const fakeParentEditor = this._instantiationService.createInstance( + CodeEditorWidget, + fakeParentEditorElement, + { + extraEditorClassName: 'ignore-panel-bg' + }, + { isSimpleWidget: true } + ); + this._inlineChatWidget = this._instantiationService.createInstance( InlineChatWidget, fakeParentEditor, @@ -203,6 +214,10 @@ export class TerminalChatWidget extends Disposable { this._visibleContextKey.set(false); this._instance.focus(); } + cancel(): void { + // TODO: Impl + this._inlineChatWidget.value = ''; + } focus(): void { this._inlineChatWidget.focus(); } From 55b7c86d9a818977b50d602a01d2bdfe7e72f74b Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 13:00:27 -0600 Subject: [PATCH 069/228] use chat model --- .../chat/browser/terminalChatActions.ts | 4 +- .../chat/browser/terminalChatController.ts | 73 +++++++++++++++++-- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 641737229dc..a1561e9b63c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -63,7 +63,7 @@ registerActiveXtermAction({ return; } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.chatWidget?.hide(); + contr?.clear(); } }); @@ -178,7 +178,7 @@ registerActiveXtermAction({ return; } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.chatWidget?.cancel(); + contr?.cancel(); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 54430cd8fee..6daeb2bd3cb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -44,6 +44,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ITextModel } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; +import { ChatRequestModel } from 'vs/workbench/contrib/chat/common/chatModel'; const enum Message { NONE = 0, @@ -80,15 +81,22 @@ export class TerminalChatController extends Disposable implements ITerminalContr private _messages = this._store.add(new Emitter()); + private _currentRequest: ChatRequestModel | undefined; + private _lastInput: string | undefined; private _lastResponseContent: string | undefined; - get lastResponseContent(): string | undefined { return this._lastResponseContent; } + get lastResponseContent(): string | undefined { + // TODO: use model + return this._lastResponseContent; + } 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); private _terminalAgentId = 'terminal'; + private _model: ChatModel | undefined; + constructor( private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, @@ -99,8 +107,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IChatService private readonly _chatService: IChatService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService ) { super(); @@ -166,7 +174,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr } cancel(): void { - this._cancellationTokenSource.cancel(); + if (this._currentRequest) { + this._model?.cancelRequest(this._currentRequest); + } } private _forcedPlaceholder: string | undefined = undefined; @@ -194,7 +204,24 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._updatePlaceholder(); } + clear(): void { + this._model?.dispose(); + this._model = undefined; + this.updateModel(); + this._chatWidget?.rawValue?.hide(); + this._chatWidget?.rawValue?.setValue(undefined); + } + + private updateModel(): void { + const providerInfo = this._chatService.getProviderInfos()?.[0]; + if (!providerInfo) { + return; + } + this._model ??= this._chatService.startSession(providerInfo.id, CancellationToken.None); + } + async acceptInput(): Promise { + this.updateModel(); this._lastInput = this._chatWidget?.rawValue?.input(); if (!this._lastInput) { return; @@ -211,6 +238,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (progress.kind === 'content' || progress.kind === 'markdownContent') { responseContent += progress.content; } + if (this._currentRequest) { + this._model?.acceptResponseProgress(this._currentRequest, progress); + } }; const requestId = generateUuid(); const requestProps: IChatAgentRequest = { @@ -221,6 +251,16 @@ export class TerminalChatController extends Disposable implements ITerminalContr // TODO: ? variables: { variables: [] }, }; + // TODO: fix requester usrname, responder username + this._model?.initialize({ id: this._accessibilityRequestId, requesterUsername: 'userGesture', responderUsername: 'terminal' }, undefined); + const request: IParsedChatRequest = { + text: this._lastInput, + parts: [] + }; + const requestVarData: IChatRequestVariableData = { + variables: [] + }; + this._currentRequest = this._model?.addRequest(request, requestVarData); try { const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, [], cancellationToken); this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); @@ -236,6 +276,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + if (this._currentRequest) { + this._model?.completeResponse(this._currentRequest); + } } this._lastResponseContent = responseContent; const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; @@ -289,18 +332,34 @@ export class TerminalChatController extends Disposable implements ITerminalContr } async viewInChat(): Promise { - if (!this._lastInput || !this._lastResponseContent) { + const providerInfo = this._chatService.getProviderInfos()?.[0]; + if (!providerInfo) { return; } - const widget = await this._chatWidgetService.revealViewForProvider('copilot'); - if (widget && widget.viewModel) { - this._chatService.addCompleteRequest(widget.viewModel.sessionId, this._lastInput, undefined, { message: this._lastResponseContent }); + const widget = await this._chatWidgetService.revealViewForProvider(providerInfo.id); + if (widget && widget.viewModel && this._model) { + for (const request of this._model.getRequests()) { + if (request.response?.response.value || request.response?.result) { + this._chatService.addCompleteRequest(widget.viewModel.sessionId, + request.message as IParsedChatRequest, + request.variableData, + { + message: request.response.response.value, + result: request.response.result, + followups: request.response.followups + }); + } + } widget.focusLastMessage(); } } override dispose() { + if (this._currentRequest) { + this._model?.cancelRequest(this._currentRequest); + } super.dispose(); + this.clear(); this._chatWidget?.rawValue?.dispose(); } } From 79ef477d826dc46e9617a8b2db3477841c89e4e4 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 13:11:42 -0600 Subject: [PATCH 070/228] rm unused --- .../chat/browser/terminalChatController.ts | 64 ++++--------------- 1 file changed, 13 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 6daeb2bd3cb..bfafbb102fc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -9,42 +9,25 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; -import { Schemas } from 'vs/base/common/network'; -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/codeEditorWidget'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/model'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { marked } from 'vs/base/common/marked/marked'; +import { IChatAgentService, IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatService, IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { InlineChatResponseTypes, CTX_INLINE_CHAT_RESPONSE_TYPES } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +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'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { Emitter, Event } from 'vs/base/common/event'; -import { localize } from 'vs/nls'; -import { CTX_INLINE_CHAT_RESPONSE_TYPES, EditMode, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { IInlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessionService'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { EmptyResponse, Session, SessionExchange, SessionPrompt, TerminalResponse } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { assertType } from 'vs/base/common/types'; -import { IModelService } from 'vs/editor/common/services/model'; -import { ITextModel } from 'vs/editor/common/model'; -import { Schemas } from 'vs/base/common/network'; -import { URI } from 'vs/base/common/uri'; -import { ChatRequestModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; + + +import { ChatModel, ChatRequestModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; const enum Message { NONE = 0, @@ -74,9 +57,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _requestActiveContextKey!: IContextKey; private readonly _terminalAgentRegisteredContextKey!: IContextKey; private readonly _lastResponseTypeContextKey!: IContextKey; - - private _scopedInstantiationService: IInstantiationService | undefined; - private _accessibilityRequestId: number = 0; private _messages = this._store.add(new Emitter()); @@ -128,7 +108,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr } else { this._terminalAgentRegisteredContextKey.set(true); } - this._cancellationTokenSource = new CancellationTokenSource(); } xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { @@ -136,25 +115,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr return; } this._chatWidget = new Lazy(() => { - const scopedContextKeyService = this._register(this._contextKeyService.createScoped(this._instance.domElement!)); - this._scopedInstantiationService = this._instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService])); - // The inline chat widget requires a parent editor that it bases the diff view on, since the - // terminal doesn't use that feature we can just pass in an unattached editor instance. - const fakeParentEditorElement = document.createElement('div'); - this._fakeEditor = this._scopedInstantiationService.createInstance( - CodeEditorWidget, - fakeParentEditorElement, - { - extraEditorClassName: 'ignore-panel-bg' - }, - { isSimpleWidget: true } - ); - const path = `terminal-chat-input-${this._instance.instanceId}`; - const inputUri = URI.from({ path: path, scheme: Schemas.untitled, fragment: '' }); - const result: ITextModel = this._modelService.createModel('', null, inputUri, false); - this._fakeEditor.setModel(result); - const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._fakeEditor!, this._instance); + const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); chatWidget.focusTracker.onDidFocus(() => { TerminalChatController.activeChatWidget = this; if (!isDetachedTerminalInstance(this._instance)) { @@ -228,7 +190,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } this._chatAccessibilityService.acceptRequest(); this._requestActiveContextKey.set(true); - const cancellationToken = this._cancellationTokenSource.token; + const cancellationToken = new CancellationTokenSource().token; let responseContent = ''; const progressCallback = (progress: IChatProgress) => { if (cancellationToken.isCancellationRequested) { From f7915fc65f7639a109599dc133f7dd2fa17282a3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:06:19 -0800 Subject: [PATCH 071/228] Accept -> Run, add insert button, improve keybindings --- .../chat/browser/terminalChat.ts | 3 +- .../browser/terminalChatAccessibilityHelp.ts | 2 +- .../chat/browser/terminalChatActions.ts | 44 ++++++++++++++++--- .../chat/browser/terminalChatController.ts | 4 +- .../chat/browser/terminalChatWidget.ts | 4 +- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index f75c9c3d9a7..d409361d517 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -13,7 +13,8 @@ export const enum TerminalChatCommandId { FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful', FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue', - AcceptCommand = 'workbench.action.terminal.chat.acceptCommand', + RunCommand = 'workbench.action.terminal.chat.runCommand', + InsertCommand = 'workbench.action.terminal.chat.insertCommand', ViewInChat = 'workbench.action.terminal.chat.viewInChat', } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index 2fe29ea5d8a..d851609de14 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -48,7 +48,7 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor): string { const keybindingService = accessor.get(IKeybindingService); const content = []; const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); - const acceptCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.AcceptCommand)?.getAriaLabel(); + const acceptCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.RunCommand)?.getAriaLabel(); const makeRequestKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.MakeRequest)?.getAriaLabel(); //TODO: using this instead of the terminal command bc by definition the inline terminal chat is focused when this dialog is invoked. const startChatKeybinding = keybindingService.lookupKeybinding('inlineChat.start')?.getAriaLabel(); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index a1561e9b63c..901f536f0bf 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -71,9 +71,9 @@ registerActiveXtermAction({ registerActiveXtermAction({ - id: TerminalChatCommandId.AcceptCommand, - title: localize2('acceptCommand', 'Terminal: Accept Chat Command'), - shortTitle: localize2('accept', 'Accept'), + id: TerminalChatCommandId.RunCommand, + title: localize2('runCommand', 'Terminal: Run Chat Command'), + shortTitle: localize2('run', 'Run'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -83,8 +83,8 @@ registerActiveXtermAction({ ), icon: Codicon.check, keybinding: { - when: ContextKeyExpr.and(TerminalContextKeys.chatResponseEditorFocused, TerminalContextKeys.chatRequestActive.negate()), - weight: KeybindingWeight.EditorCore + 7, + when: TerminalContextKeys.chatRequestActive.negate(), + weight: KeybindingWeight.WorkbenchContrib + 7, primary: KeyMod.CtrlCmd | KeyCode.Enter, }, menu: { @@ -98,7 +98,39 @@ registerActiveXtermAction({ return; } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - contr?.acceptCommand(); + contr?.acceptCommand(true); + } +}); + +registerActiveXtermAction({ + id: TerminalChatCommandId.InsertCommand, + title: localize2('insertCommand', 'Terminal: Insert Chat Command'), + shortTitle: localize2('insert', 'Insert'), + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalContextKeys.chatRequestActive.negate(), + TerminalContextKeys.chatAgentRegistered, + CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty) + ), + icon: Codicon.check, + keybinding: { + when: TerminalContextKeys.chatRequestActive.negate(), + weight: KeybindingWeight.WorkbenchContrib + 7, + primary: KeyMod.Alt | KeyCode.Enter, + }, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 1, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), TerminalContextKeys.chatRequestActive.negate()), + }, + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptCommand(false); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index bfafbb102fc..903c81b74fb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -285,8 +285,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr return !!this._chatWidget?.rawValue?.hasFocus(); } - acceptCommand(): void { - this._chatWidget?.rawValue?.acceptCommand(); + acceptCommand(shouldExecute: boolean): void { + this._chatWidget?.rawValue?.acceptCommand(shouldExecute); } reveal(): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 39892d8a381..53a67093ffb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -233,12 +233,12 @@ export class TerminalChatWidget extends Disposable { this.hideTerminalCommandWidget(); } } - acceptCommand(): void { + acceptCommand(shouldExecute: boolean): void { const value = this._terminalCommandWidget?.getValue(); if (!value) { return; } - this._instance.sendText(value, false, true); + this._instance.runCommand(value, shouldExecute); this.hide(); } updateProgress(progress?: IChatProgress): void { From a9c300cd68ec0b2f1290aa32141367fbb0abf6da Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:11:28 -0800 Subject: [PATCH 072/228] Trim suggestion to prevent running on alt+enter --- .../terminalContrib/chat/browser/terminalChatController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 903c81b74fb..d0d9590813e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -246,7 +246,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; const regex = /```(?\w+)\n(?[\s\S]*?)```/g; const match = regex.exec(firstCodeBlockContent); - const codeBlock = match?.groups?.content; + const codeBlock = match?.groups?.content.trim(); const shellType = match?.groups?.language; this._accessibilityRequestId++; if (cancellationToken.isCancellationRequested) { From 4d81b4e49afb5581431a2534463ed522894a0b36 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 13:15:53 -0600 Subject: [PATCH 073/228] cancel request on clear --- .../terminalContrib/chat/browser/terminalChatController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index d0d9590813e..6f292f51fde 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -167,9 +167,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr } clear(): void { + if (this._currentRequest) { + this._model?.cancelRequest(this._currentRequest); + } this._model?.dispose(); this._model = undefined; - this.updateModel(); this._chatWidget?.rawValue?.hide(); this._chatWidget?.rawValue?.setValue(undefined); } From 50a0433578968dea132f628203144e55c991246f Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 13:17:31 -0600 Subject: [PATCH 074/228] rename --- .../chat/browser/terminalChatController.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 6f292f51fde..2585ae766da 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -57,7 +57,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _requestActiveContextKey!: IContextKey; private readonly _terminalAgentRegisteredContextKey!: IContextKey; private readonly _lastResponseTypeContextKey!: IContextKey; - private _accessibilityRequestId: number = 0; + private _requestId: number = 0; private _messages = this._store.add(new Emitter()); @@ -216,7 +216,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr variables: { variables: [] }, }; // TODO: fix requester usrname, responder username - this._model?.initialize({ id: this._accessibilityRequestId, requesterUsername: 'userGesture', responderUsername: 'terminal' }, undefined); + this._model?.initialize({ id: this._requestId, requesterUsername: 'userGesture', responderUsername: 'terminal' }, undefined); const request: IParsedChatRequest = { text: this._lastInput, parts: [] @@ -250,15 +250,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr const match = regex.exec(firstCodeBlockContent); const codeBlock = match?.groups?.content.trim(); const shellType = match?.groups?.language; - this._accessibilityRequestId++; + this._requestId++; if (cancellationToken.isCancellationRequested) { return; } if (codeBlock) { - this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._accessibilityRequestId, shellType); + this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._requestId, shellType); this._lastResponseTypeContextKey.set(InlineChatResponseTypes.Empty); } else { - this._chatWidget?.rawValue?.renderMessage(responseContent, this._accessibilityRequestId, requestId); + this._chatWidget?.rawValue?.renderMessage(responseContent, this._requestId, requestId); this._lastResponseTypeContextKey.set(InlineChatResponseTypes.OnlyMessages); } this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); From 1a0199379a05a6a2f9f5c9e80ec410b2808334a0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:22:56 -0800 Subject: [PATCH 075/228] More tweaks to response style --- .../terminalContrib/chat/browser/media/terminalChatWidget.css | 3 ++- .../terminalContrib/chat/browser/terminalChatActions.ts | 2 ++ .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index a916f2dd0d7..1feb4831c23 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -13,7 +13,8 @@ .terminal-inline-chat .terminal-inline-chat-response { border: 1px solid var(--vscode-input-border, transparent); - background-color: var(--vscode-interactive-result-editor-background-color); + /* TODO: Make themeable */ + background-color: #181818; } .terminal-inline-chat .terminal-inline-chat-response:has(.monaco-editor.focused) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 901f536f0bf..50c1587b124 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -35,6 +35,8 @@ registerActiveXtermAction({ } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); contr?.chatWidget?.reveal(); + // TODO: Remove this before merging to main + contr?.chatWidget?.setValue('list files'); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 53a67093ffb..637e2f77104 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -122,7 +122,7 @@ export class TerminalChatWidget extends Disposable { scrollbar: { useShadows: false, vertical: 'hidden', - horizontal: 'auto', + horizontal: 'hidden', alwaysConsumeMouseWheel: false }, lineDecorationsWidth: 0, From 64e1a39e1bcf91e38860ead296912c06976f8f60 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:23:34 -0800 Subject: [PATCH 076/228] Remove category prefix from commands --- .../chat/browser/terminalChatActions.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 50c1587b124..19d9fdf140c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -18,7 +18,7 @@ import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/cha registerActiveXtermAction({ id: TerminalChatCommandId.Start, - title: localize2('startChat', 'Terminal: Start Chat'), + title: localize2('startChat', 'Start Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), @@ -42,7 +42,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.Hide, - title: localize2('closeChat', 'Terminal: Close Chat'), + title: localize2('closeChat', 'Close Chat'), keybinding: { primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], @@ -74,7 +74,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.RunCommand, - title: localize2('runCommand', 'Terminal: Run Chat Command'), + title: localize2('runCommand', 'Run Chat Command'), shortTitle: localize2('run', 'Run'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -106,7 +106,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.InsertCommand, - title: localize2('insertCommand', 'Terminal: Insert Chat Command'), + title: localize2('insertCommand', 'Insert Chat Command'), shortTitle: localize2('insert', 'Insert'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), @@ -138,7 +138,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.ViewInChat, - title: localize2('viewInChat', 'Terminal: View in Chat'), + title: localize2('viewInChat', 'View in Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -164,7 +164,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.MakeRequest, - title: localize2('makeChatRequest', 'Terminal: Make Chat Request'), + title: localize2('makeChatRequest', 'Make Chat Request'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -195,7 +195,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.Cancel, - title: localize2('cancelChat', 'Terminal: Cancel Chat'), + title: localize2('cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatRequestActive, From 85aaa473fbf47de6762d990d6982c42e53886340 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:25:54 -0800 Subject: [PATCH 077/228] Hide command border before it's shown --- .../chat/browser/terminalChatWidget.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 637e2f77104..bc7442b5f48 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -31,7 +31,7 @@ export class TerminalChatWidget extends Disposable { private readonly _inlineChatWidget: InlineChatWidget; public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } - private readonly _terminalCommandWidgetContainer: HTMLElement; + private _terminalCommandWidgetContainer: HTMLElement | undefined; private _terminalCommandWidget: CodeEditorWidget | undefined; private readonly _focusTracker: IFocusTracker; @@ -60,10 +60,6 @@ export class TerminalChatWidget extends Disposable { this._container.classList.add('terminal-inline-chat'); terminalElement.appendChild(this._container); - this._terminalCommandWidgetContainer = document.createElement('div'); - this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); - this._container.prepend(this._terminalCommandWidgetContainer); - // The inline chat widget requires a parent editor that it bases the diff view on, since the // terminal doesn't use that feature we can just pass in an unattached editor instance. const fakeParentEditorElement = document.createElement('div'); @@ -106,6 +102,9 @@ export class TerminalChatWidget extends Disposable { this._chatAccessibilityService.acceptResponse(command, requestId); this.showTerminalCommandWidget(); if (!this._terminalCommandWidget) { + this._terminalCommandWidgetContainer = document.createElement('div'); + this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); + this._container.prepend(this._terminalCommandWidgetContainer); this._terminalCommandWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._terminalCommandWidgetContainer, { readOnly: false, ariaLabel: this._getAriaLabel(), @@ -248,9 +247,9 @@ export class TerminalChatWidget extends Disposable { return this._focusTracker; } hideTerminalCommandWidget(): void { - this._terminalCommandWidgetContainer.classList.add('hide'); + this._terminalCommandWidgetContainer?.classList.add('hide'); } showTerminalCommandWidget(): void { - this._terminalCommandWidgetContainer.classList.remove('hide'); + this._terminalCommandWidgetContainer?.classList.remove('hide'); } } From 45e54c6f405e759e91e736a35ae8a58b1209cceb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:28:54 -0800 Subject: [PATCH 078/228] Wider widget, resize suggestion based on content height --- .../chat/browser/terminalChatWidget.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index bc7442b5f48..d65b9a0ffc3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -105,7 +105,7 @@ export class TerminalChatWidget extends Disposable { this._terminalCommandWidgetContainer = document.createElement('div'); this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); this._container.prepend(this._terminalCommandWidgetContainer); - this._terminalCommandWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._terminalCommandWidgetContainer, { + const widget = this._terminalCommandWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._terminalCommandWidgetContainer, { readOnly: false, ariaLabel: this._getAriaLabel(), fontSize: 13, @@ -146,14 +146,18 @@ export class TerminalChatWidget extends Disposable { }, { isSimpleWidget: true })); this._register(this._terminalCommandWidget.onDidFocusEditorText(() => this._responseEditorFocusedContextKey.set(true))); this._register(this._terminalCommandWidget.onDidBlurEditorText(() => this._responseEditorFocusedContextKey.set(false))); + this._register(this._terminalCommandWidget.onDidChangeModelContent(e => { + const height = widget.getContentHeight(); + widget.layout(new Dimension(640, height)); + })); this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: command })).then((model) => { if (!model || !this._terminalCommandWidget) { return; } - this._terminalCommandWidget.layout(new Dimension(400, 0)); + this._terminalCommandWidget.layout(new Dimension(640, 0)); this._terminalCommandWidget.setModel(model); const height = this._terminalCommandWidget.getContentHeight(); - this._terminalCommandWidget.layout(new Dimension(400, height)); + this._terminalCommandWidget.layout(new Dimension(640, height)); }); } else { this._terminalCommandWidget.setValue(command); @@ -194,7 +198,7 @@ export class TerminalChatWidget extends Disposable { return this._modelService.createModel(resource.fragment, null, resource, false); } reveal(): void { - this._inlineChatWidget.layout(new Dimension(400, 150)); + this._inlineChatWidget.layout(new Dimension(640, 150)); this._container.classList.remove('hide'); this._focusedContextKey.set(true); this._visibleContextKey.set(true); From 0dadd8f5402a6e2b21c696d16750c72b34f03ec3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:29:53 -0800 Subject: [PATCH 079/228] Trim command before accepting --- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index d65b9a0ffc3..c049e71d013 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -237,7 +237,8 @@ export class TerminalChatWidget extends Disposable { } } acceptCommand(shouldExecute: boolean): void { - const value = this._terminalCommandWidget?.getValue(); + // Trim command to remove any whitespace, otherwise this may execute the command + const value = this._terminalCommandWidget?.getValue().trim(); if (!value) { return; } From 69592b6d498fa3dab8c4c7a1710843f7c12aae37 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:33:37 -0800 Subject: [PATCH 080/228] Standardize on workbench contrib weight --- .../chat/browser/terminalChatActions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 19d9fdf140c..3ad13dbb898 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -22,7 +22,7 @@ registerActiveXtermAction({ keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), - weight: KeybindingWeight.WorkbenchContrib + weight: KeybindingWeight.WorkbenchContrib, }, f1: true, precondition: ContextKeyExpr.and( @@ -47,7 +47,7 @@ registerActiveXtermAction({ primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.and(TerminalContextKeys.chatFocused, TerminalContextKeys.chatVisible), - weight: KeybindingWeight.WorkbenchContrib + weight: KeybindingWeight.WorkbenchContrib, }, icon: Codicon.close, menu: { @@ -86,7 +86,7 @@ registerActiveXtermAction({ icon: Codicon.check, keybinding: { when: TerminalContextKeys.chatRequestActive.negate(), - weight: KeybindingWeight.WorkbenchContrib + 7, + weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.Enter, }, menu: { @@ -118,7 +118,7 @@ registerActiveXtermAction({ icon: Codicon.check, keybinding: { when: TerminalContextKeys.chatRequestActive.negate(), - weight: KeybindingWeight.WorkbenchContrib + 7, + weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Alt | KeyCode.Enter, }, menu: { @@ -175,7 +175,7 @@ registerActiveXtermAction({ icon: Codicon.send, keybinding: { when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalContextKeys.chatRequestActive.negate()), - weight: KeybindingWeight.EditorCore + 7, + weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Enter }, menu: { From 06afd593735a230025ed5b9b5010574ef5748d22 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:36:50 -0800 Subject: [PATCH 081/228] Await model (fixes lang mode) --- .../chat/browser/terminalChatWidget.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index c049e71d013..91bec4acda0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -98,9 +98,10 @@ export class TerminalChatWidget extends Disposable { return localize('terminalChatInput', "Terminal Chat Input"); } - renderTerminalCommand(command: string, requestId: number, shellType?: string): void { + async renderTerminalCommand(command: string, requestId: number, shellType?: string): Promise { this._chatAccessibilityService.acceptResponse(command, requestId); this.showTerminalCommandWidget(); + let model: ITextModel | null | void = null; if (!this._terminalCommandWidget) { this._terminalCommandWidgetContainer = document.createElement('div'); this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); @@ -150,7 +151,7 @@ export class TerminalChatWidget extends Disposable { const height = widget.getContentHeight(); widget.layout(new Dimension(640, height)); })); - this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: command })).then((model) => { + model = await this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: command })).then((model) => { if (!model || !this._terminalCommandWidget) { return; } @@ -162,9 +163,13 @@ export class TerminalChatWidget extends Disposable { } else { this._terminalCommandWidget.setValue(command); } - const languageId = this._getLanguageFromShell(shellType); - console.log('languageId', languageId); - this._terminalCommandWidget.getModel()?.setLanguage(languageId); + if (!model) { + model = this._terminalCommandWidget.getModel(); + } + if (model) { + const languageId = this._getLanguageFromShell(shellType); + model.setLanguage(languageId); + } } private _getLanguageFromShell(shell?: string): string { From b269a4b6a536614f5f6357fa686572e2bc8aa1b3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:44:46 -0800 Subject: [PATCH 082/228] Improve handling of wrapping in response editor --- .../chat/browser/terminalChatWidget.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 91bec4acda0..32e1e685ba8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -144,21 +145,22 @@ export class TerminalChatWidget extends Disposable { showWords: true, showStatusBar: false, }, + wordWrap: 'on' }, { isSimpleWidget: true })); this._register(this._terminalCommandWidget.onDidFocusEditorText(() => this._responseEditorFocusedContextKey.set(true))); this._register(this._terminalCommandWidget.onDidBlurEditorText(() => this._responseEditorFocusedContextKey.set(false))); - this._register(this._terminalCommandWidget.onDidChangeModelContent(e => { + this._register(Event.any(this._terminalCommandWidget.onDidChangeModelContent, this._terminalCommandWidget.onDidChangeModelDecorations)(() => { const height = widget.getContentHeight(); widget.layout(new Dimension(640, height)); })); model = await this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: command })).then((model) => { - if (!model || !this._terminalCommandWidget) { + if (!model) { return; } - this._terminalCommandWidget.layout(new Dimension(640, 0)); - this._terminalCommandWidget.setModel(model); - const height = this._terminalCommandWidget.getContentHeight(); - this._terminalCommandWidget.layout(new Dimension(640, height)); + widget.layout(new Dimension(640, 0)); + widget.setModel(model); + const height = widget.getContentHeight(); + widget.layout(new Dimension(640, height)); }); } else { this._terminalCommandWidget.setValue(command); From aa5626dc23146a651ae078db2860e436d9e9104a Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 13:57:34 -0600 Subject: [PATCH 083/228] fix issue with cancel --- .../terminalContrib/chat/browser/terminalChatController.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 2585ae766da..8f9c3defee0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -139,6 +139,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (this._currentRequest) { this._model?.cancelRequest(this._currentRequest); } + this._requestActiveContextKey.set(false); + this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(false); + this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(''); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); } private _forcedPlaceholder: string | undefined = undefined; From a1ac397ba2b9c4404836b3606b5b52bc580a880f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:06:24 -0800 Subject: [PATCH 084/228] Pull response editor into separate class --- .../chat/browser/terminalChatWidget.ts | 256 ++++++++++-------- 1 file changed, 145 insertions(+), 111 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 32e1e685ba8..014f746c786 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -6,7 +6,7 @@ import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/terminalChatWidget'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -33,29 +33,24 @@ export class TerminalChatWidget extends Disposable { public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } private _terminalCommandWidgetContainer: HTMLElement | undefined; - private _terminalCommandWidget: CodeEditorWidget | undefined; + private _responseEditor: TerminalChatResponseEditor | undefined; private readonly _focusTracker: IFocusTracker; private readonly _focusedContextKey: IContextKey; private readonly _visibleContextKey: IContextKey; - private readonly _responseEditorFocusedContextKey!: IContextKey; constructor( terminalElement: HTMLElement, private readonly _instance: ITerminalInstance, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, - @ILanguageService private readonly _languageService: ILanguageService, - @IModelService private readonly _modelService: IModelService ) { super(); this._focusedContextKey = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); this._visibleContextKey = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); - this._responseEditorFocusedContextKey = TerminalContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); this._container = document.createElement('div'); this._container.classList.add('terminal-inline-chat'); @@ -90,105 +85,13 @@ export class TerminalChatWidget extends Disposable { this._focusTracker = this._register(trackFocus(this._container)); } - - private _getAriaLabel(): string { - const verbose = this._configurationService.getValue(AccessibilityVerbositySettingId.Chat); - if (verbose) { - // TODO: Add verbose description - } - return localize('terminalChatInput', "Terminal Chat Input"); - } - async renderTerminalCommand(command: string, requestId: number, shellType?: string): Promise { this._chatAccessibilityService.acceptResponse(command, requestId); this.showTerminalCommandWidget(); - let model: ITextModel | null | void = null; - if (!this._terminalCommandWidget) { - this._terminalCommandWidgetContainer = document.createElement('div'); - this._terminalCommandWidgetContainer.classList.add('terminal-inline-chat-response'); - this._container.prepend(this._terminalCommandWidgetContainer); - const widget = this._terminalCommandWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._terminalCommandWidgetContainer, { - readOnly: false, - ariaLabel: this._getAriaLabel(), - fontSize: 13, - lineHeight: 20, - padding: { top: 8, bottom: 8 }, - overviewRulerLanes: 0, - glyphMargin: false, - lineNumbers: 'off', - folding: false, - hideCursorInOverviewRuler: true, - selectOnLineNumbers: false, - selectionHighlight: false, - scrollbar: { - useShadows: false, - vertical: 'hidden', - horizontal: 'hidden', - alwaysConsumeMouseWheel: false - }, - lineDecorationsWidth: 0, - overviewRulerBorder: false, - scrollBeyondLastLine: false, - renderLineHighlight: 'none', - fixedOverflowWidgets: true, - dragAndDrop: false, - revealHorizontalRightPadding: 5, - minimap: { enabled: false }, - guides: { indentation: false }, - rulers: [], - renderWhitespace: 'none', - dropIntoEditor: { enabled: true }, - quickSuggestions: false, - suggest: { - showIcons: false, - showSnippets: false, - showWords: true, - showStatusBar: false, - }, - wordWrap: 'on' - }, { isSimpleWidget: true })); - this._register(this._terminalCommandWidget.onDidFocusEditorText(() => this._responseEditorFocusedContextKey.set(true))); - this._register(this._terminalCommandWidget.onDidBlurEditorText(() => this._responseEditorFocusedContextKey.set(false))); - this._register(Event.any(this._terminalCommandWidget.onDidChangeModelContent, this._terminalCommandWidget.onDidChangeModelDecorations)(() => { - const height = widget.getContentHeight(); - widget.layout(new Dimension(640, height)); - })); - model = await this._getTextModel(URI.from({ path: `terminal-inline-chat-${this._instance.instanceId}`, scheme: 'terminal-inline-chat', fragment: command })).then((model) => { - if (!model) { - return; - } - widget.layout(new Dimension(640, 0)); - widget.setModel(model); - const height = widget.getContentHeight(); - widget.layout(new Dimension(640, height)); - }); - } else { - this._terminalCommandWidget.setValue(command); - } - if (!model) { - model = this._terminalCommandWidget.getModel(); - } - if (model) { - const languageId = this._getLanguageFromShell(shellType); - model.setLanguage(languageId); - } - } - - private _getLanguageFromShell(shell?: string): string { - switch (shell) { - case 'fish': - return this._languageService.isRegisteredLanguageId('fish') ? 'fish' : 'shellscript'; - case 'zsh': - return this._languageService.isRegisteredLanguageId('zsh') ? 'zsh' : 'shellscript'; - case 'bash': - return this._languageService.isRegisteredLanguageId('bash') ? 'bash' : 'shellscript'; - case 'sh': - return 'shellscript'; - case 'pwsh': - return 'powershell'; - default: - return 'plaintext'; + if (!this._responseEditor) { + this._responseEditor = this._instantiationService.createInstance(TerminalChatResponseEditor, command, shellType, this._container, this._instance); } + this._responseEditor.setValue(command); } renderMessage(message: string, accessibilityRequestId: number, requestId: string): void { @@ -197,13 +100,6 @@ export class TerminalChatWidget extends Disposable { this._chatAccessibilityService.acceptResponse(message, accessibilityRequestId); } - private async _getTextModel(resource: URI): Promise { - const existing = this._modelService.getModel(resource); - if (existing && !existing.isDisposed()) { - return existing; - } - return this._modelService.createModel(resource.fragment, null, resource, false); - } reveal(): void { this._inlineChatWidget.layout(new Dimension(640, 150)); this._container.classList.remove('hide'); @@ -215,7 +111,8 @@ export class TerminalChatWidget extends Disposable { this.hideTerminalCommandWidget(); this._container.classList.add('hide'); this._inlineChatWidget.value = ''; - this._terminalCommandWidget?.setValue(''); + this._responseEditor?.dispose(); + this._responseEditor = undefined; this._inlineChatWidget.updateChatMessage(undefined); this._inlineChatWidget.updateFollowUps(undefined); this._inlineChatWidget.updateProgress(false); @@ -245,7 +142,7 @@ export class TerminalChatWidget extends Disposable { } acceptCommand(shouldExecute: boolean): void { // Trim command to remove any whitespace, otherwise this may execute the command - const value = this._terminalCommandWidget?.getValue().trim(); + const value = this._responseEditor?.getValue().trim(); if (!value) { return; } @@ -265,3 +162,140 @@ export class TerminalChatWidget extends Disposable { this._terminalCommandWidgetContainer?.classList.remove('hide'); } } + +class TerminalChatResponseEditor extends Disposable { + private readonly _editorContainer: HTMLElement; + private readonly _editor: CodeEditorWidget; + + private readonly _responseEditorFocusedContextKey: IContextKey; + + readonly model: Promise; + + constructor( + initialCommandResponse: string, + shellType: string | undefined, + private readonly _container: HTMLElement, + private readonly _instance: ITerminalInstance, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILanguageService private readonly _languageService: ILanguageService, + @IModelService private readonly _modelService: IModelService, + ) { + super(); + + this._responseEditorFocusedContextKey = TerminalContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); + + this._editorContainer = document.createElement('div'); + this._editorContainer.classList.add('terminal-inline-chat-response'); + this._container.prepend(this._editorContainer); + this._register(toDisposable(() => this._editorContainer.remove())); + const editor = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._editorContainer, { + readOnly: false, + ariaLabel: this._getAriaLabel(), + fontSize: 13, + lineHeight: 20, + padding: { top: 8, bottom: 8 }, + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + hideCursorInOverviewRuler: true, + selectOnLineNumbers: false, + selectionHighlight: false, + scrollbar: { + useShadows: false, + vertical: 'hidden', + horizontal: 'hidden', + alwaysConsumeMouseWheel: false + }, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + dragAndDrop: false, + revealHorizontalRightPadding: 5, + minimap: { enabled: false }, + guides: { indentation: false }, + rulers: [], + renderWhitespace: 'none', + dropIntoEditor: { enabled: true }, + quickSuggestions: false, + suggest: { + showIcons: false, + showSnippets: false, + showWords: true, + showStatusBar: false, + }, + wordWrap: 'on' + }, { isSimpleWidget: true })); + this._editor = editor; + this._register(editor.onDidFocusEditorText(() => this._responseEditorFocusedContextKey.set(true))); + this._register(editor.onDidBlurEditorText(() => this._responseEditorFocusedContextKey.set(false))); + this._register(Event.any(editor.onDidChangeModelContent, editor.onDidChangeModelDecorations)(() => { + const height = editor.getContentHeight(); + editor.layout(new Dimension(640, height)); + })); + + this.model = this._getTextModel(URI.from({ + path: `terminal-inline-chat-${this._instance.instanceId}`, + scheme: 'terminal-inline-chat', + fragment: initialCommandResponse + })); + this.model.then(model => { + if (model) { + // Initial layout + editor.layout(new Dimension(640, 0)); + editor.setModel(model); + const height = editor.getContentHeight(); + editor.layout(new Dimension(640, height)); + + // Initialize language + const languageId = this._getLanguageFromShell(shellType); + model.setLanguage(languageId); + } + }); + } + + private _getAriaLabel(): string { + const verbose = this._configurationService.getValue(AccessibilityVerbositySettingId.Chat); + if (verbose) { + // TODO: Add verbose description + } + return localize('terminalChatInput', "Terminal Chat Input"); + } + + private async _getTextModel(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing && !existing.isDisposed()) { + return existing; + } + return this._modelService.createModel(resource.fragment, null, resource, false); + } + + private _getLanguageFromShell(shell?: string): string { + switch (shell) { + case 'fish': + return this._languageService.isRegisteredLanguageId('fish') ? 'fish' : 'shellscript'; + case 'zsh': + return this._languageService.isRegisteredLanguageId('zsh') ? 'zsh' : 'shellscript'; + case 'bash': + return this._languageService.isRegisteredLanguageId('bash') ? 'bash' : 'shellscript'; + case 'sh': + return 'shellscript'; + case 'pwsh': + return 'powershell'; + default: + return 'plaintext'; + } + } + + setValue(value: string) { + this._editor.setValue(value); + } + + getValue(): string { + return this._editor.getValue(); + } +} From 98e01f9016d920127d70b468731b75e5ae65dfb4 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 14:07:54 -0600 Subject: [PATCH 085/228] rm unused --- .../terminalContrib/chat/browser/terminalChatWidget.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 014f746c786..8448d7e5c61 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -121,10 +121,6 @@ export class TerminalChatWidget extends Disposable { this._visibleContextKey.set(false); this._instance.focus(); } - cancel(): void { - // TODO: Impl - this._inlineChatWidget.value = ''; - } focus(): void { this._inlineChatWidget.focus(); } From 204e5ba8295196b5305ae15747ddfbb7159405e1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:08:26 -0800 Subject: [PATCH 086/228] Register chat widget against contr --- .../chat/browser/terminalChatController.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 8f9c3defee0..e609ce9067f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -116,17 +116,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr } this._chatWidget = new Lazy(() => { - const chatWidget = this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance); - chatWidget.focusTracker.onDidFocus(() => { + const chatWidget = this._register(this._instantiationService.createInstance(TerminalChatWidget, this._instance.domElement!, this._instance)); + this._register(chatWidget.focusTracker.onDidFocus(() => { TerminalChatController.activeChatWidget = this; if (!isDetachedTerminalInstance(this._instance)) { this._terminalService.setActiveInstance(this._instance); } - }); - chatWidget.focusTracker.onDidBlur(() => { + })); + this._register(chatWidget.focusTracker.onDidBlur(() => { TerminalChatController.activeChatWidget = undefined; this._instance.resetScrollbarVisibility(); - }); + })); if (!this._instance.domElement) { throw new Error('FindWidget expected terminal DOM to be initialized'); } From 2453accbe856fa1a9266459fda3d84572b1fdd89 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:09:49 -0800 Subject: [PATCH 087/228] Pull placeholder from model --- .../terminalContrib/chat/browser/terminalChatController.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index e609ce9067f..4c0ef9145d8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -155,9 +155,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } private _getPlaceholderText(): string { - return this._forcedPlaceholder ?? ''; - // TODO: Pass through session placeholder - // return this._forcedPlaceholder ?? this._session?.session.placeholder ?? ''; + return this._forcedPlaceholder ?? this._model?.inputPlaceholder ?? ''; } setPlaceholder(text: string): void { From 342570da6a7973b7030b00ae45ecb524944bb64a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:14:59 -0800 Subject: [PATCH 088/228] Bring back initial placeholder and info on hide --- .../terminalContrib/chat/browser/terminalChatWidget.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 8448d7e5c61..a4bf3b55990 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -78,13 +78,17 @@ export class TerminalChatWidget extends Disposable { feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK } ); - this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); - this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); + this._reset(); this._container.appendChild(this._inlineChatWidget.domNode); this._focusTracker = this._register(trackFocus(this._container)); } + private _reset() { + this._inlineChatWidget.placeholder = localize('default.placeholder', "Ask how to do something in the terminal"); + this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); + } + async renderTerminalCommand(command: string, requestId: number, shellType?: string): Promise { this._chatAccessibilityService.acceptResponse(command, requestId); this.showTerminalCommandWidget(); @@ -110,7 +114,7 @@ export class TerminalChatWidget extends Disposable { hide(): void { this.hideTerminalCommandWidget(); this._container.classList.add('hide'); - this._inlineChatWidget.value = ''; + this._reset(); this._responseEditor?.dispose(); this._responseEditor = undefined; this._inlineChatWidget.updateChatMessage(undefined); From 4de80e1c389db99170b22f785587c7b93bcb5f3f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:19:11 -0800 Subject: [PATCH 089/228] Handle hide/show in editor class --- .../chat/browser/terminalChatWidget.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index a4bf3b55990..499d7e375da 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Dimension, IFocusTracker, trackFocus } from 'vs/base/browser/dom'; +import { Dimension, IFocusTracker, hide, show, trackFocus } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -32,7 +32,6 @@ export class TerminalChatWidget extends Disposable { private readonly _inlineChatWidget: InlineChatWidget; public get inlineChatWidget(): InlineChatWidget { return this._inlineChatWidget; } - private _terminalCommandWidgetContainer: HTMLElement | undefined; private _responseEditor: TerminalChatResponseEditor | undefined; private readonly _focusTracker: IFocusTracker; @@ -91,7 +90,7 @@ export class TerminalChatWidget extends Disposable { async renderTerminalCommand(command: string, requestId: number, shellType?: string): Promise { this._chatAccessibilityService.acceptResponse(command, requestId); - this.showTerminalCommandWidget(); + this._responseEditor?.show(); if (!this._responseEditor) { this._responseEditor = this._instantiationService.createInstance(TerminalChatResponseEditor, command, shellType, this._container, this._instance); } @@ -99,7 +98,7 @@ export class TerminalChatWidget extends Disposable { } renderMessage(message: string, accessibilityRequestId: number, requestId: string): void { - this.hideTerminalCommandWidget(); + this._responseEditor?.hide(); this._inlineChatWidget.updateChatMessage({ message: new MarkdownString(message), requestId, providerId: 'terminal' }); this._chatAccessibilityService.acceptResponse(message, accessibilityRequestId); } @@ -112,7 +111,6 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.focus(); } hide(): void { - this.hideTerminalCommandWidget(); this._container.classList.add('hide'); this._reset(); this._responseEditor?.dispose(); @@ -137,7 +135,7 @@ export class TerminalChatWidget extends Disposable { setValue(value?: string) { this._inlineChatWidget.value = value ?? ''; if (!value) { - this.hideTerminalCommandWidget(); + this._responseEditor?.hide(); } } acceptCommand(shouldExecute: boolean): void { @@ -155,12 +153,6 @@ export class TerminalChatWidget extends Disposable { public get focusTracker(): IFocusTracker { return this._focusTracker; } - hideTerminalCommandWidget(): void { - this._terminalCommandWidgetContainer?.classList.add('hide'); - } - showTerminalCommandWidget(): void { - this._terminalCommandWidgetContainer?.classList.remove('hide'); - } } class TerminalChatResponseEditor extends Disposable { @@ -298,4 +290,12 @@ class TerminalChatResponseEditor extends Disposable { getValue(): string { return this._editor.getValue(); } + + hide() { + hide(this._editorContainer); + } + + show() { + show(this._editorContainer); + } } From 556c0e67c9df1257fefbf9fada21a519ee6c4897 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 14:25:55 -0600 Subject: [PATCH 090/228] await initialization --- .../terminalContrib/chat/browser/terminalChatController.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 4c0ef9145d8..c96dd7326eb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -217,8 +217,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr // TODO: ? variables: { variables: [] }, }; - // TODO: fix requester usrname, responder username - this._model?.initialize({ id: this._requestId, requesterUsername: 'userGesture', responderUsername: 'terminal' }, undefined); + await this._model?.waitForInitialization(); const request: IParsedChatRequest = { text: this._lastInput, parts: [] From 7162515cd775bae7eec4af2b04f3ba6d6574bb08 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 14:40:35 -0600 Subject: [PATCH 091/228] open in chat view --- .../terminalContrib/chat/browser/terminalChatActions.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 3ad13dbb898..1a813210025 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -144,15 +144,20 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.chatRequestActive.negate(), TerminalContextKeys.chatAgentRegistered, - CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages) ), icon: Codicon.commentDiscussion, - menu: { + menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), TerminalContextKeys.chatRequestActive.negate()), }, + { + id: MENU_TERMINAL_CHAT_WIDGET, + group: 'main', + order: 1, + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), TerminalContextKeys.chatRequestActive.negate()), + }], run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { return; From 2408cc098b7feb83e9b48054e3e59b90dfb444e2 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 14:48:23 -0600 Subject: [PATCH 092/228] if no model, focus input like quick chat does --- .../terminalContrib/chat/browser/terminalChatController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index c96dd7326eb..9c274d92b9a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -316,6 +316,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr } } widget.focusLastMessage(); + } else if (!this._model) { + widget?.focusInput(); } } From a3602c3db9bb1c8fc41a050c727fe8a5090dfbc6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:53:30 -0800 Subject: [PATCH 093/228] Remove margin between chat and response --- .../chat/browser/media/terminalChatWidget.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index 1feb4831c23..25d492a063c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -11,6 +11,10 @@ height: auto !important; } +.terminal-inline-chat .inline-chat { + margin-top: 0 !important; +} + .terminal-inline-chat .terminal-inline-chat-response { border: 1px solid var(--vscode-input-border, transparent); /* TODO: Make themeable */ @@ -28,6 +32,5 @@ } .terminal-inline-chat .terminal-inline-chat-response { - margin: 0 0 8px 0; padding-left: 8px; } From 3c0c5106b81dc4f2c169415e816b361d6d13010a Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 15:08:56 -0600 Subject: [PATCH 094/228] add chatResponseType context key for terminal --- .../terminal/common/terminalContextKey.ts | 9 +++++++++ .../chat/browser/terminalChatActions.ts | 16 ++++++++-------- .../chat/browser/terminalChatController.ts | 11 +++++------ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 8ea1469a4b1..dcca7e64e68 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -45,6 +45,12 @@ export const enum TerminalContextKeyStrings { ChatInputHasText = 'terminalChatInputHasText', ChatAgentRegistered = 'terminalChatAgentRegistered', ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', + ChatResponseType = 'terminalChatResponseType', +} + +export const enum TerminalChatResponseTypes { + Message = 'message', + TerminalCommand = 'terminalCommand' } export namespace TerminalContextKeys { @@ -183,4 +189,7 @@ export namespace TerminalContextKeys { /** Whether the chat response editor is focused */ export const chatResponseEditorFocused = new RawContextKey(TerminalContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); + + /** The type of chat response, if any */ + export const chatResponseType = new RawContextKey(TerminalContextKeyStrings.ChatResponseType, undefined, localize('chatResponseTypeContextKey', "The type of chat response, if any")); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 1a813210025..aa8f4e045d1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -9,10 +9,10 @@ import { localize2 } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalChatResponseTypes, TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; @@ -81,7 +81,7 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.chatRequestActive.negate(), TerminalContextKeys.chatAgentRegistered, - CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty) + TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) ), icon: Codicon.check, keybinding: { @@ -93,7 +93,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 0, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalContextKeys.chatRequestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -113,7 +113,7 @@ registerActiveXtermAction({ ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.chatRequestActive.negate(), TerminalContextKeys.chatAgentRegistered, - CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty) + TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) ), icon: Codicon.check, keybinding: { @@ -125,7 +125,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalContextKeys.chatRequestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -150,13 +150,13 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.Message), TerminalContextKeys.chatRequestActive.negate()), }, { id: MENU_TERMINAL_CHAT_WIDGET, group: 'main', order: 1, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Empty), TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalContextKeys.chatRequestActive.negate()), }], run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 9c274d92b9a..fd5f072846b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -19,11 +19,10 @@ import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/cont import { IChatAgentService, IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService, IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; -import { InlineChatResponseTypes, CTX_INLINE_CHAT_RESPONSE_TYPES } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; 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'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalChatResponseTypes, TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; @@ -56,7 +55,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _requestActiveContextKey!: IContextKey; private readonly _terminalAgentRegisteredContextKey!: IContextKey; - private readonly _lastResponseTypeContextKey!: IContextKey; + private readonly _responseTypeContextKey!: IContextKey; private _requestId: number = 0; private _messages = this._store.add(new Emitter()); @@ -97,7 +96,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } this._requestActiveContextKey = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); this._terminalAgentRegisteredContextKey = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); - this._lastResponseTypeContextKey = CTX_INLINE_CHAT_RESPONSE_TYPES.bindTo(this._contextKeyService); + this._responseTypeContextKey = TerminalContextKeys.chatResponseType.bindTo(this._contextKeyService); if (!this._chatAgentService.hasAgent(this._terminalAgentId)) { this._register(this._chatAgentService.onDidChangeAgents(() => { @@ -257,10 +256,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr } if (codeBlock) { this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._requestId, shellType); - this._lastResponseTypeContextKey.set(InlineChatResponseTypes.Empty); + this._responseTypeContextKey.set(TerminalChatResponseTypes.TerminalCommand); } else { this._chatWidget?.rawValue?.renderMessage(responseContent, this._requestId, requestId); - this._lastResponseTypeContextKey.set(InlineChatResponseTypes.OnlyMessages); + this._responseTypeContextKey.set(TerminalChatResponseTypes.Message); } this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); From de3d5a4c4b128f59afa06a25253c1d243dd720e4 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 15:30:14 -0600 Subject: [PATCH 095/228] rm terminal stuff from inline chat actions --- .../contrib/inlineChat/electron-sandbox/inlineChatActions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts index 57c29d41e9d..99862b01d7b 100644 --- a/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/electron-sandbox/inlineChatActions.ts @@ -20,17 +20,16 @@ import { HasSpeechProvider, ISpeechService } from 'vs/workbench/contrib/speech/c import { localize2 } from 'vs/nls'; import { Action2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; export class HoldToSpeak extends AbstractInlineChatAction { constructor() { super({ id: 'inlineChat.holdForSpeech', - precondition: ContextKeyExpr.and(HasSpeechProvider, ContextKeyExpr.or(CTX_INLINE_CHAT_VISIBLE, TerminalContextKeys.chatVisible)), + precondition: ContextKeyExpr.and(HasSpeechProvider, CTX_INLINE_CHAT_VISIBLE), title: localize2('holdForSpeech', "Hold for Speech"), keybinding: { - when: ContextKeyExpr.or(EditorContextKeys.textInputFocus, TerminalContextKeys.chatFocused), + when: EditorContextKeys.textInputFocus, weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyI, }, From a9f226d8eba2370051e88cfedf8e47f716e261f8 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 16:02:41 -0600 Subject: [PATCH 096/228] fix requestId, sessionId, wip feedback --- .../chat/browser/terminalChatActions.ts | 28 ++++++++---- .../chat/browser/terminalChatController.ts | 43 +++++++++++++------ 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index aa8f4e045d1..187d16f6d51 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -226,7 +226,7 @@ registerActiveXtermAction({ title: localize2('feedbackHelpful', 'Helpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive, + TerminalContextKeys.chatResponseType.notEqualsTo(undefined) ), // TODO: toggled: CTX_INLINE_CHAT_LAST_FEEDBACK.isEqualTo('helpful'), icon: Codicon.thumbsup, @@ -234,11 +234,14 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', order: 1, - // TODO: Fill in ctx - // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), + when: TerminalContextKeys.chatResponseType.notEqualsTo(undefined), }, run: (_xterm, _accessor, activeInstance) => { - // TODO: Impl + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(true); } }); @@ -247,7 +250,7 @@ registerActiveXtermAction({ title: localize2('feedbackUnhelpful', 'Unhelpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive, + TerminalContextKeys.chatResponseType.notEqualsTo(undefined), ), // TODO: toggled: CTX_INLINE_CHAT_LAST_FEEDBACK.isEqualTo('unhelpful'), icon: Codicon.thumbsdown, @@ -255,11 +258,14 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', order: 2, - // TODO: Fill in ctx - // when: CTX_INLINE_CHAT_LAST_RESPONSE_TYPE.notEqualsTo(undefined), + when: TerminalContextKeys.chatResponseType.notEqualsTo(undefined), }, run: (_xterm, _accessor, activeInstance) => { - // TODO: Impl + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(false); } }); @@ -284,7 +290,11 @@ registerActiveXtermAction({ order: 3 }], run: (_xterm, _accessor, activeInstance) => { - // TODO: Impl + // if (isDetachedTerminalInstance(activeInstance)) { + // return; + // } + // const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + // contr?.acceptFeedback(true); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index fd5f072846b..030e2bb8fdf 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -9,7 +9,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; -import { generateUuid } from 'vs/base/common/uuid'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -18,7 +17,7 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentService, IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatService, IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatService, IChatProgress, InteractiveSessionVoteDirection } 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'; @@ -134,6 +133,25 @@ export class TerminalChatController extends Disposable implements ITerminalContr }); } + acceptFeedback(helpful: boolean): void { + const providerId = this._chatService.getProviderInfos()?.[0]?.id; + if (!providerId || !this._currentRequest || !this._model) { + return; + } + this._chatService.notifyUserAction({ + providerId, + sessionId: this._model?.sessionId, + requestId: this._currentRequest.id, + agentId: this._terminalAgentId, + //TODO: fill in error details if any etc. + result: {}, + action: { + kind: 'vote', + direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down + }, + }); + } + cancel(): void { if (this._currentRequest) { this._model?.cancelRequest(this._currentRequest); @@ -175,6 +193,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._model = undefined; this._chatWidget?.rawValue?.hide(); this._chatWidget?.rawValue?.setValue(undefined); + this._responseTypeContextKey.reset(); } private updateModel(): void { @@ -207,15 +226,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._model?.acceptResponseProgress(this._currentRequest, progress); } }; - const requestId = generateUuid(); - const requestProps: IChatAgentRequest = { - sessionId: generateUuid(), - requestId, - agentId: this._terminalAgentId, - message: this._lastInput, - // TODO: ? - variables: { variables: [] }, - }; + await this._model?.waitForInitialization(); const request: IParsedChatRequest = { text: this._lastInput, @@ -225,6 +236,14 @@ export class TerminalChatController extends Disposable implements ITerminalContr variables: [] }; this._currentRequest = this._model?.addRequest(request, requestVarData); + const requestProps: IChatAgentRequest = { + sessionId: this._model!.sessionId, + requestId: this._currentRequest!.id, + agentId: this._terminalAgentId, + message: this._lastInput, + // TODO: ? + variables: { variables: [] }, + }; try { const task = this._chatAgentService.invokeAgent(this._terminalAgentId, requestProps, progressCallback, [], cancellationToken); this._chatWidget?.rawValue?.inlineChatWidget.updateChatMessage(undefined); @@ -258,7 +277,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._requestId, shellType); this._responseTypeContextKey.set(TerminalChatResponseTypes.TerminalCommand); } else { - this._chatWidget?.rawValue?.renderMessage(responseContent, this._requestId, requestId); + this._chatWidget?.rawValue?.renderMessage(responseContent, this._requestId, this._currentRequest!.id); this._responseTypeContextKey.set(TerminalChatResponseTypes.Message); } this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); From 4b295eda6f9de1762e44a818085d1736976bef8f Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 16:08:57 -0600 Subject: [PATCH 097/228] fix feedback action --- .../chat/browser/terminalChatController.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 030e2bb8fdf..baa3943274c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -138,18 +138,22 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!providerId || !this._currentRequest || !this._model) { return; } - this._chatService.notifyUserAction({ - providerId, - sessionId: this._model?.sessionId, - requestId: this._currentRequest.id, - agentId: this._terminalAgentId, - //TODO: fill in error details if any etc. - result: {}, - action: { - kind: 'vote', - direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down - }, - }); + // TODO:extract into helper method + for (const request of this._model.getRequests()) { + if (request.response?.response.value || request.response?.result) { + this._chatService.notifyUserAction({ + providerId, + sessionId: request.session.sessionId, + requestId: request.id, + agentId: request.response?.agent?.id, + result: request.response?.result, + action: { + kind: 'vote', + direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down + }, + }); + } + } } cancel(): void { From ed4a3ce3ec379421c165b7fb1af78c5c241dd95d Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 15 Feb 2024 16:12:04 -0600 Subject: [PATCH 098/228] fix feedback action, add styling --- .../terminalContrib/chat/browser/terminalChatController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index baa3943274c..f8e97671171 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -154,6 +154,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr }); } } + this._chatWidget?.rawValue?.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); } cancel(): void { From 559a3413bc85864e22f29df1fb7a207d5aba7590 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 08:57:13 -0600 Subject: [PATCH 099/228] add support for issue reporting when terminal agent supports that --- .../terminal/common/terminalContextKey.ts | 4 +++ .../chat/browser/terminalChatActions.ts | 36 +++++++++---------- .../chat/browser/terminalChatController.ts | 24 +++++++++---- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index dcca7e64e68..d823a5b7fd9 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -46,6 +46,7 @@ export const enum TerminalContextKeyStrings { ChatAgentRegistered = 'terminalChatAgentRegistered', ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', ChatResponseType = 'terminalChatResponseType', + ChatResponseSupportsIssueReporting = 'terminalChatResponseSupportsIssueReporting' } export const enum TerminalChatResponseTypes { @@ -192,4 +193,7 @@ export namespace TerminalContextKeys { /** The type of chat response, if any */ export const chatResponseType = new RawContextKey(TerminalContextKeyStrings.ChatResponseType, undefined, localize('chatResponseTypeContextKey', "The type of chat response, if any")); + + /** Whether the response supports issue reporting */ + export const chatResponseSupportsIssueReporting = new RawContextKey(TerminalContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 187d16f6d51..d1586f8c4ba 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -228,7 +228,6 @@ registerActiveXtermAction({ ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatResponseType.notEqualsTo(undefined) ), - // TODO: toggled: CTX_INLINE_CHAT_LAST_FEEDBACK.isEqualTo('helpful'), icon: Codicon.thumbsup, menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, @@ -252,7 +251,6 @@ registerActiveXtermAction({ ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatResponseType.notEqualsTo(undefined), ), - // TODO: toggled: CTX_INLINE_CHAT_LAST_FEEDBACK.isEqualTo('unhelpful'), icon: Codicon.thumbsdown, menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, @@ -274,27 +272,29 @@ registerActiveXtermAction({ title: localize2('reportIssue', 'Report Issue'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive, + TerminalContextKeys.chatRequestActive.negate(), + TerminalContextKeys.chatResponseType.notEqualsTo(undefined), + TerminalContextKeys.chatResponseSupportsIssueReporting ), - // TODO: precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), icon: Codicon.report, - menu: [/*{ - // TODO: Enable this + menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - when: ContextKeyExpr.and(CTX_TERMINAL_CHAT_SUPPORT_ISSUE_REPORTING, CTX_TERMINAL_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChatResponseTypes.Empty)), - group: '2_feedback', + when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.notEqualsTo(undefined), TerminalContextKeys.chatResponseSupportsIssueReporting), + group: 'inline', order: 3 - }, */{ - id: MENU_TERMINAL_CHAT_WIDGET, - group: 'config', - order: 3 - }], + }], + // { + // id: MENU_TERMINAL_CHAT_WIDGET, + // when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.notEqualsTo(undefined), TerminalContextKeys.chatResponseSupportsIssueReporting), + // group: 'config', + // order: 3 + // }], run: (_xterm, _accessor, activeInstance) => { - // if (isDetachedTerminalInstance(activeInstance)) { - // return; - // } - // const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); - // contr?.acceptFeedback(true); + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.acceptFeedback(); } }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index f8e97671171..996effa0743 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -17,7 +17,7 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentService, IChatAgentRequest } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { IChatService, IChatProgress, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatService, IChatProgress, InteractiveSessionVoteDirection, ChatUserAction } 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'; @@ -55,6 +55,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _requestActiveContextKey!: IContextKey; private readonly _terminalAgentRegisteredContextKey!: IContextKey; private readonly _responseTypeContextKey!: IContextKey; + private readonly __responseSupportsIssueReportingContextKey!: IContextKey; + private _requestId: number = 0; private _messages = this._store.add(new Emitter()); @@ -96,6 +98,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._requestActiveContextKey = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); this._terminalAgentRegisteredContextKey = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); this._responseTypeContextKey = TerminalContextKeys.chatResponseType.bindTo(this._contextKeyService); + this.__responseSupportsIssueReportingContextKey = TerminalContextKeys.chatResponseSupportsIssueReporting.bindTo(this._contextKeyService); if (!this._chatAgentService.hasAgent(this._terminalAgentId)) { this._register(this._chatAgentService.onDidChangeAgents(() => { @@ -133,11 +136,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr }); } - acceptFeedback(helpful: boolean): void { + acceptFeedback(helpful?: boolean): void { const providerId = this._chatService.getProviderInfos()?.[0]?.id; if (!providerId || !this._currentRequest || !this._model) { return; } + let action: ChatUserAction; + if (helpful === undefined) { + action = { kind: 'bug' }; + } else { + action = { kind: 'vote', direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down }; + } // TODO:extract into helper method for (const request of this._model.getRequests()) { if (request.response?.response.value || request.response?.result) { @@ -147,10 +156,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr requestId: request.id, agentId: request.response?.agent?.id, result: request.response?.result, - action: { - kind: 'vote', - direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down - }, + action }); } } @@ -267,8 +273,12 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (this._currentRequest) { this._model?.completeResponse(this._currentRequest); } + const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; + if (supportIssueReporting !== undefined) { + this.__responseSupportsIssueReportingContextKey.set(supportIssueReporting); + } + this._lastResponseContent = responseContent; } - this._lastResponseContent = responseContent; const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; const regex = /```(?\w+)\n(?[\s\S]*?)```/g; const match = regex.exec(firstCodeBlockContent); From 705d61ad975a564f0b7b30d6c3e12fefda85dfb3 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 09:02:11 -0600 Subject: [PATCH 100/228] rm todo --- .../terminalContrib/chat/browser/terminalChatController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 996effa0743..70ee41baa68 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -66,7 +66,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr private _lastInput: string | undefined; private _lastResponseContent: string | undefined; get lastResponseContent(): string | undefined { - // TODO: use model return this._lastResponseContent; } From 881321da5cfac6fe0ce6894fe40b2ba6623cba0c Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 09:22:53 -0600 Subject: [PATCH 101/228] fix aria label for terminal response editor --- .../chat/browser/terminalChatAccessibilityHelp.ts | 6 ++++-- .../terminalContrib/chat/browser/terminalChatWidget.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index d851609de14..890fb103292 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -48,7 +48,8 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor): string { const keybindingService = accessor.get(IKeybindingService); const content = []; const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel(); - const acceptCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.RunCommand)?.getAriaLabel(); + const runCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.RunCommand)?.getAriaLabel(); + const insertCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.InsertCommand)?.getAriaLabel(); const makeRequestKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.MakeRequest)?.getAriaLabel(); //TODO: using this instead of the terminal command bc by definition the inline terminal chat is focused when this dialog is invoked. const startChatKeybinding = keybindingService.lookupKeybinding('inlineChat.start')?.getAriaLabel(); @@ -58,7 +59,8 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor): string { content.push(localize('inlineChat.results', "A result may contain a terminal command or just a message. In either case, the result will be announced.")); content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponseMessage', 'If just a message comes back, it can be inspected 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.inspectTerminalCommand', 'If a terminal command comes back, it can be inspected in an editor reached via Shift+Tab.')); - content.push(acceptCommandKeybinding ? localize('inlineChat.acceptCommand', 'With focus in the command editor, the Terminal: Accept Chat Command ({0}) action.', acceptCommandKeybinding) : localize('inlineChat.acceptCommandNoKb', 'Accept a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); + content.push(runCommandKeybinding ? localize('inlineChat.runCommand', 'With focus in the input box or command editor, the Terminal: Run Chat Command ({0}) action.', runCommandKeybinding) : localize('inlineChat.runCommandNoKb', 'Run a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); + content.push(insertCommandKeybinding ? localize('inlineChat.insertCommand', 'With focus in the input box command editor, the Terminal: Insert Chat Command ({0}) action.', insertCommandKeybinding) : localize('inlineChat.insertCommandNoKb', 'Insert a command by tabbing to the button as the action is currently not triggerable by a keybinding.')); 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'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 499d7e375da..2cc70c7eb07 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -255,7 +255,7 @@ class TerminalChatResponseEditor extends Disposable { if (verbose) { // TODO: Add verbose description } - return localize('terminalChatInput', "Terminal Chat Input"); + return localize('terminalResponseEditor', "Terminal Response Editor"); } private async _getTextModel(resource: URI): Promise { From ee1cb48bb1005d7299e97f8445783a7c976f9c08 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 10:17:11 -0600 Subject: [PATCH 102/228] add discard action --- .../chat/browser/terminalChat.ts | 3 +- .../chat/browser/terminalChatActions.ts | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index d409361d517..a2349bc25c1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -7,7 +7,8 @@ import { MenuId } from 'vs/platform/actions/common/actions'; export const enum TerminalChatCommandId { Start = 'workbench.action.terminal.chat.start', - Hide = 'workbench.action.terminal.chat.close', + Close = 'workbench.action.terminal.chat.close', + Discard = 'workbench.action.terminal.chat.discard', MakeRequest = 'workbench.action.terminal.chat.makeRequest', Cancel = 'workbench.action.terminal.chat.cancel', FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful', diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index d1586f8c4ba..abf356fbdff 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -41,7 +41,7 @@ registerActiveXtermAction({ }); registerActiveXtermAction({ - id: TerminalChatCommandId.Hide, + id: TerminalChatCommandId.Close, title: localize2('closeChat', 'Close Chat'), keybinding: { primary: KeyCode.Escape, @@ -70,6 +70,32 @@ registerActiveXtermAction({ }); +registerActiveXtermAction({ + id: TerminalChatCommandId.Discard, + title: localize2('discard', 'Discard'), + icon: Codicon.discard, + menu: { + id: MENU_TERMINAL_CHAT_WIDGET_STATUS, + group: '0_main', + order: 2, + when: ContextKeyExpr.and(TerminalContextKeys.chatFocused, + TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand)) + }, + f1: true, + precondition: ContextKeyExpr.and( + ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), + ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalContextKeys.chatFocused, + TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + ), + run: (_xterm, _accessor, activeInstance) => { + if (isDetachedTerminalInstance(activeInstance)) { + return; + } + const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); + contr?.clear(); + } +}); registerActiveXtermAction({ From 29924c918fc21025c13354b62ff260678fda83db Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 10:32:42 -0600 Subject: [PATCH 103/228] add feedback context key, get styling to apply --- .../contrib/terminal/common/terminalContextKey.ts | 6 +++++- .../chat/browser/terminalChatActions.ts | 2 ++ .../chat/browser/terminalChatController.ts | 11 ++++++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index d823a5b7fd9..0c577a9df8a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -46,7 +46,8 @@ export const enum TerminalContextKeyStrings { ChatAgentRegistered = 'terminalChatAgentRegistered', ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', ChatResponseType = 'terminalChatResponseType', - ChatResponseSupportsIssueReporting = 'terminalChatResponseSupportsIssueReporting' + ChatResponseSupportsIssueReporting = 'terminalChatResponseSupportsIssueReporting', + ChatSessionResponseVote = 'terminalChatSessionResponseVote', } export const enum TerminalChatResponseTypes { @@ -196,4 +197,7 @@ export namespace TerminalContextKeys { /** Whether the response supports issue reporting */ export const chatResponseSupportsIssueReporting = new RawContextKey(TerminalContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); + + /** The chat vote, if any for the response, if any */ + export const chatSessionResponseVote = new RawContextKey(TerminalContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index abf356fbdff..cd984a4d61e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -255,6 +255,7 @@ registerActiveXtermAction({ TerminalContextKeys.chatResponseType.notEqualsTo(undefined) ), icon: Codicon.thumbsup, + toggled: TerminalContextKeys.chatSessionResponseVote.isEqualTo('up'), menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', @@ -277,6 +278,7 @@ registerActiveXtermAction({ ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), TerminalContextKeys.chatResponseType.notEqualsTo(undefined), ), + toggled: TerminalContextKeys.chatSessionResponseVote.isEqualTo('down'), icon: Codicon.thumbsdown, menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 70ee41baa68..ca3bdbab893 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -55,7 +55,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _requestActiveContextKey!: IContextKey; private readonly _terminalAgentRegisteredContextKey!: IContextKey; private readonly _responseTypeContextKey!: IContextKey; - private readonly __responseSupportsIssueReportingContextKey!: IContextKey; + private readonly _responseSupportsIssueReportingContextKey!: IContextKey; + private readonly _sessionResponseVoteContextKey!: IContextKey; private _requestId: number = 0; @@ -97,7 +98,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._requestActiveContextKey = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); this._terminalAgentRegisteredContextKey = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); this._responseTypeContextKey = TerminalContextKeys.chatResponseType.bindTo(this._contextKeyService); - this.__responseSupportsIssueReportingContextKey = TerminalContextKeys.chatResponseSupportsIssueReporting.bindTo(this._contextKeyService); + this._responseSupportsIssueReportingContextKey = TerminalContextKeys.chatResponseSupportsIssueReporting.bindTo(this._contextKeyService); + this._sessionResponseVoteContextKey = TerminalContextKeys.chatSessionResponseVote.bindTo(this._contextKeyService); if (!this._chatAgentService.hasAgent(this._terminalAgentId)) { this._register(this._chatAgentService.onDidChangeAgents(() => { @@ -144,6 +146,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (helpful === undefined) { action = { kind: 'bug' }; } else { + this._sessionResponseVoteContextKey.set(helpful ? 'up' : 'down'); action = { kind: 'vote', direction: helpful ? InteractiveSessionVoteDirection.Up : InteractiveSessionVoteDirection.Down }; } // TODO:extract into helper method @@ -204,6 +207,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.hide(); this._chatWidget?.rawValue?.setValue(undefined); this._responseTypeContextKey.reset(); + this._sessionResponseVoteContextKey.reset(); + this._requestActiveContextKey.reset(); } private updateModel(): void { @@ -274,7 +279,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; if (supportIssueReporting !== undefined) { - this.__responseSupportsIssueReportingContextKey.set(supportIssueReporting); + this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); } this._lastResponseContent = responseContent; } From ba24d929651af34cd13b52b21cf88d5521068194 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 13:07:26 -0600 Subject: [PATCH 104/228] move css to right file --- .../terminal/browser/media/terminal.css | 18 ------------------ .../chat/browser/media/terminalChatWidget.css | 12 ++++++++++++ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 1aaf1516c5e..488815cf258 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -565,21 +565,3 @@ .monaco-workbench .xterm.terminal.hide { visibility: hidden; } - -.monaco-workbench .terminal-chat-widget { - z-index: 33 !important; - position: absolute; - top: 10px; -} - -.monaco-workbench .terminal-inline-chat.hide { - visibility: hidden; -} - -.monaco-workbench .terminal-inline-chat-response.hide { - visibility: hidden; -} - -.monaco-workbench .terminal-inline-chat .chatMessageContent { - width: 400px !important; -} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index 25d492a063c..602560618cc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -15,6 +15,18 @@ margin-top: 0 !important; } +.terminal-inline-chat.hide { + visibility: hidden; +} + +.terminal-inline-chat-response.hide { + visibility: hidden; +} + +.terminal-inline-chat .chatMessageContent { + width: 400px !important; +} + .terminal-inline-chat .terminal-inline-chat-response { border: 1px solid var(--vscode-input-border, transparent); /* TODO: Make themeable */ From c57d3061b82d738f3062365127a60660af3929da Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:23:50 -0800 Subject: [PATCH 105/228] Move a11y help provider into terminal chat folder --- .../browser/accessibilityConfiguration.ts | 2 + .../inlineChat/browser/inlineChatActions.ts | 3 +- .../browser/terminal.chat.contribution.ts | 2 + ...rminalChatAccessibilityHelpContribution.ts | 48 +++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index bba5a1f648e..ca7ebdca1a0 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -45,6 +45,7 @@ export const enum AccessibilityVerbositySettingId { DiffEditor = 'accessibility.verbosity.diffEditor', Chat = 'accessibility.verbosity.panelChat', InlineChat = 'accessibility.verbosity.inlineChat', + TerminalChat = 'accessibility.verbosity.terminalChat', InlineCompletions = 'accessibility.verbosity.inlineCompletions', KeybindingsEditor = 'accessibility.verbosity.keybindingsEditor', Notebook = 'accessibility.verbosity.notebook', @@ -57,6 +58,7 @@ export const enum AccessibilityVerbositySettingId { export const enum AccessibleViewProviderId { Terminal = 'terminal', + TerminalChat = 'terminal-chat', TerminalHelp = 'terminal-help', DiffEditor = 'diffEditor', Chat = 'panelChat', diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index d9f725ed61d..8deb70a073e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -30,7 +30,6 @@ 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 { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -786,6 +785,6 @@ export class InlineAccessibilityHelpContribution extends Disposable { return; } runAccessibilityHelpAction(accessor, codeEditor, 'inlineChat'); - }, ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED), TerminalContextKeys.chatFocused.negate()))); + }, ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_FOCUSED))); } } 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 0c2dba50035..0049340fadd 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 @@ -8,6 +8,7 @@ import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/brow import { TerminalInlineChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; import { TerminalInlineChatAccessibleViewContribution } 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/terminalChatAccessibilityHelpContribution'; import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; @@ -15,3 +16,4 @@ registerTerminalContribution(TerminalChatController.ID, TerminalChatController, registerWorkbenchContribution2(TerminalInlineChatAccessibleViewContribution.ID, TerminalInlineChatAccessibleViewContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(TerminalInlineChatAccessibilityHelpContribution.ID, TerminalInlineChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(TerminalChatAccessibilityHelpContribution.ID, TerminalChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts new file mode 100644 index 00000000000..6e5f79e1cb1 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from 'vs/nls'; +import type { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +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 { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +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, TerminalContextKeys.chatFocused)); + } +} + +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 content = []; + // TODO: Fill in more help text + content.push(localize('chat.overview', 'The terminal chat view is comprised of an input box, an editor where suggested commands are provided (Shift+Tab) and buttons to action the suggestion.')); + return content.join('\n\n'); +} From ab75d8d74b5dc30c84c65581e3674ee19f3744bd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:27:24 -0800 Subject: [PATCH 106/228] Remove terminal action ids from inline button config provider --- src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts | 2 +- .../contrib/terminalContrib/chat/browser/terminalChatActions.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 537c7b8951f..3aeb25cd2e2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -388,7 +388,7 @@ export class InlineChatWidget { buttonConfigProvider: action => { if (action.id === ACTION_REGENERATE_RESPONSE) { return { showIcon: true, showLabel: false, isSecondary: true }; - } else if ([ACTION_VIEW_IN_CHAT, ACTION_ACCEPT_CHANGES, 'workbench.action.terminal.chat.acceptCommand', 'workbench.action.terminal.chat.viewInChat'].includes(action.id)) { + } else if ([ACTION_VIEW_IN_CHAT, ACTION_ACCEPT_CHANGES].includes(action.id)) { return { isSecondary: false }; } else { return { isSecondary: true }; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index cd984a4d61e..f374aee77e8 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -116,6 +116,7 @@ registerActiveXtermAction({ primary: KeyMod.CtrlCmd | KeyCode.Enter, }, menu: { + // TODO: Allow action to be made primary, the action list is hardcoded within InlineChatWidget id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 0, From ae04cff1450064534d4490d7b21fa138fb06e03e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:31:56 -0800 Subject: [PATCH 107/228] Move context keys and response types into terminalContrib --- .../terminal/common/terminalContextKey.ts | 42 ---------- .../chat/browser/terminalChat.ts | 49 ++++++++++++ .../browser/terminalChatAccessibilityHelp.ts | 5 +- ...rminalChatAccessibilityHelpContribution.ts | 4 +- .../browser/terminalChatAccessibleView.ts | 4 +- .../chat/browser/terminalChatActions.ts | 80 +++++++++---------- .../chat/browser/terminalChatController.ts | 12 +-- .../chat/browser/terminalChatWidget.ts | 9 +-- 8 files changed, 105 insertions(+), 100 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index 0c577a9df8a..72335480aee 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -39,20 +39,6 @@ export const enum TerminalContextKeyStrings { ShellType = 'terminalShellType', InTerminalRunCommandPicker = 'inTerminalRunCommandPicker', TerminalShellIntegrationEnabled = 'terminalShellIntegrationEnabled', - ChatFocus = 'terminalChatFocus', - ChatVisible = 'terminalChatVisible', - ChatActiveRequest = 'terminalChatActiveRequest', - ChatInputHasText = 'terminalChatInputHasText', - ChatAgentRegistered = 'terminalChatAgentRegistered', - ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', - ChatResponseType = 'terminalChatResponseType', - ChatResponseSupportsIssueReporting = 'terminalChatResponseSupportsIssueReporting', - ChatSessionResponseVote = 'terminalChatSessionResponseVote', -} - -export const enum TerminalChatResponseTypes { - Message = 'message', - TerminalCommand = 'terminalCommand' } export namespace TerminalContextKeys { @@ -172,32 +158,4 @@ export namespace TerminalContextKeys { ContextKeyExpr.equals(`config.${TerminalSettingId.TabsShowActions}`, 'always') ) ); - - - /** Whether the chat widget is focused */ - export const chatFocused = new RawContextKey(TerminalContextKeyStrings.ChatFocus, false, localize('chatFocusedContextKey', "Whether the chat view is focused.")); - - /** Whether the chat widget is visible */ - export const chatVisible = new RawContextKey(TerminalContextKeyStrings.ChatVisible, false, localize('chatVisibleContextKey', "Whether the chat view is visible.")); - - /** Whether there is an active chat request */ - export const chatRequestActive = new RawContextKey(TerminalContextKeyStrings.ChatActiveRequest, false, localize('chatRequestActiveContextKey', "Whether there is an active chat request.")); - - /** Whether the chat input has text */ - export const chatInputHasText = new RawContextKey(TerminalContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); - - /** Whether the terminal chat agent has been registered */ - export const chatAgentRegistered = new RawContextKey(TerminalContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); - - /** Whether the chat response editor is focused */ - export const chatResponseEditorFocused = new RawContextKey(TerminalContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); - - /** The type of chat response, if any */ - export const chatResponseType = new RawContextKey(TerminalContextKeyStrings.ChatResponseType, undefined, localize('chatResponseTypeContextKey', "The type of chat response, if any")); - - /** Whether the response supports issue reporting */ - export const chatResponseSupportsIssueReporting = new RawContextKey(TerminalContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); - - /** The chat vote, if any for the response, if any */ - export const chatSessionResponseVote = new RawContextKey(TerminalContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index a2349bc25c1..5aefe757469 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const enum TerminalChatCommandId { Start = 'workbench.action.terminal.chat.start', @@ -24,3 +26,50 @@ export const MENU_TERMINAL_CHAT_WIDGET = MenuId.for('terminalChatWidget'); export const MENU_TERMINAL_CHAT_WIDGET_STATUS = MenuId.for('terminalChatWidget.status'); export const MENU_TERMINAL_CHAT_WIDGET_FEEDBACK = MenuId.for('terminalChatWidget.feedback'); export const MENU_TERMINAL_CHAT_WIDGET_TOOLBAR = MenuId.for('terminalChatWidget.toolbar'); + +export const enum TerminalChatContextKeyStrings { + ChatFocus = 'terminalChatFocus', + ChatVisible = 'terminalChatVisible', + ChatActiveRequest = 'terminalChatActiveRequest', + ChatInputHasText = 'terminalChatInputHasText', + ChatAgentRegistered = 'terminalChatAgentRegistered', + ChatResponseEditorFocused = 'terminalChatResponseEditorFocused', + ChatResponseType = 'terminalChatResponseType', + ChatResponseSupportsIssueReporting = 'terminalChatResponseSupportsIssueReporting', + ChatSessionResponseVote = 'terminalChatSessionResponseVote', +} + +export const enum TerminalChatResponseTypes { + Message = 'message', + TerminalCommand = 'terminalCommand' +} + +export namespace TerminalChatContextKeys { + + /** Whether the chat widget is focused */ + export const chatFocused = new RawContextKey(TerminalChatContextKeyStrings.ChatFocus, false, localize('chatFocusedContextKey', "Whether the chat view is focused.")); + + /** Whether the chat widget is visible */ + export const chatVisible = new RawContextKey(TerminalChatContextKeyStrings.ChatVisible, false, localize('chatVisibleContextKey', "Whether the chat view is visible.")); + + /** Whether there is an active chat request */ + export const chatRequestActive = new RawContextKey(TerminalChatContextKeyStrings.ChatActiveRequest, false, localize('chatRequestActiveContextKey', "Whether there is an active chat request.")); + + /** Whether the chat input has text */ + export const chatInputHasText = new RawContextKey(TerminalChatContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); + + /** Whether the terminal chat agent has been registered */ + export const chatAgentRegistered = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); + + /** Whether the chat response editor is focused */ + export const chatResponseEditorFocused = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); + + /** The type of chat response, if any */ + export const chatResponseType = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseType, undefined, localize('chatResponseTypeContextKey', "The type of chat response, if any")); + + /** Whether the response supports issue reporting */ + export const chatResponseSupportsIssueReporting = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); + + /** The chat vote, if any for the response, if any */ + export const chatSessionResponseVote = new RawContextKey(TerminalChatContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index 890fb103292..c9631f7fa1d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -13,8 +13,7 @@ import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; export class TerminalInlineChatAccessibilityHelpContribution extends Disposable { @@ -39,7 +38,7 @@ export class TerminalInlineChatAccessibilityHelpContribution extends Disposable options: { type: AccessibleViewType.Help } }); return true; - }, ContextKeyExpr.or(TerminalContextKeys.chatFocused, TerminalContextKeys.chatResponseEditorFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + }, ContextKeyExpr.or(TerminalChatContextKeys.chatFocused, TerminalChatContextKeys.chatResponseEditorFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts index 6e5f79e1cb1..afc24a15163 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts @@ -10,14 +10,14 @@ import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/wo import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { 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, TerminalContextKeys.chatFocused)); + this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.chatFocused)); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index 8c146698482..93854689479 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -10,7 +10,7 @@ import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; export class TerminalInlineChatAccessibleViewContribution extends Disposable { @@ -35,6 +35,6 @@ export class TerminalInlineChatAccessibleViewContribution extends Disposable { options: { type: AccessibleViewType.View } }); return true; - }, ContextKeyExpr.and(TerminalContextKeys.chatFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + }, ContextKeyExpr.and(TerminalChatContextKeys.chatFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index f374aee77e8..7f8dc50524d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -12,8 +12,8 @@ import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; -import { TerminalChatResponseTypes, TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys, TerminalChatResponseTypes } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; registerActiveXtermAction({ @@ -21,7 +21,7 @@ registerActiveXtermAction({ title: localize2('startChat', 'Start Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, - when: ContextKeyExpr.and(TerminalContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), + when: ContextKeyExpr.and(TerminalChatContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), weight: KeybindingWeight.WorkbenchContrib, }, f1: true, @@ -46,7 +46,7 @@ registerActiveXtermAction({ keybinding: { primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], - when: ContextKeyExpr.and(TerminalContextKeys.chatFocused, TerminalContextKeys.chatVisible), + when: ContextKeyExpr.and(TerminalChatContextKeys.chatFocused, TerminalChatContextKeys.chatVisible), weight: KeybindingWeight.WorkbenchContrib, }, icon: Codicon.close, @@ -78,15 +78,15 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 2, - when: ContextKeyExpr.and(TerminalContextKeys.chatFocused, - TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand)) + when: ContextKeyExpr.and(TerminalChatContextKeys.chatFocused, + TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand)) }, f1: true, precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalContextKeys.chatFocused, - TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.chatFocused, + TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) ), run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -105,13 +105,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalContextKeys.chatRequestActive.negate(), - TerminalContextKeys.chatAgentRegistered, - TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.chatRequestActive.negate(), + TerminalChatContextKeys.chatAgentRegistered, + TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) ), icon: Codicon.check, keybinding: { - when: TerminalContextKeys.chatRequestActive.negate(), + when: TerminalChatContextKeys.chatRequestActive.negate(), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.Enter, }, @@ -120,7 +120,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 0, - when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.chatRequestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -138,13 +138,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalContextKeys.chatRequestActive.negate(), - TerminalContextKeys.chatAgentRegistered, - TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.chatRequestActive.negate(), + TerminalChatContextKeys.chatAgentRegistered, + TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) ), icon: Codicon.check, keybinding: { - when: TerminalContextKeys.chatRequestActive.negate(), + when: TerminalChatContextKeys.chatRequestActive.negate(), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Alt | KeyCode.Enter, }, @@ -152,7 +152,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.chatRequestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -169,21 +169,21 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalContextKeys.chatRequestActive.negate(), - TerminalContextKeys.chatAgentRegistered, + TerminalChatContextKeys.chatRequestActive.negate(), + TerminalChatContextKeys.chatAgentRegistered, ), icon: Codicon.commentDiscussion, menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.Message), TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.Message), TerminalChatContextKeys.chatRequestActive.negate()), }, { id: MENU_TERMINAL_CHAT_WIDGET, group: 'main', order: 1, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.chatRequestActive.negate()), }], run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -200,13 +200,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalContextKeys.chatRequestActive.negate(), - TerminalContextKeys.chatAgentRegistered, + TerminalChatContextKeys.chatRequestActive.negate(), + TerminalChatContextKeys.chatAgentRegistered, CTX_INLINE_CHAT_EMPTY.negate() ), icon: Codicon.send, keybinding: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalChatContextKeys.chatRequestActive.negate()), weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Enter }, @@ -214,7 +214,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_INPUT, group: 'main', order: 1, - when: TerminalContextKeys.chatRequestActive.negate(), + when: TerminalChatContextKeys.chatRequestActive.negate(), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -230,14 +230,14 @@ registerActiveXtermAction({ title: localize2('cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive, - TerminalContextKeys.chatAgentRegistered + TerminalChatContextKeys.chatRequestActive, + TerminalChatContextKeys.chatAgentRegistered ), icon: Codicon.debugStop, menu: { id: MENU_TERMINAL_CHAT_INPUT, group: 'main', - when: TerminalContextKeys.chatRequestActive, + when: TerminalChatContextKeys.chatRequestActive, }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -253,15 +253,15 @@ registerActiveXtermAction({ title: localize2('feedbackHelpful', 'Helpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatResponseType.notEqualsTo(undefined) + TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined) ), icon: Codicon.thumbsup, - toggled: TerminalContextKeys.chatSessionResponseVote.isEqualTo('up'), + toggled: TerminalChatContextKeys.chatSessionResponseVote.isEqualTo('up'), menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', order: 1, - when: TerminalContextKeys.chatResponseType.notEqualsTo(undefined), + when: TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -277,15 +277,15 @@ registerActiveXtermAction({ title: localize2('feedbackUnhelpful', 'Unhelpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatResponseType.notEqualsTo(undefined), + TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), ), - toggled: TerminalContextKeys.chatSessionResponseVote.isEqualTo('down'), + toggled: TerminalChatContextKeys.chatSessionResponseVote.isEqualTo('down'), icon: Codicon.thumbsdown, menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', order: 2, - when: TerminalContextKeys.chatResponseType.notEqualsTo(undefined), + when: TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -301,20 +301,20 @@ registerActiveXtermAction({ title: localize2('reportIssue', 'Report Issue'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalContextKeys.chatRequestActive.negate(), - TerminalContextKeys.chatResponseType.notEqualsTo(undefined), - TerminalContextKeys.chatResponseSupportsIssueReporting + TerminalChatContextKeys.chatRequestActive.negate(), + TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), + TerminalChatContextKeys.chatResponseSupportsIssueReporting ), icon: Codicon.report, menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.notEqualsTo(undefined), TerminalContextKeys.chatResponseSupportsIssueReporting), + when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), TerminalChatContextKeys.chatResponseSupportsIssueReporting), group: 'inline', order: 3 }], // { // id: MENU_TERMINAL_CHAT_WIDGET, - // when: ContextKeyExpr.and(TerminalContextKeys.chatResponseType.notEqualsTo(undefined), TerminalContextKeys.chatResponseSupportsIssueReporting), + // when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), TerminalChatContextKeys.chatResponseSupportsIssueReporting), // group: 'config', // order: 3 // }], diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index ca3bdbab893..c3409674e12 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -21,11 +21,11 @@ import { IChatService, IChatProgress, InteractiveSessionVoteDirection, ChatUserA 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'; -import { TerminalChatResponseTypes, TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget'; import { ChatModel, ChatRequestModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { TerminalChatContextKeys, TerminalChatResponseTypes } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; const enum Message { NONE = 0, @@ -95,11 +95,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { return; } - this._requestActiveContextKey = TerminalContextKeys.chatRequestActive.bindTo(this._contextKeyService); - this._terminalAgentRegisteredContextKey = TerminalContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); - this._responseTypeContextKey = TerminalContextKeys.chatResponseType.bindTo(this._contextKeyService); - this._responseSupportsIssueReportingContextKey = TerminalContextKeys.chatResponseSupportsIssueReporting.bindTo(this._contextKeyService); - this._sessionResponseVoteContextKey = TerminalContextKeys.chatSessionResponseVote.bindTo(this._contextKeyService); + this._requestActiveContextKey = TerminalChatContextKeys.chatRequestActive.bindTo(this._contextKeyService); + this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); + this._responseTypeContextKey = TerminalChatContextKeys.chatResponseType.bindTo(this._contextKeyService); + this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.chatResponseSupportsIssueReporting.bindTo(this._contextKeyService); + this._sessionResponseVoteContextKey = TerminalChatContextKeys.chatSessionResponseVote.bindTo(this._contextKeyService); if (!this._chatAgentService.hasAgent(this._terminalAgentId)) { this._register(this._chatAgentService.onDidChangeAgents(() => { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 2cc70c7eb07..82f68ccf9f4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -22,8 +22,7 @@ import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/cha import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; +import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat'; export class TerminalChatWidget extends Disposable { @@ -48,8 +47,8 @@ export class TerminalChatWidget extends Disposable { ) { super(); - this._focusedContextKey = TerminalContextKeys.chatFocused.bindTo(this._contextKeyService); - this._visibleContextKey = TerminalContextKeys.chatVisible.bindTo(this._contextKeyService); + this._focusedContextKey = TerminalChatContextKeys.chatFocused.bindTo(this._contextKeyService); + this._visibleContextKey = TerminalChatContextKeys.chatVisible.bindTo(this._contextKeyService); this._container = document.createElement('div'); this._container.classList.add('terminal-inline-chat'); @@ -176,7 +175,7 @@ class TerminalChatResponseEditor extends Disposable { ) { super(); - this._responseEditorFocusedContextKey = TerminalContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); + this._responseEditorFocusedContextKey = TerminalChatContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); this._editorContainer = document.createElement('div'); this._editorContainer.classList.add('terminal-inline-chat-response'); From 9b38a276ce04615099802f185e911b2f086274e3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:33:19 -0800 Subject: [PATCH 108/228] Remove 'chat' from context keys as it's in the namespace --- .../chat/browser/terminalChat.ts | 18 ++--- .../browser/terminalChatAccessibilityHelp.ts | 2 +- ...rminalChatAccessibilityHelpContribution.ts | 2 +- .../browser/terminalChatAccessibleView.ts | 2 +- .../chat/browser/terminalChatActions.ts | 74 +++++++++---------- .../chat/browser/terminalChatController.ts | 10 +-- .../chat/browser/terminalChatWidget.ts | 6 +- 7 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 5aefe757469..89893d6d31e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -47,29 +47,29 @@ export const enum TerminalChatResponseTypes { export namespace TerminalChatContextKeys { /** Whether the chat widget is focused */ - export const chatFocused = new RawContextKey(TerminalChatContextKeyStrings.ChatFocus, false, localize('chatFocusedContextKey', "Whether the chat view is focused.")); + export const focused = new RawContextKey(TerminalChatContextKeyStrings.ChatFocus, false, localize('chatFocusedContextKey', "Whether the chat view is focused.")); /** Whether the chat widget is visible */ - export const chatVisible = new RawContextKey(TerminalChatContextKeyStrings.ChatVisible, false, localize('chatVisibleContextKey', "Whether the chat view is visible.")); + export const visible = new RawContextKey(TerminalChatContextKeyStrings.ChatVisible, false, localize('chatVisibleContextKey', "Whether the chat view is visible.")); /** Whether there is an active chat request */ - export const chatRequestActive = new RawContextKey(TerminalChatContextKeyStrings.ChatActiveRequest, false, localize('chatRequestActiveContextKey', "Whether there is an active chat request.")); + export const requestActive = new RawContextKey(TerminalChatContextKeyStrings.ChatActiveRequest, false, localize('chatRequestActiveContextKey', "Whether there is an active chat request.")); /** Whether the chat input has text */ - export const chatInputHasText = new RawContextKey(TerminalChatContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); + export const inputHasText = new RawContextKey(TerminalChatContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text.")); /** Whether the terminal chat agent has been registered */ - export const chatAgentRegistered = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); + export const agentRegistered = new RawContextKey(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered.")); /** Whether the chat response editor is focused */ - export const chatResponseEditorFocused = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); + export const responseEditorFocused = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseEditorFocused, false, localize('chatResponseEditorFocusedContextKey', "Whether the chat response editor is focused.")); /** The type of chat response, if any */ - export const chatResponseType = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseType, undefined, localize('chatResponseTypeContextKey', "The type of chat response, if any")); + export const responseType = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseType, undefined, localize('chatResponseTypeContextKey', "The type of chat response, if any")); /** Whether the response supports issue reporting */ - export const chatResponseSupportsIssueReporting = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); + export const responseSupportsIssueReporting = new RawContextKey(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting")); /** The chat vote, if any for the response, if any */ - export const chatSessionResponseVote = new RawContextKey(TerminalChatContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); + export const sessionResponseVote = new RawContextKey(TerminalChatContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index c9631f7fa1d..d330683af2e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -38,7 +38,7 @@ export class TerminalInlineChatAccessibilityHelpContribution extends Disposable options: { type: AccessibleViewType.Help } }); return true; - }, ContextKeyExpr.or(TerminalChatContextKeys.chatFocused, TerminalChatContextKeys.chatResponseEditorFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + }, ContextKeyExpr.or(TerminalChatContextKeys.focused, TerminalChatContextKeys.responseEditorFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts index afc24a15163..61d11998690 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts @@ -17,7 +17,7 @@ export class TerminalChatAccessibilityHelpContribution extends Disposable { static ID = 'terminalChatAccessiblityHelp'; constructor() { super(); - this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.chatFocused)); + this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.focused)); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index 93854689479..798c0bf9774 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -35,6 +35,6 @@ export class TerminalInlineChatAccessibleViewContribution extends Disposable { options: { type: AccessibleViewType.View } }); return true; - }, ContextKeyExpr.and(TerminalChatContextKeys.chatFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + }, ContextKeyExpr.and(TerminalChatContextKeys.focused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 7f8dc50524d..fc0777f9cde 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -21,7 +21,7 @@ registerActiveXtermAction({ title: localize2('startChat', 'Start Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, - when: ContextKeyExpr.and(TerminalChatContextKeys.chatFocused.negate(), TerminalContextKeys.focusInAny), + when: ContextKeyExpr.and(TerminalChatContextKeys.focused.negate(), TerminalContextKeys.focusInAny), weight: KeybindingWeight.WorkbenchContrib, }, f1: true, @@ -46,7 +46,7 @@ registerActiveXtermAction({ keybinding: { primary: KeyCode.Escape, secondary: [KeyMod.Shift | KeyCode.Escape], - when: ContextKeyExpr.and(TerminalChatContextKeys.chatFocused, TerminalChatContextKeys.chatVisible), + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.visible), weight: KeybindingWeight.WorkbenchContrib, }, icon: Codicon.close, @@ -78,15 +78,15 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 2, - when: ContextKeyExpr.and(TerminalChatContextKeys.chatFocused, - TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand)) + when: ContextKeyExpr.and(TerminalChatContextKeys.focused, + TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand)) }, f1: true, precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalChatContextKeys.chatFocused, - TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.focused, + TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) ), run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -105,13 +105,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalChatContextKeys.chatRequestActive.negate(), - TerminalChatContextKeys.chatAgentRegistered, - TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) ), icon: Codicon.check, keybinding: { - when: TerminalChatContextKeys.chatRequestActive.negate(), + when: TerminalChatContextKeys.requestActive.negate(), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.Enter, }, @@ -120,7 +120,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 0, - when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -138,13 +138,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalChatContextKeys.chatRequestActive.negate(), - TerminalChatContextKeys.chatAgentRegistered, - TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, + TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand) ), icon: Codicon.check, keybinding: { - when: TerminalChatContextKeys.chatRequestActive.negate(), + when: TerminalChatContextKeys.requestActive.negate(), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.Alt | KeyCode.Enter, }, @@ -152,7 +152,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -169,21 +169,21 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalChatContextKeys.chatRequestActive.negate(), - TerminalChatContextKeys.chatAgentRegistered, + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, ), icon: Codicon.commentDiscussion, menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_STATUS, group: '0_main', order: 1, - when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.Message), TerminalChatContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.Message), TerminalChatContextKeys.requestActive.negate()), }, { id: MENU_TERMINAL_CHAT_WIDGET, group: 'main', order: 1, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalChatContextKeys.chatResponseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EMPTY.negate(), TerminalChatContextKeys.responseType.isEqualTo(TerminalChatResponseTypes.TerminalCommand), TerminalChatContextKeys.requestActive.negate()), }], run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -200,13 +200,13 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalChatContextKeys.chatRequestActive.negate(), - TerminalChatContextKeys.chatAgentRegistered, + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.agentRegistered, CTX_INLINE_CHAT_EMPTY.negate() ), icon: Codicon.send, keybinding: { - when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalChatContextKeys.chatRequestActive.negate()), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, TerminalChatContextKeys.requestActive.negate()), weight: KeybindingWeight.WorkbenchContrib, primary: KeyCode.Enter }, @@ -214,7 +214,7 @@ registerActiveXtermAction({ id: MENU_TERMINAL_CHAT_INPUT, group: 'main', order: 1, - when: TerminalChatContextKeys.chatRequestActive.negate(), + when: TerminalChatContextKeys.requestActive.negate(), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -230,14 +230,14 @@ registerActiveXtermAction({ title: localize2('cancelChat', 'Cancel Chat'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalChatContextKeys.chatRequestActive, - TerminalChatContextKeys.chatAgentRegistered + TerminalChatContextKeys.requestActive, + TerminalChatContextKeys.agentRegistered ), icon: Codicon.debugStop, menu: { id: MENU_TERMINAL_CHAT_INPUT, group: 'main', - when: TerminalChatContextKeys.chatRequestActive, + when: TerminalChatContextKeys.requestActive, }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -253,15 +253,15 @@ registerActiveXtermAction({ title: localize2('feedbackHelpful', 'Helpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined) + TerminalChatContextKeys.responseType.notEqualsTo(undefined) ), icon: Codicon.thumbsup, - toggled: TerminalChatContextKeys.chatSessionResponseVote.isEqualTo('up'), + toggled: TerminalChatContextKeys.sessionResponseVote.isEqualTo('up'), menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', order: 1, - when: TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), + when: TerminalChatContextKeys.responseType.notEqualsTo(undefined), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -277,15 +277,15 @@ registerActiveXtermAction({ title: localize2('feedbackUnhelpful', 'Unhelpful'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), + TerminalChatContextKeys.responseType.notEqualsTo(undefined), ), - toggled: TerminalChatContextKeys.chatSessionResponseVote.isEqualTo('down'), + toggled: TerminalChatContextKeys.sessionResponseVote.isEqualTo('down'), icon: Codicon.thumbsdown, menu: { id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, group: 'inline', order: 2, - when: TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), + when: TerminalChatContextKeys.responseType.notEqualsTo(undefined), }, run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { @@ -301,14 +301,14 @@ registerActiveXtermAction({ title: localize2('reportIssue', 'Report Issue'), precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - TerminalChatContextKeys.chatRequestActive.negate(), - TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), - TerminalChatContextKeys.chatResponseSupportsIssueReporting + TerminalChatContextKeys.requestActive.negate(), + TerminalChatContextKeys.responseType.notEqualsTo(undefined), + TerminalChatContextKeys.responseSupportsIssueReporting ), icon: Codicon.report, menu: [{ id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, - when: ContextKeyExpr.and(TerminalChatContextKeys.chatResponseType.notEqualsTo(undefined), TerminalChatContextKeys.chatResponseSupportsIssueReporting), + when: ContextKeyExpr.and(TerminalChatContextKeys.responseType.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting), group: 'inline', order: 3 }], diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index c3409674e12..0226d6536f0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -95,11 +95,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!this._configurationService.getValue(TerminalSettingId.ExperimentalInlineChat)) { return; } - this._requestActiveContextKey = TerminalChatContextKeys.chatRequestActive.bindTo(this._contextKeyService); - this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.chatAgentRegistered.bindTo(this._contextKeyService); - this._responseTypeContextKey = TerminalChatContextKeys.chatResponseType.bindTo(this._contextKeyService); - this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.chatResponseSupportsIssueReporting.bindTo(this._contextKeyService); - this._sessionResponseVoteContextKey = TerminalChatContextKeys.chatSessionResponseVote.bindTo(this._contextKeyService); + this._requestActiveContextKey = TerminalChatContextKeys.requestActive.bindTo(this._contextKeyService); + this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.agentRegistered.bindTo(this._contextKeyService); + this._responseTypeContextKey = TerminalChatContextKeys.responseType.bindTo(this._contextKeyService); + this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService); + this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService); if (!this._chatAgentService.hasAgent(this._terminalAgentId)) { this._register(this._chatAgentService.onDidChangeAgents(() => { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 82f68ccf9f4..86b3fd8de33 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -47,8 +47,8 @@ export class TerminalChatWidget extends Disposable { ) { super(); - this._focusedContextKey = TerminalChatContextKeys.chatFocused.bindTo(this._contextKeyService); - this._visibleContextKey = TerminalChatContextKeys.chatVisible.bindTo(this._contextKeyService); + this._focusedContextKey = TerminalChatContextKeys.focused.bindTo(this._contextKeyService); + this._visibleContextKey = TerminalChatContextKeys.visible.bindTo(this._contextKeyService); this._container = document.createElement('div'); this._container.classList.add('terminal-inline-chat'); @@ -175,7 +175,7 @@ class TerminalChatResponseEditor extends Disposable { ) { super(); - this._responseEditorFocusedContextKey = TerminalChatContextKeys.chatResponseEditorFocused.bindTo(this._contextKeyService); + this._responseEditorFocusedContextKey = TerminalChatContextKeys.responseEditorFocused.bindTo(this._contextKeyService); this._editorContainer = document.createElement('div'); this._editorContainer.classList.add('terminal-inline-chat-response'); From 31df2b2df478f28cc65dc45ce7c7d11500052ba0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:34:40 -0800 Subject: [PATCH 109/228] Revert unneeded change to InlineChatWidget --- src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 3aeb25cd2e2..ca909ba98e0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -388,7 +388,7 @@ export class InlineChatWidget { buttonConfigProvider: action => { if (action.id === ACTION_REGENERATE_RESPONSE) { return { showIcon: true, showLabel: false, isSecondary: true }; - } else if ([ACTION_VIEW_IN_CHAT, ACTION_ACCEPT_CHANGES].includes(action.id)) { + } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { return { isSecondary: false }; } else { return { isSecondary: true }; From b9f9060711db2ab23c91e16687065523ff5b6161 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:37:54 -0800 Subject: [PATCH 110/228] Clarify lint suppression reason --- .../contrib/chat/electron-sandbox/actions/voiceChatActions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 aeb85706fc6..fe9ddb2f8f0 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -55,7 +55,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ThemeIcon } from 'vs/base/common/themables'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; -// TODO: The chat needs to move into contrib/terminal/ as we don't want anything importing from terminalContrib/ + +// This is a one-off/safe import, changing the eslint rules would require duplicating/complicating the rules // eslint-disable-next-line local/code-import-patterns import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; From ec00f84dce8115176af91c856894bc749e2367db Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:01:13 -0800 Subject: [PATCH 111/228] Remove test string --- .../contrib/terminalContrib/chat/browser/terminalChatActions.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index fc0777f9cde..72eac948274 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -35,8 +35,6 @@ registerActiveXtermAction({ } const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance); contr?.chatWidget?.reveal(); - // TODO: Remove this before merging to main - contr?.chatWidget?.setValue('list files'); } }); From 29d9a36ba6ea6f23dba59caec8ab344a4db10606 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 15:08:12 -0600 Subject: [PATCH 112/228] set vertical position --- .../chat/browser/terminalChatWidget.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 86b3fd8de33..0653f403de0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -108,6 +108,18 @@ export class TerminalChatWidget extends Disposable { this._focusedContextKey.set(true); this._visibleContextKey.set(true); this._inlineChatWidget.focus(); + const font = this._instance.xterm?.getFont(); + if (!font?.charHeight) { + return; + } + const cursorY = this._instance.xterm?.raw.buffer.active.cursorY ?? 0; + const height = font.charHeight * font.lineHeight; + const top = (cursorY + .5) * height; + this._container.style.top = `${top}px`; + const terminalHeight = this._instance.domElement.clientHeight; + if (terminalHeight && top > terminalHeight - this._inlineChatWidget.getHeight()) { + this._container.style.top = ''; + } } hide(): void { this._container.classList.add('hide'); From f70e23725040fca3ae5517c2d26a4b0765a9e444 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:09:23 -0800 Subject: [PATCH 113/228] Resolve simple todos, remove duplication --- .../chat/browser/media/terminalChatWidget.css | 3 +- .../browser/terminal.chat.contribution.ts | 4 +- .../browser/terminalChatAccessibilityHelp.ts | 44 ++++++++--------- ...rminalChatAccessibilityHelpContribution.ts | 48 ------------------- .../chat/browser/terminalChatController.ts | 2 - 5 files changed, 23 insertions(+), 78 deletions(-) delete mode 100644 src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index 602560618cc..64253f67db5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -29,8 +29,7 @@ .terminal-inline-chat .terminal-inline-chat-response { border: 1px solid var(--vscode-input-border, transparent); - /* TODO: Make themeable */ - background-color: #181818; + background-color: var(--vscode-panel-background); } .terminal-inline-chat .terminal-inline-chat-response:has(.monaco-editor.focused) { 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 0049340fadd..44eabbf13f6 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 @@ -5,15 +5,13 @@ import { WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { registerTerminalContribution } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; -import { TerminalInlineChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; import { TerminalInlineChatAccessibleViewContribution } 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/terminalChatAccessibilityHelpContribution'; import 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions'; +import { TerminalChatAccessibilityHelpContribution } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp'; registerTerminalContribution(TerminalChatController.ID, TerminalChatController, false); registerWorkbenchContribution2(TerminalInlineChatAccessibleViewContribution.ID, TerminalInlineChatAccessibleViewContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(TerminalInlineChatAccessibilityHelpContribution.ID, TerminalInlineChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(TerminalChatAccessibilityHelpContribution.ID, TerminalChatAccessibilityHelpContribution, WorkbenchPhase.Eventually); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index d330683af2e..c29c7394c3f 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -5,43 +5,41 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; 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 { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; 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 TerminalInlineChatAccessibilityHelpContribution extends Disposable { - static ID: 'terminalInlineChatAccessibilityHelpContribution'; +export class TerminalChatAccessibilityHelpContribution extends Disposable { + static ID = 'terminalChatAccessiblityHelp'; constructor() { super(); - this._register(AccessibilityHelpAction.addImplementation(106, 'terminalInlineChat', accessor => { - const terminalService = accessor.get(ITerminalService); - const accessibleViewService = accessor.get(IAccessibleViewService); - const controller: TerminalChatController | undefined = terminalService.activeInstance?.getContribution(TerminalChatController.ID) ?? undefined; - if (controller === undefined) { - return false; - } - const helpContent = getAccessibilityHelpText(accessor); - accessibleViewService.show({ - id: AccessibleViewProviderId.TerminalInlineChat, - verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, - provideContent(): string { return helpContent; }, - onClose() { - controller.focus(); - }, - options: { type: AccessibleViewType.Help } - }); - return true; - }, ContextKeyExpr.or(TerminalChatContextKeys.focused, TerminalChatContextKeys.responseEditorFocused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.focused)); } } +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); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts deleted file mode 100644 index 61d11998690..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelpContribution.ts +++ /dev/null @@ -1,48 +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 { localize } from 'vs/nls'; -import type { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -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 { 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'; - -export class TerminalChatAccessibilityHelpContribution extends Disposable { - static ID = 'terminalChatAccessiblityHelp'; - constructor() { - super(); - this._register(AccessibilityHelpAction.addImplementation(110, 'terminalChat', runAccessibilityHelpAction, TerminalChatContextKeys.focused)); - } -} - -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 content = []; - // TODO: Fill in more help text - content.push(localize('chat.overview', 'The terminal chat view is comprised of an input box, an editor where suggested commands are provided (Shift+Tab) and buttons to action the suggestion.')); - return content.join('\n\n'); -} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 0226d6536f0..47025af317b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -256,7 +256,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr requestId: this._currentRequest!.id, agentId: this._terminalAgentId, message: this._lastInput, - // TODO: ? variables: { variables: [] }, }; try { @@ -265,7 +264,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.rawValue?.inlineChatWidget.updateFollowUps(undefined); this._chatWidget?.rawValue?.inlineChatWidget.updateProgress(true); this._chatWidget?.rawValue?.inlineChatWidget.updateInfo(localize('thinking', "Thinking\u2026")); - // TODO: this._zone.value.widget.updateInfo(!this._session.lastExchange ? localize('thinking', "Thinking\u2026") : ''); await task; } catch (e) { From fa685221f2d1598d0da2d229ce3885a16b61a492 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 15:19:53 -0600 Subject: [PATCH 114/228] rm todo --- .../chat/browser/terminalChatAccessibilityHelp.ts | 3 +-- .../terminalContrib/chat/browser/terminalChatActions.ts | 2 +- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index c29c7394c3f..dad7747e7f1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -48,8 +48,7 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor): string { const runCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.RunCommand)?.getAriaLabel(); const insertCommandKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.InsertCommand)?.getAriaLabel(); const makeRequestKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.MakeRequest)?.getAriaLabel(); - //TODO: using this instead of the terminal command bc by definition the inline terminal chat is focused when this dialog is invoked. - const startChatKeybinding = keybindingService.lookupKeybinding('inlineChat.start')?.getAriaLabel(); + const startChatKeybinding = keybindingService.lookupKeybinding(TerminalChatCommandId.Start)?.getAriaLabel(); content.push(localize('inlineChat.overview', "Inline chat occurs within a terminal. It is useful for suggesting terminal commands. Keep in mind that AI generated code may be incorrect.")); content.push(localize('inlineChat.access', "It can be activated using the command: Terminal: Start Chat ({0}), which will focus the input box.", startChatKeybinding)); content.push(makeRequestKeybinding ? localize('inlineChat.input', "The input box is where the user can type a request and can make the request ({0}). The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.", makeRequestKeybinding) : localize('inlineChat.inputNoKb', "The input box is where the user can type a request and can make the request by tabbing to the Make Request button, which is not currently triggerable via keybindings. The widget will be closed and all content will be discarded when the Escape key is pressed and the terminal will regain focus.")); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 72eac948274..04ff3b3a472 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -21,7 +21,7 @@ registerActiveXtermAction({ title: localize2('startChat', 'Start Chat'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, - when: ContextKeyExpr.and(TerminalChatContextKeys.focused.negate(), TerminalContextKeys.focusInAny), + when: ContextKeyExpr.and(TerminalContextKeys.focusInAny), weight: KeybindingWeight.WorkbenchContrib, }, f1: true, diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 0653f403de0..4e9bed0f354 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -114,7 +114,7 @@ export class TerminalChatWidget extends Disposable { } const cursorY = this._instance.xterm?.raw.buffer.active.cursorY ?? 0; const height = font.charHeight * font.lineHeight; - const top = (cursorY + .5) * height; + const top = cursorY * height; this._container.style.top = `${top}px`; const terminalHeight = this._instance.domElement.clientHeight; if (terminalHeight && top > terminalHeight - this._inlineChatWidget.getHeight()) { From 27d1fc77f4fee8fdf442f9ae3f1175c1105760aa Mon Sep 17 00:00:00 2001 From: meganrogge Date: Fri, 16 Feb 2024 16:52:57 -0600 Subject: [PATCH 115/228] Fix position --- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 4e9bed0f354..180d51c1549 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -114,7 +114,7 @@ export class TerminalChatWidget extends Disposable { } const cursorY = this._instance.xterm?.raw.buffer.active.cursorY ?? 0; const height = font.charHeight * font.lineHeight; - const top = cursorY * height; + const top = cursorY * height + 10; this._container.style.top = `${top}px`; const terminalHeight = this._instance.domElement.clientHeight; if (terminalHeight && top > terminalHeight - this._inlineChatWidget.getHeight()) { From ead787f6f01630a1261fe80abe87242c9170ebc6 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 26 Feb 2024 11:13:02 -0800 Subject: [PATCH 116/228] fix issue --- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 180d51c1549..aa43972ef64 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -9,7 +9,7 @@ import { MarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/terminalChatWidget'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; From 9b3f22b333b4bb4d065abb787411411494f7c45b Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 26 Feb 2024 12:03:16 -0800 Subject: [PATCH 117/228] only allow starting chat when terminal agent has been registered --- .../contrib/terminalContrib/chat/browser/terminalChatActions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 04ff3b3a472..878246a2e26 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -28,6 +28,7 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + TerminalChatContextKeys.agentRegistered ), run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { From 51209b3a1448ff6df84c973ca1ab786fa3a99d72 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 26 Feb 2024 12:26:30 -0800 Subject: [PATCH 118/228] on hide, reset input value --- .../contrib/terminalContrib/chat/browser/terminalChatWidget.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index aa43972ef64..74bb3b4ee9d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -132,6 +132,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.updateToolbar(false); this._focusedContextKey.set(false); this._visibleContextKey.set(false); + this._inlineChatWidget.value = ''; this._instance.focus(); } focus(): void { From a1070cb7f172f4930edfe3aa38f7b473c85169f4 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 26 Feb 2024 12:40:34 -0800 Subject: [PATCH 119/228] set vertical position to below cursor line --- .../terminalContrib/chat/browser/terminalChatWidget.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 74bb3b4ee9d..26efec69639 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -112,9 +112,9 @@ export class TerminalChatWidget extends Disposable { if (!font?.charHeight) { return; } - const cursorY = this._instance.xterm?.raw.buffer.active.cursorY ?? 0; + const cursorY = (this._instance.xterm?.raw.buffer.active.cursorY ?? 0) + 1; const height = font.charHeight * font.lineHeight; - const top = cursorY * height + 10; + const top = cursorY * height + 12; this._container.style.top = `${top}px`; const terminalHeight = this._instance.domElement.clientHeight; if (terminalHeight && top > terminalHeight - this._inlineChatWidget.getHeight()) { From 7d546baaeacf0a045fa68259f934109c9f51e973 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 27 Feb 2024 10:08:15 -0800 Subject: [PATCH 120/228] align start command name with inline chat, get it to show up in command pallette --- .../contrib/inlineChat/browser/inlineChatActions.ts | 2 +- .../terminalContrib/chat/browser/terminalChatActions.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 75ca16a1f24..ae407d07b67 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -35,7 +35,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); -export const LOCALIZED_START_INLINE_CHAT_STRING = localize2('run', 'Start Inline Chat'); +export const LOCALIZED_START_INLINE_CHAT_STRING = localize2('run', 'Start in Editor'); export const START_INLINE_CHAT = registerIcon('start-inline-chat', Codicon.sparkle, localize('startInlineChat', 'Icon which spawns the inline chat from the editor toolbar.')); // some gymnastics to enable hold for speech without moving the StartSessionAction into the electron-layer diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 878246a2e26..d7e2d3850d3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -9,7 +9,8 @@ import { localize2 } from 'vs/nls'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { AbstractInlineChatAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_PROVIDER } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; @@ -18,17 +19,18 @@ import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/cha registerActiveXtermAction({ id: TerminalChatCommandId.Start, - title: localize2('startChat', 'Start Chat'), + title: localize2('startChat', 'Start in Terminal'), keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyI, when: ContextKeyExpr.and(TerminalContextKeys.focusInAny), weight: KeybindingWeight.WorkbenchContrib, }, f1: true, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), - TerminalChatContextKeys.agentRegistered + CTX_INLINE_CHAT_HAS_PROVIDER ), run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { From 37b275146adf89a63d6f29c3f72129069f6fed12 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 27 Feb 2024 10:19:17 -0800 Subject: [PATCH 121/228] fix context keys for terminal chat start/close --- .../terminalContrib/chat/browser/terminalChatActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index d7e2d3850d3..709b4ac8f24 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -29,7 +29,7 @@ registerActiveXtermAction({ category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.focusInAny), CTX_INLINE_CHAT_HAS_PROVIDER ), run: (_xterm, _accessor, activeInstance) => { @@ -59,7 +59,7 @@ registerActiveXtermAction({ f1: true, precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), - ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), + ContextKeyExpr.and(TerminalChatContextKeys.focused, TerminalChatContextKeys.visible) ), run: (_xterm, _accessor, activeInstance) => { if (isDetachedTerminalInstance(activeInstance)) { From d528f1cd4a2957711d7b948395549adde32ff431 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 27 Feb 2024 14:54:24 -0800 Subject: [PATCH 122/228] render first code block ASAP, fix issue with accessibility service --- .../chat/browser/terminalChatController.ts | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 47025af317b..20d8959e32b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -58,8 +58,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr private readonly _responseSupportsIssueReportingContextKey!: IContextKey; private readonly _sessionResponseVoteContextKey!: IContextKey; - private _requestId: number = 0; - private _messages = this._store.add(new Emitter()); private _currentRequest: ChatRequestModel | undefined; @@ -225,10 +223,12 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (!this._lastInput) { return; } - this._chatAccessibilityService.acceptRequest(); + const accessibilityRequestId = this._chatAccessibilityService.acceptRequest(); this._requestActiveContextKey.set(true); const cancellationToken = new CancellationTokenSource().token; let responseContent = ''; + let firstCodeBlock: string | undefined; + let shellType: string | undefined; const progressCallback = (progress: IChatProgress) => { if (cancellationToken.isCancellationRequested) { return; @@ -240,6 +240,21 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (this._currentRequest) { this._model?.acceptResponseProgress(this._currentRequest, progress); } + if (!firstCodeBlock) { + const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; + if (firstCodeBlockContent) { + const regex = /```(?[\w\n]+)\n(?[\s\S]*?)```/g; + const match = regex.exec(firstCodeBlockContent); + firstCodeBlock = match?.groups?.content.trim(); + shellType = match?.groups?.language; + if (firstCodeBlock) { + this._chatWidget?.rawValue?.renderTerminalCommand(firstCodeBlock, accessibilityRequestId, shellType); + this._responseTypeContextKey.set(TerminalChatResponseTypes.TerminalCommand); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + this._messages.fire(Message.ACCEPT_INPUT); + } + } + } }; await this._model?.waitForInitialization(); @@ -280,25 +295,13 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); } this._lastResponseContent = responseContent; + if (!firstCodeBlock) { + this._chatWidget?.rawValue?.renderMessage(responseContent, accessibilityRequestId, this._currentRequest!.id); + this._responseTypeContextKey.set(TerminalChatResponseTypes.Message); + this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); + this._messages.fire(Message.ACCEPT_INPUT); + } } - const firstCodeBlockContent = marked.lexer(responseContent).filter(token => token.type === 'code')?.[0]?.raw; - const regex = /```(?\w+)\n(?[\s\S]*?)```/g; - const match = regex.exec(firstCodeBlockContent); - const codeBlock = match?.groups?.content.trim(); - const shellType = match?.groups?.language; - this._requestId++; - if (cancellationToken.isCancellationRequested) { - return; - } - if (codeBlock) { - this._chatWidget?.rawValue?.renderTerminalCommand(codeBlock, this._requestId, shellType); - this._responseTypeContextKey.set(TerminalChatResponseTypes.TerminalCommand); - } else { - this._chatWidget?.rawValue?.renderMessage(responseContent, this._requestId, this._currentRequest!.id); - this._responseTypeContextKey.set(TerminalChatResponseTypes.Message); - } - this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); - this._messages.fire(Message.ACCEPT_INPUT); } updateInput(text: string, selectAll = true): void { From 179eeb65ce28e5aae850ae09692995192615e073 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 27 Feb 2024 15:04:43 -0800 Subject: [PATCH 123/228] move all accessibility chat service code into controller --- .../chat/browser/terminalChatController.ts | 16 +++++++++------- .../chat/browser/terminalChatWidget.ts | 10 +++------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 20d8959e32b..9ddc1e54349 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -248,7 +248,8 @@ export class TerminalChatController extends Disposable implements ITerminalContr firstCodeBlock = match?.groups?.content.trim(); shellType = match?.groups?.language; if (firstCodeBlock) { - this._chatWidget?.rawValue?.renderTerminalCommand(firstCodeBlock, accessibilityRequestId, shellType); + this._chatWidget?.rawValue?.renderTerminalCommand(firstCodeBlock, shellType); + this._chatAccessibilityService.acceptResponse(firstCodeBlock, accessibilityRequestId); this._responseTypeContextKey.set(TerminalChatResponseTypes.TerminalCommand); this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); @@ -290,17 +291,18 @@ export class TerminalChatController extends Disposable implements ITerminalContr if (this._currentRequest) { this._model?.completeResponse(this._currentRequest); } - const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; - if (supportIssueReporting !== undefined) { - this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); - } this._lastResponseContent = responseContent; - if (!firstCodeBlock) { - this._chatWidget?.rawValue?.renderMessage(responseContent, accessibilityRequestId, this._currentRequest!.id); + if (!firstCodeBlock && this._currentRequest) { + this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId); + this._chatWidget?.rawValue?.renderMessage(responseContent, this._currentRequest.id); this._responseTypeContextKey.set(TerminalChatResponseTypes.Message); this._chatWidget?.rawValue?.inlineChatWidget.updateToolbar(true); this._messages.fire(Message.ACCEPT_INPUT); } + const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting; + if (supportIssueReporting !== undefined) { + this._responseSupportsIssueReportingContextKey.set(supportIssueReporting); + } } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 26efec69639..8400830c1d7 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -18,7 +18,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService'; import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -42,8 +41,7 @@ export class TerminalChatWidget extends Disposable { terminalElement: HTMLElement, private readonly _instance: ITerminalInstance, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService ) { super(); @@ -87,8 +85,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.updateInfo(localize('welcome.1', "AI-generated commands may be incorrect")); } - async renderTerminalCommand(command: string, requestId: number, shellType?: string): Promise { - this._chatAccessibilityService.acceptResponse(command, requestId); + async renderTerminalCommand(command: string, shellType?: string): Promise { this._responseEditor?.show(); if (!this._responseEditor) { this._responseEditor = this._instantiationService.createInstance(TerminalChatResponseEditor, command, shellType, this._container, this._instance); @@ -96,10 +93,9 @@ export class TerminalChatWidget extends Disposable { this._responseEditor.setValue(command); } - renderMessage(message: string, accessibilityRequestId: number, requestId: string): void { + renderMessage(message: string, requestId: string): void { this._responseEditor?.hide(); this._inlineChatWidget.updateChatMessage({ message: new MarkdownString(message), requestId, providerId: 'terminal' }); - this._chatAccessibilityService.acceptResponse(message, accessibilityRequestId); } reveal(): void { From 57ebec79c2ded13f497a279b09ef691e060722a5 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Tue, 27 Feb 2024 15:19:27 -0800 Subject: [PATCH 124/228] fix width issues --- .../terminalContrib/chat/browser/terminalChatWidget.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 8400830c1d7..3bf19460298 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -116,7 +116,17 @@ export class TerminalChatWidget extends Disposable { if (terminalHeight && top > terminalHeight - this._inlineChatWidget.getHeight()) { this._container.style.top = ''; } + this._updateWidth(); + this._register(this._instance.onDimensionsChanged(() => this._updateWidth())); } + + private _updateWidth() { + const terminalWidth = this._instance.domElement.clientWidth; + if (terminalWidth && terminalWidth < 640) { + this._inlineChatWidget.layout(new Dimension(terminalWidth - 40, this._inlineChatWidget.getHeight())); + } + } + hide(): void { this._container.classList.add('hide'); this._reset(); From ca48da35e18e56366a44672e9dd3e531a6186cb7 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 10:31:31 -0800 Subject: [PATCH 125/228] cue -> signal --- .../browser/accessibilitySignalService.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index d2689b002d7..782de469f5b 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -17,15 +17,15 @@ export const IAccessibilitySignalService = createDecorator; - playAccessibilitySignals(cues: (AccessibilitySignal | { cue: AccessibilitySignal; source: string })[]): Promise; - isSoundEnabled(cue: AccessibilitySignal): boolean; - isAnnouncementEnabled(cue: AccessibilitySignal): boolean; - onSoundEnabledChanged(cue: AccessibilitySignal): Event; - onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event; + playSignal(signal: AccessibilitySignal, options?: IAccessbilitySignalOptions): Promise; + playAccessibilitySignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise; + isSoundEnabled(signal: AccessibilitySignal): boolean; + isAnnouncementEnabled(signal: AccessibilitySignal): boolean; + onSoundEnabledChanged(signal: AccessibilitySignal): Event; + onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event; - playSound(cue: Sound, allowManyInParallel?: boolean): Promise; - playSignalLoop(cue: AccessibilitySignal, milliseconds: number): IDisposable; + playSound(signal: Sound, allowManyInParallel?: boolean): Promise; + playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable; } export interface IAccessbilitySignalOptions { @@ -68,26 +68,26 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } } - public async playAccessibilitySignals(cues: (AccessibilitySignal | { cue: AccessibilitySignal; source: string })[]): Promise { - for (const cue of cues) { - this.sendSignalTelemetry('cue' in cue ? cue.cue : cue, 'source' in cue ? cue.source : undefined); + public async playAccessibilitySignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise { + for (const signal of signals) { + this.sendSignalTelemetry('signal' in signal ? signal.signal : signal, 'source' in signal ? signal.source : undefined); } - const cueArray = cues.map(c => 'cue' in c ? c.cue : c); - const alerts = cueArray.filter(cue => this.isAnnouncementEnabled(cue)).map(c => c.announcementMessage); + const signalArray = signals.map(s => 'signal' in s ? s.signal : s); + const alerts = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage); if (alerts.length) { this.accessibilityService.status(alerts.join(', ')); } // Some sounds are reused. Don't play the same sound twice. - const sounds = new Set(cueArray.filter(cue => this.isSoundEnabled(cue)).map(cue => cue.sound.getSound())); + const sounds = new Set(signalArray.filter(signal => this.isSoundEnabled(signal)).map(signal => signal.sound.getSound())); await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true))); } - private sendSignalTelemetry(cue: AccessibilitySignal, source: string | undefined): void { + private sendSignalTelemetry(signal: AccessibilitySignal, source: string | undefined): void { const isScreenReaderOptimized = this.accessibilityService.isScreenReaderOptimized(); - const key = cue.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); + const key = signal.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); // Only send once per user session if (this.sentTelemetry.has(key) || this.getVolumeInPercent() === 0) { return; @@ -107,7 +107,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi comment: 'This data is collected to understand how signals are used and if more signals should be added.'; }>('signal.played', { - signal: cue.name, + signal: signal.name, source: source ?? '', isScreenReaderOptimized, }); @@ -240,8 +240,8 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return Event.fromObservableLight(this.isSoundEnabledCache.get({ signal })); } - public onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event { - return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal: cue })); + public onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event { + return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal })); } } From 819d26fe01fd2c6aaf03e17ca426a3a95d95f2a3 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 10:50:58 -0800 Subject: [PATCH 126/228] alert -> announcement --- .../browser/accessibilitySignalService.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 782de469f5b..b8078d3182c 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -57,9 +57,9 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } public async playSignal(signal: AccessibilitySignal, options: IAccessbilitySignalOptions = {}): Promise { - const alertMessage = signal.announcementMessage; - if (this.isAnnouncementEnabled(signal, options.userGesture) && alertMessage) { - this.accessibilityService.status(alertMessage); + const announcementMessage = signal.announcementMessage; + if (this.isAnnouncementEnabled(signal, options.userGesture) && announcementMessage) { + this.accessibilityService.status(announcementMessage); } if (this.isSoundEnabled(signal, options.userGesture)) { @@ -73,9 +73,9 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi this.sendSignalTelemetry('signal' in signal ? signal.signal : signal, 'source' in signal ? signal.source : undefined); } const signalArray = signals.map(s => 'signal' in s ? s.signal : s); - const alerts = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage); - if (alerts.length) { - this.accessibilityService.status(alerts.join(', ')); + const announcements = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage); + if (announcements.length) { + this.accessibilityService.status(announcements.join(', ')); } // Some sounds are reused. Don't play the same sound twice. @@ -214,7 +214,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi () => event.signal.announcementMessage ? this.configurationService.getValue<'auto' | 'off' | 'userGesture' | 'always' | 'never'>(event.signal.settingsKey + '.announcement') : false ); return derived(reader => { - /** @description alert enabled */ + /** @description announcement enabled */ const setting = settingObservable.read(reader); if ( !this.screenReaderAttached.read(reader) From a629412f21715c62de57df845a3477322bc3eb5c Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 11:48:22 -0800 Subject: [PATCH 127/228] #201901 --- .../browser/accessibilitySignalService.ts | 8 ++++++++ .../browser/media/voiceRecordingStarted.mp3 | Bin 0 -> 42112 bytes .../browser/accessibilityConfiguration.ts | 10 ++++++++++ .../contrib/speech/browser/speechService.ts | 6 ++++-- 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index d2689b002d7..469f8fbf019 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -314,6 +314,7 @@ export class Sound { public static readonly clear = Sound.register({ fileName: 'clear.mp3' }); public static readonly save = Sound.register({ fileName: 'save.mp3' }); public static readonly format = Sound.register({ fileName: 'format.mp3' }); + public static readonly voiceRecordingStarted = Sound.register({ fileName: 'voiceRecordingStarted.mp3' }); private constructor(public readonly fileName: string) { } } @@ -582,6 +583,13 @@ export class AccessibilitySignal { settingsKey: 'accessibility.signals.format' }); + public static readonly voiceRecordingStarted = AccessibilitySignal.register({ + name: localize('accessibilitySignals.voiceRecordingStarted', 'Voice Recording Started'), + sound: Sound.voiceRecordingStarted, + legacySoundSettingsKey: 'audioCues.voiceRecordingStarted', + settingsKey: 'accessibility.signals.voiceRecordingStarted' + }); + private constructor( public readonly sound: SoundSource, public readonly name: string, diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..cc959d00a46cc72bb6fb6a6763e36d129c5e317e GIT binary patch literal 42112 zcmeFYXH-*NxA(nw8X*J-J@ilw2)GkKiUJ8uO6V34l+cSH-G*MI&;_I^0Z~JhrXu#9 z08$i@CU!+pM6gg*P{jHQQ#j1 z{!!o`1^!Xs9|itV;2#D4QQ#j1{!!o`1^)k{fZ+FK%HP*w#`ybyn)&+xy!qEbnfI^b zf7@3teoCIZOzARC3#*>~?8>|8a=3`P&F;gMUWGTcWEjZ60RrfNye=MA5GT zsO$cwBCbY7XFab=(qvf`n5YEJ8jb;IXtd637_K&&XRd_U7OCOL z70a`%FXK#X1AK{a;&r0f;&q*-D_3VU%dW>8`y{IOjXh7yhV_qRKT+WCKK7Iu9o1VgJUkiKLsmfMP9g*jC#tBX#lo z7Vwhyjfd8_JaLu#84bT;)=0Z;lu_zpk|tQir*8LXj*AnlmVh%l;{?wU zt$*=@Tr@C)|7oXR|9E07wVWn1)A~hnUX+!3LY2811c??FB;hmM*ap|pN$_+G90tJ0 zrTY}5f+VqEp+p|*?nWMZ{M{^Co0q}uAmH7}MrsNi2oZ#v#23;jc;c(gdU8N+UN$4v zFp0y(WfU{$0X+J)yN?+=M8s1vIvEb}yW0iaaRQ~&Ct#1E>~Zfft2YiyTG1QwtPI<0 zMNdornST(NYSSV3{kRKXJ@NUaK-kJ4GuUR*w*p7#m1qj-l4m+}0M-ckQT~Fw89zy4 zd^EI`k*3cegggf?oU9kcW2O2+z3m-iTySLI*+Z5sJtvb9E>2_}4FMK^1=HJil{GN5 zH;YRFTnf$#t@uJWqcazWX_wEtxwrht(KmHd=L6Ga>1gk!Z;3m7&fn;&|7Am-E@@p& zdHWFWJrwx#)AH!Sq|aYIHGO|BoZavUWS}^oT#*4KOO2yW+MDEx`k*OnC<7eEa`{jS zdKk$d%U@3%sW^_p!R10Uf?T0UCc~W(X#sSpwAbaIABeQC^Zr$pfGs z>U3jaPuu-zACSz$Qcc>O%=!#?3%p2x(Sc>!eV~H5&uaU);n(ZG-0s7;blc_U+<7^JXJV7-~fHXtlRD>Ht1$|=JtWjG0~KI5dtp=gV04#1A-UCSb!;UJ&CS$ zC~u=JC86Ep7Oy6A?A37quqRk6$l!s1ABrw_Y=3Q3bN#UCtnY$!wVE_ImY6^E${liX zgjK@`-5Mfx<$YDQqbIlJx4v^o3mE#H<#8_LPujFqk6HtCRkoz<$7|zT2_;fra?NaX zl3)~ZXg#b1Mfe4smaRckCWs=W=ZRx@f2V;QqA@)=uhQ^$z#d||9mDRZ{PH!loy<7; zpyKh}lE-vB*U2R+H{hmsoHI%{(F$!@QIJkH!d`Hmx>0q-vM@gP+%Uq$vEpY)SKPhZ zaVzaZ#E)NBpsVu3=MN|v+a>y7pg#GBt5);M&x{?<1E1^U`{Q@j-dur+73@Jks?%6IjZ}`ZCN;^x#cvB1~f1RuA zEUUtv)mM~^rS-K)86L3tnE`IsAms9qsCM}@f#CCJXjnGnF0EG`=$ZblDCl#3&+6 zgoF+c0e}R+e*-kUjN2OD_f;c+pBaN6Zr(!c7f4mMKL-BrhOn(XYfy!!hHUBljZEil z0!-@0svSdaE20B}DJAP5V9XO0PUx7nP=X_w?cLi9pY%XlSi8NOA^y?G`Nz(Fdmh_V zRuL7nnL~BY2F|w~^CRsD7bI2Sn%q!K>abC?kWKb)0#LC3Pkk4av&FvU1>n zLpz(NR0lt88g9ZaY2RS9_55MM>(C8d`wi~Zp0;6m#LO$klRtfJi1t45S$E}^Q8fDW z<9b;I!uPwW@Aort(lQJH#-N{Lvm!T}OoSz41A@9q&%$pB`JtBp$UIBR$v=n2cGo-` z-tXZe=O%@Yo1JE!snB}#>QwwmSWp1Yo*v26rzfI9h26dB8(4ek^_i7)51>7Xn?PX{ z7Vd9#W&ur?d4wPh(F`o%-QgSO+sIFASZ%#;aCB9s)7PQvk9OaQTxd)4UT(K@eYHDH zM|ea7l~gD87h5|zDu-65^QsWU4#?%1>)lat1e zA4ML;p+P%=VD1P3fWhHhE4K3X9Ja+mc?shAN{zvXb#}?b1h~ADk`^Mskd1*v;jlvV zFgUU+3E3iZDHWEUK`#vLF0K|ar(L8Yu&Btk5@*%qF-iwu?uj&@t~#kqUBwUodU12)P94T z(K+u(X5DXM#lJ+1#B*Cd{s_w_<3oZs6CwC$Tr9hBi~b?A?|)u7AM-!ctt#bU z_sLESgTwCe5KLYx0pI|THW7UI&%_?4zoT}@Os31P^{CG1zrr=w_<v63Oi{e-3>J zMD-dcAp_(bs92A$&4UF>+WD{L;(zJuib76~;$tv?4yUQ`$TTY`vAY;e$zp)5x|}~J z#*WWR!VM;qvIm|=D?YfbD)i{&5SaQNd#dXf{RwvE{qK^o9>DNkBu042J&8KwPXtoC)CRRob8o^~3Y|$j+`aT`Rwt zqA&-uoqdfbKi=a;Ev>e(u038JdfFv++09n}T=2cif9}03v)yOSqW;v?9>gWuf7l`z z3@rp;cBsQ9aQ_#UY-nqD2^##tmKxDw({lpZ@E%h;1V_wHgo91s$>f1Ecd!lvv5y;6 zMjv_h4cPayYrCjeI&GBa0`ltF_rKdVNg=KwZ^xAd(V4vXkU+&_RxU3FB-H;lZ$I1E1#WN*izvG<9Z%#nnCPK< z*i3r}u}~c>KWMVw>+lh?ls@0VdW{dyKa!o!j+FJE9kdxeXk9EmBoO>~(to+sLFiP3 zZG{f_{6X?UNa{5v*_ULWb(~`k>rEL0jCkj06Y|Xv*lZ>tn-+va(Ju6vSB*)DIPS&z zTCD4x+dGhVrTId{-jB1-!+#6(zduObd2H9<@YlB)tFOzdUY6gKcvZXjJ54x5^Q!0v z3e?7Hwj!gvjWeXzLq{7`NR6l)qJu)~gJVx0#?MA1WJY`WFa_xg?srNP>|-!6qZh3NDxiWxi^b+;UO9o97=Ixijn zHc9jM{atdaix%5%`P>suS+KwW*7NACwsC#Id+;j3xnn*9l^S-6!J>HWtmB~k-BFQ_ zt|N#l(}Z&go|6~7u5WB@D4f*SOPRldG`C#+oZ+OYyH&enlKvdQuGL=Mx3YAh^wiH! zQkL6hJ_g-+bLO?+uNu(<&f^1b6zxK{UL9Bd7CTPG71&|R{^kcjGA-KSe~QO~!u;U- zb7}Ivvna_qSLQM4@%*pnh==SD&jR5v74s6w9$7Z98976dm>>g(&*qwx`{sr*qVf@% z`F1{%wv%ibDF*evJ*cSgB&`H_)+^=Q%le2Sr|cP$RYck*3{nMajQB33Jh3WX8|a~TLN5yc9(>2lKADc-n84aIKP9MG zm3g;$$~ibR$@*oRJ!`D+beu38(BAe6Abw+@w7Q?#v+EXZELc))p8% zRJgH~Fr2<~&e*9o6!-{S*_mOa%`hF<-2lAHI4`xih5IV_#nqoRBWLp5qMXn5_&u0= zk@j3rQwxYG*~6*PE>g*_B|JVi zGnpk?&fiRleVfi6rD@{?eel*@G-=9)J``k#g^x+o)PUoUKery^4rpYkBwcbUJy@?+ zSNgzYNH?&&j6YLzd+gm_MC_#r1)SWbVC}*-45vgvwrV3L8II?13omaVOxUJADy=*| z{b|FxaNSkf-Zu8)nfY}COL2mpISMca!i@FbkCQ~@_(gRlTwq*Va~pXWo+JWfK@Y+B zcx4nQPD1JKKaq(vqHhJl3Y8LxCFOpcV6;kD2A zsJf#j{(Jt&-5YtM;utqpu)}8Mn2n(5QsDPJfq(cXjc>(GR&V*8CVYN`HiQuZTpjS_N7bz%OVM+*r<24MHH2#Ew6DE`xMzXCOAjDlrS(!(Kr+q^>C_ zBl&jm>)um&ZAZyX8>(U^zGy@)MJ-03sIayXIcpSfva7)BaLTpXQ#S5ey&pCT1Vbb| z{LbdrV~b|84oRd5A9)Kc}SaUj3}6%`#pD0dYNh=E!0Wm7%D-70zAu2 zzirX=LAI95ICdif8brBeVTUBzTs{F{?H(P=$=zrpx>t- ziyHI$dzT}Ni@p>JR|~=?{N6H83tpH`W_XPgSa?Xw-*ca}qIuT4R-Pu_iCL9%0E-s~ zoeJ$ZG?|2#p|6Juq-y7;M>6e#&IYc=b+=X62zrt^hq*j;udM0{&)nO1Rg{7%oad%^bM}KO()KKV zTh%*`GB*v6XQ<*;>993^&N7fq))e?k@mTCcLs9CMfD;F;SJ&d7@-=?s$Q-J@sQM^6 zEI6uyk%>kYiRBr6k=5la^o7hI4PbPj!kKtGcF?m1jhqdF+-mnj9BNOYZLBnc&lMA` zlWQePZu_C#UmKr}))cG%sFRVH|6o9LIZOv=O+PHQCtP#TCnCr`uUR_ivsu7t%kx)t z&n%pI@b{ATORJn8|n-gTUpoG;t;!@PKxLy)<^?Z235xarWTZr-wHbFak>b zJF>+eANeAE8qbSp4fb-zZbLgFYRoXTO&1&PzuQvj@am|s^08ZzR?c@fO}*JsA`nbL zHcU&?^Fy-a@LV$}@_~u50+2@X?DjU%XFP-A+BZP<3=|{+$U`uSH3VnBw=oM}sgXuV zvy^rPohiWIz_lXoETK20`CT>+A836WJb&Yj1p3+@i{=KZ<*$>qBo{<>?`&P&QqQ{>m~uz z(3JNb=qnKNLpi}fn47n=SG>paw%{pgu>n%(=V&26ERD(_aMd)P!SAH{;5TzsIU-zD zJceS%5qcq7Ve8_FodxC= zP8b(;TYhl}jDFi&&F=H{p(ux(u#?>({bi}D!;jn!^$H9YE0@CWE(=x**dSzJZ2kG@ zu(b(I3xi<5H!xs!5(W+PHh|19JWtIBhy+UjZCDME4QRo?Xv0rR#7lrf#Y&r^?x*S`46O6l0Ahp6f^ddIcY z%5ydhZ}R(65i}a}CT_}jv2H%r(nc@>S=gFDOxaHIUBCv~k+5H-s6P4ry&?#6R=I?F!L8%a;CNJ4c|Isza1rWfywwMTi7cqE+S$ z1#$PO=`P)KH@C?FVQ9}vCxv5yyR(#{R_#5MTH1e4ZlzF7`9Su1_Q73~ii#l7@r0Gzpv}4F;i^yqz4)D(TbN-UhDn zqS+K%hA~N-_Qim5@m*)5d#3XvJQog7w}bo!OEdViiL`%X(_QZF8b^tEgCV!R5|!RO zDqKz1cE7)bjBe43D;vV@%!7}I)-N?LW%d0PRXSL+@-rHuu|&x`J${nzIVwVKkED#( z_$g)}Y|Vx7v-jB1Zag6^KL9uR^lyF)E4j>7y?7XS4(cy9G)}jbgX~yeAO#)?Lh$?{ zBx5(E2aH*nqa%X@Gyscn=*2?&n3hQ9_RzjTbkPO!>n#f!u$};`FctS3)3)7CsjwLEk_s^$3^0G;qP&QTiOkc%*&%P)b z<38otpuErAJ{oc(vCo&%KtnNN`s8O+oP>TxnV}BBJ5o#(^9blmvORc!WRK3K1LAB( ztITqcXZ8T;=?%^ORM6Mow`4B#(yUuVgbuQ6qizWWA58egKx@-25A@j?+9ptA+dt{A z+wcA3erLc??W&Q7#qC{lE4KtID%p@5wbZygR~LG>Wey_XNaV}RZfIKcw<$<@2wB4N zAay_o0w_VUI(a|?cE1#yQ8-4SfZB~2_s#K_A$yHI3mYbhl76{&Z+BvM=We`Z6&+=& zI2&u4S@C?R>9uHZpLy%8b(x%fPb5b2ad;%nZDA*Y3YzZz)%#l$r zi35U@i^#Y6jGt5EqX?;HW$&{&BY2d{nLW=^RJ1mIx6-_ZI9ba*=e^~W#s>SFH0$ik z(RO!2^>kWlrVsvkw_)haeEui@;W*M0F7Gmk=zobGdvHR*fY)!cu*Oe5fH2nQvsVt$ z*%3nfBjktv6eYAj%wH!p*6dG@1n{Lp4c|Ka5r*a?&=B7Mi01*IffAg_Bx7e^B0AgO z5yV!TX}$$S3YHY!qW`jKG&U z&@n*ZwbHY7IY$@(%dI}_WnnK`%g)p_wOMS_zDLiGUnr^9peceZ@2q2zlD#5)^90*@ zbE4-V)-BJ~rl~Z^TY7N{ha1yKf6n`_=}?aB8!1p+7`UL4%<1YcBw(xR>FKm#3RtOq)2CDQZ*C|N58Lb6b2YH;RNZke;f@Tjy;+?C5=qE2A zf0hI@4vG-3F(xq;<`i#=_X(B6T2}S_xqv6Z0aH#VC`H9eS*tf%UiwMaCcpe!kih%$OD5 zw>{R6JF(>RG0?wRu$aO@QGJzy8ZGgUGeht?HL2yfyc~`n%b{ZLm@!@hI43L8M}{vk z(BLfNhZ6sd44l=KodxRv9%+nBw1TT-BBTbZ!-2Zru41OuPc_#5rhXI+djFMA?3BN{<>T910F7 zSyI0|8d7O^WOUCb^^vp(SFG;|RxiOILz#^U*NiQRJQ>l#q!NQd#JC79y@qC@!UjE+ zYfxX{T&`F(B#ssHWE9{rc^cpo*s#ku5;Lv_mx0+j4C=F};K}}h>04evBz9_})1g(Y z-|{yWG0p=y%mAs+KRUJAkr`VVV+Ygabe*K+fcNbIXQXhaeE3q4 z3x^%W{-TTdFIvf6iMUKmxTa3zax5Rc>U_`UxRw_Thha-2^TO~qd_L5SEWBQn`mJ|+ z{EwhA&U|GNuc|@AM;mA;nFdkudgIJ;G|wF`<8V!Jfc_jW$~%cy9@EDI47n0uH+?fx zIuX*hSC>SbbKAzOr^`w~CNSGqcaUhN`PTEhB=Q*FV_e0y5>sIT?N{!TPk5^AVR$`& zu8&p5tRrVsm>&(=OCA`V*ZnB@C4cs<_sc{j3bc-KiEQhdPrl2thmyq}l9G6}Q2KB* znFtm`hWwA3C>|Zb;7Rm}5_BcRQMn+7p<>*ojTFl zKb8*MyFL5JhVWaai;lMp9b$|o0K6NNE8JtaAyv$ua@T0L$+W7pvYwb8juB{Be*FAw zg-S*s-V}MT+=rLW0UziqX}q(+!^g()(p+OHz=^KGaHpGOh94jt3L*jqnoNq0JW0Dr z&6UkcY}L0>Qy6su)vU}NrP$7E(p>o{>T%IPNbF$cNS zP<-B8>ub%+k$quZ-=_4IM(#MAPX1ZBZ#hpq-ry0Usu9bav!p3$RnQtBTA$hR3ULy_ zGEd@P@elmDvCZG{^Ew5dkRPe+4?iRqn_240fouM&^%R8?!vPptS_c^|%3ed`Ad@G` z8k2UxGFb3JCo1Htd~kUYn{^cNuI~|_&A7bzb7zPBgjr|Y;_ z4mXlRK2hIR45pDO&$xyBD$muzc`wt8<0G|E(K@NBuKjhNyywqYGzSWrTlnd4?r0M1 z`oO%lXZz>%w|lfvXnt7$y6c|Eb>2x7npX^Rd3a$M9|UUic1xLbeVl29xZ5F|tJkxf zT!#$L*Pq3SChqnIWqhBjEbc_q_BU58MGm%1m*I|E*zS#v7kBJ(s2vYdcDfA77Rwmv zezSJ_s(p74dBpjrpU6nal;NV%^0Db79zr_^yuv3LM85DboU|GijsM&J2;*mW^E>cA z(ZcxIj4SkCk_+z_B)>K@7gbL4h4HhD5f;V97?_9b_=OpUm*SG(Dljuo-e#bd$Cjil z%P|SucOq_5*ivod16}FTxm4S2dX(p31|OaSC!0eo1CZ3_r?2{95GL9#(?D8Vv@gx{51UtqnOTN=+a>f3AzDLPv4yhCAG$=DCB_6 zZn88H;|>vFvLGvZA%-ZK14hm3GuD;V?YN^DvQ1{9?n;wq*_%^8Hqmr-7O*s|sW zYxalp{@a@Ufh(Q{SFgllD`?^OpiL9VXP7S0NklM!>_al}H0;1Si3N0@A|P|cZ`7H+ zg-C_r5t@B}W&nA4(W*q}eQ?6?5%Hn0WE||qHG{B}%}S?47e-+X>GbLJ569O@bcEUm zjb6;o{+9To#>RWS9p%8)2Zr-G%lStf?j|hm7c6p=Kv~XNvCMKE9ZCGU+WVaS3~T(B zNOQ$FUI<=+9)aJ$kia7VeY^-A&A|a}s#9noJErVjv;xQQ<($tuPfDbdQOJi|CM-~RBpNaZW8`x0<u24%*UU z7oDLmbvdVo@skCL`{p;Cx#0PB^zxmDft4Pk{!=#1_XHPO_$8R^?pFV2L;GYpJBa$X z`Mcp`{A*U`11Dh5cmj~f-2?}4NZuHx4+Tt0iYcbE<>fVD4|~W*v8;MHTr&OmAF(W_ zDv1Yb_;+ZMsac3)f z9lLD2JVtyLPaOV{cSf*`l_KcU9t`Hw?&0dz_$gu__FCLp<0mI;9es@-!iTA~_*ogR zN)`GqtTHJI`?ggZi_UWGTjEvs!Ey9F=|T}207>-08Pa`-!s-5j2yKA4{(Om}+787+ z_pM?r5{W>(d5fWlduNz-)z1?;f4#PM^n1GJuO2G&#p&||##YH;uDBy~7AhthI1*d- zX+lGObKhHrPFy;rQ*`@t>WKd*@1>OrnhK+k_I3VA-=8)GnrVUhD0A^E2?Y)rL3{(g9D@@d1NMcSRPFK*ch5|8c) zCLwd4g0Vwp9XS#C1IiS6Wqg>D3LQ}ICZkwK>2#Oyh+(Qnb;a9BXaU?s7 zZ3qxd!)`u@ARbpM&O3Dsb*0>(+HqIueCgi$VCGHk4b$Re%8awmGr&FrO+;1xCH;^f z_dE}|C^o9}y8DQ{D@XTgkKIQHwVF76|BpYSAU~?R$ze7YQp~T0wqnjf`>1*lvs1~` zj!_~k=JSVA0C5NeoZvt_eN(PeoI9_ukU_Yve@bM78v4)y4aCB;yb<59qi(~F-S&Ph zZ3VB}c@C+E?m;f18P#9LERShluB5xU1Vw3aDf{vZTs*9u7rm>x%K~cVjvkC?xHgie zRV)xt84MAx5^H~ub=hi-pF98oSy;k6 z&*>%~bF!~WJ2S>4KxQ@~B&P;7&)tN(Ez%vw|a(1$w{vuK0zNTM!G0G zyJA(j)6R%DLkCk(5iQdF->i2AKOKFtVO7<7SZqS$-ZzWYiNNK!+?yJZCG|G6Rb0h* z9nk)7ekekIR(6i1qq-?+%>g*>#0SYmSC&>~B~!=`bXbf<-a*ZUo|g@qyrZf@_TWBY z@(>BZc^VLgvK>-qsaeQsBSi>wi&9HnG^buS!=4b_?9OLWo;zq|Hn>UW6Fyp}END$-E$3T_{IzM)-+vmivz;XU z_VPBqkNE|i%r+$XQga~vfg2{7V3JU*JCF!NFEtNI5bML@)E~1@-RV^XdOBcqkeO|f z%@|}*0`B@r!$5ng_vx}1*%C1E%S}IaH~4H_K6i zAJn~Z8$F(qxFzht?wT~OWA)xkf3{o`3<(K2Mrw3%AggyxLxHrLqz^nCG*$FyqXF+b zWDMG?AwqeI0F5UJ7UGnPdLP0Fxrusm+aSA^E@s4_yDl9-w;s?_yr`Iy!8W>`w0>|F zrz=tWSdUq#>s5U4CjIm&+Q{u}HyTbQG=zRDU~IxZzHfUS{?DR>_jkQRxz?BBEMq^Z z?!JT&F5*%71YpUjwVwfimc6aU?uSs3kr2gDXe!SFn`^j@3vL*XdN3$uILaF0Yn}~{+%&%9_ z-%~?%eZ-0nS3X*|XV;u);OI2@!au{~PA$fJhAi`XuZ`|XN%%*VLzAbMpH-PrcD-4k1W6TW9o}h^LZifxF>A9Snb4|TqPul zK`+ZGL0}Iu;!!!w?SLo)1NbqVz+OhUvffLKHsii~ep#Oh-FQasPRP(QrNQ?A{?oH& zvOXAm(drY+-}=yF1K-1$+q;R|cpCtZ z0|4ab9qz^xSyagJm?ku9YYlxy{W8Bo?Ge`IH-ktrU6QfY1$L3~O}c2DIS_v}!21b5 zQ%MHq;>1-u*EUgLX>bXDXcN*|#5Ua7?)W)0B~0p#*&X1Vd&LC(rIy_B3J&Y98Y+)m zm2)B(>asm__&%bxIpz|wuA$_41om*>i3RkVPIaeiqp8|-2-FOJ!r2{TBT5W9jCm3A zi#bK_+ITg@3E@cJ1n#6)F&yb=VO%c*{6$v~ZHtyEXEM7a!CK9opl;@7nXO^$zof| z(AkEA3-xc9X^jr*=4T#moO~%*(B>H+0(wwNmmig2u>5YLwfeEO|G(lko2|t^1)p?h z{>~qMc(dlev`;JZh4zOhi6*umwm$IUE6S6{L6jmSoEc=0$`FQ!j34NzcZ_nVw9DJj zCVd^lTy=}=^f3w@*veJkUGnpj!p``IOSkG6;$78eZU~=Zr>0{jKFB)RE!EZZT~0Pi zs<@VlZcLU;StCYitLLP$v#LFvgR*O9vskp(OLK8)TGzdwy^D|7t8KA3Je!F zjHWui6&475U!g#FVSN$av4WJGSqq`FQ4j-o2Kg1Mm`qTIptT)lFk3Ak$`T7NtnA+` z+Qq70Y`g%6JGZl9MMW2bwPjH##^>jaH>EwT1AVN+I|5#{33kmzvGbWV$4uR1%SEM zan@s=V*W|sGkj7bC6TdMcS-IU{Z|9Ns43mMxDt;kg~i2MZ$)sEo41pO$aXir2gKHn zbk&LmbmS7ZK?8SHvPF{dy%WtYN4|4^9u^cFDoNhDu;KL%;Z^%F8ZeFQ%?^5bJ+|eH zYNx7f9`YU2WSC!%kpdaD*hwM=2f+XKUkrHHM)S4&^Px{>Kw47(E_>qDTKp`da&nCy zHUKgaX4XYp5%LZhd81f&1h^#+0p$DK1{223QycZ7it6OQB^^ftBbR=KRBRr6>9pJt zbF*!0FKf;-D`|(sF6|rzeDwCDl}}AWX2MiL?q8Sh`cGTsZrU%L=Mde9RUayccaK~Y zdl9q^M1>HT$wAthLt(#|SLq2tQ&+%np-VINxd4NVFCv34>5NvkdLXF;**%je7p`&% zg#_NwbMLx1x*K#=L;Qi!qb44O3GkHVb!%W0mtFa%hfK_`3O>5!RC(u* zxE~BvIRMJ5Dj`Z7!%^M$Yh9``%jeJ4+_PACQq?(o!*;FlQwbn+EUv+STaG1$kItml1xRjwA>_xEdA0l{|7&A>G1AVC=k(QR zs}`_NaA4l#Ys}lU3Xjiy$QbO6Zhh9*4yhJ}YibTVNIUQCnZ4HdS6iTWoOkS&J)`tc z)I^w>b)?}&M-;EmO2l#FW9w9X5l2a;c91tv4{-H0U|i9yYQ5EF=O-(MNlr7lPU4q_ zwqzo+nP;PSRj8sG_p7FSWTu@dA=ES`i*{xfs=ROKDk^i2N8aeI^Edo)wf5AWxj=6> zx7RnhieOB~D-mw{W0`c`5iy{nAFSlR+)~&PjEv_UglY5E0Td2G2E(HLc_Mm87QU_V z@}6*$8$*6g^ocyZOZ3t3xH)EP1Vs?oeXOc6-mwIUl?_Ydsmeq$a02cUcJ1 zE*yM4aOTm{kqRsV0u4WWZzf&Dei-(j)4-W>O;p#9pPpkxYo97L}jmf0K*X_TI1> zd-3}(ttJxBA8?bfUN3!4TQKqa@LTnq%fi|mVU<(s|8SG&|HTg>5bi^$o;EF&l@XyN zldF+63O6pBZxmsp3lWeM&`yw&06G39iO~D4Lv|aO8eWtg8&{{cp10_0`}xKEk9cJc zTUN9Cf`0$z&X_s8@Fd{{ocFn(XzxshggOv17leZA+A}hP!|#enQ?T?Q6c~?j%=kt>#XQ}SzdhG_8Rp2g zHGd+kaI2B;_KS_ltsVj8cB`x2M4W#1XS`ZgG|JI`%Y)B7Uk50NF7GH@XyMrjRQp(P z7upfbI}m$F4h2$Fw1qXAR(}?a%LT{g|EvPMAvhMbR95Toylb(hIMC zNuxg7Wy2VRSBN))6V6nO$=^D&b5#0v_9?lJ-*VAEZ{0D|bZ77vE7o%=i=I@tTugc# zn`^W8>+|pT)_k*I*#%JPXmHG{7un34(dct!%C>h#J-Pv2=5=D^wO~>8K0GD!;VF+N zimGauf}cCHPMW1HJV)M=RC|H6+IVQdCA7+Km#SRUvNaptx*Muc8Q)Q*0&&IQ;*c$5 zx|>ao&lO|f%|&I#Zcbp!IM-!R_LI7f4qE((pAy@*p|_jIB|slP^cq#Ke=;6B`|0%H z^N%|Olfu1i|EJo&`Prp*js42fb8F3snbcetQDV#+$(Tke>6nlo)&JJ~fIx4SuQE!1 zmUX~}6A3Iaml`S_B19Pt;-veqUAh|&xJUnV_i-zb>?lhdefBCVP@u2zF7Wh&@)uzV zA85UC3rEzq2=DVGq=t}@90(!H82TV)2B0lK3F3(w$q&f}JVhkH``N4{>349Od(wC8 zyD;y1EAP2RC#5~Qv;Mu(r0#e9rw_)jIBjsbh<8IdAB{hT7l$vg>6#qiOjK1N#HLUe zeIM4|zacp;?)M~t63O4w(mLhi(-`9KPd&$Z+CShOrDU@V8+EV?OU%n5 zYD8_dq!vS5K(425u|$nE z?3FWPzp&t$u_yO^VZ&2J>;w0RREC;4yeIsbLD`=5{OM30A};+^O-YLyeJ!4Xc| zcO(11-$yuU+0U1^F}8_+d-f-4!u@!1lhQ4p;O!UD=3R7S>D}cO$(*MM52_OKauvt!d=cE%Z2MQp2zaeOk(VOh z%MD)c`{5cKAr<~4(tTbO@mNkAYL!-kUJ`cepZ0!u91&Klf1wG~mHU$&J7KA^__6k> zmhb=ka{fQpTVNLw>OyWoRqlmJCE zbHzUZL!qle!A5Kc?r|EhKm5byp1a)h$Ik4|?w;@O_xsH6aqjHhn*4Hg*E+2EDKX^A zlGnSryT_YM4N1Q0P%bZcx!SL#a$qv)*Vu;*jhk9xN(*1IwF~9j_{Yi2OhEr??Y@>f zhGZfq<-X5(=c{2K*N%K|rL7(&hY$DUx)r_QVE3S@Gdm=RpZUCFrmV6wTRYL+px%G% zLfP8F97%*YVMSD_yuklZ`;zI7DI>e#ABNAw>cor-{4)}a__2Zfly7!*ROgZVk2v$r zI%-TF`V;vu|Fq%FPhNQBFM<7!injOGeR}!*$~@xus-fEhiS7G}8~?Jn9tz@mWxdOi z3fpo=;xx92>s^};vW{L?AQiE||ttB!T7ajh~cNzXd6!-t!H->YFp(OjUsxVNY% z+`P`yShnYraM_gWri?=Y>-XIIdxCgIPS>Naq4)vA#vYO^FIQW?GBPFEnLF;HF&+&h zlJl(9t;ey>KbNn2pZrYfu$)u>&YH+UjiHv(c` zT&oZY(_49DxJ&oV7shNuk}oxF-3HEF<9V8V{=qVJPqR`tD!cNw(E-y+i^|uwrv+!f zDSFDi*tb3D0mM%k?rkQ$n=T95< z{}_63m(XIFB^C2XCiossX0~5OHp*BBWSb zarwIaBVO8=Zbos!>D3UdrRt?P#E&&l5)OdN9XLSpYXHcE_wVJyyMl16RptcNcCVh8 zslr;Io{K~L0N^pp33tGf1fp4!=kEW^C!$nEr^TcjSVL;RS*{s&sg z5mEnN5J>SOoULDYubZ)7<1J9n)ggXNQ2s&up#BHoU%zCES{5K08AbdUQ~3t~D*w2k z`h`C~Bk;6=UG#YX@niV7|DpI{PX6hp&voq1&;y7cG`~O2Kd66ZK>g}h)Uk1-(E|(k z8KCly4Lbh;*!Ryp?C3=!Jx}Bx#1E=pJpAXczM5iPjFeUvF6zxu{xLxP^X&OET+vkj z%p;O+|Xy(_i z1^n>wuU}p{lZmuCaFK7mfFF4MeK!AyVg4b2p#YStT=900WzMh#-U{_w9pXnw)h`b0 ze;_|%3)nvs0Px`X&r-bY&q&2vpT4X6@BU{g#Sa6{pY=UHxZAg&et~)O7t0Us;iFw2 z%Ma}#V6*_1A4Z3dcJVAfw1 1) { this.logService.warn(`Multiple speech providers registered. Picking first one: ${provider.metadata.displayName}`); } - const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); const session = this._activeSpeechToTextSession = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); @@ -127,6 +128,7 @@ export class SpeechService extends Disposable implements ISpeechService { if (session === this._activeSpeechToTextSession) { this.speechToTextInProgress.set(true); this._onDidStartSpeechToTextSession.fire(); + this.accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStarted); } break; case SpeechToTextStatus.Recognizing: From 95f8e9de34e5a3b95c75257edd141d7dcf62d7dd Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 14:24:37 -0800 Subject: [PATCH 128/228] use voice recording started cue --- .../browser/media/voiceRecordingStarted.mp3 | Bin 42112 -> 30592 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 index cc959d00a46cc72bb6fb6a6763e36d129c5e317e..3bfbced34a36c38dc0e173ea5def13e0aa219867 100644 GIT binary patch literal 30592 zcmeHucUV(Tx9+6U0)!rVK)QfCbPzNYsX+meCQ3k>G?Aj9C<(nP-B6S&f}ny5VnITY zB1KS8v7!_a8-gfB$PN1Y?(>}UoOAy4e#guF!Jb``^{#i`y=JYMU}2&T2h1jU#D4c~ z0Qdmukk7#bc$<(zcqcpoQq@q^G2UejfG_Y0@;PW99<+0poi)HiznZDef9j|cG=5)E zCm8+i5TJja`Bz6>(@6K%Ep-io)_-~iGKIh|Ycc<_w%gjq=wH?#==bAlgnv4QMmv6A z`HcoLR#R8|zZZUWG&Obpd6xOef4sH+ng8L+Iwu=#gMkeOHW=7oV1t1T1~wSjU|@rR z4F)zC*kE9Tfxm?T#>#c#FJH7NzYgfxUkC8&_d%5Y`?&FUgMt5&fpz`R*YEr<)sF;! z?3f7$g)Ya#i7RqG?ewMYtMiUjHYZ{o)l5*T4c}8@UF-~S)fdL&v;3E>x!Db8rBQS; zA8SI|PUWuJqfaAh%kIV1;+=@)saP(ySTeXVtV~uW4P-iq9-SwzGqchD2L}F2{h%gx zzCzG^Kr>q*JrpzcKYaZE`7N!Q@d00uaLIU6sP19{0f`-QD$4NJ3(bjAyPHTb5&{=tuxJ{&qs~_0(Oss)bV( z9_0f%^(aZ>=HWMWSTI_;aJoJ@3x*LdaKWF7^xxO`J)G|cgTa{J{-SHogAr!0ee;*Q zPh%b&OT_LNj<@*6;xwCL-)Y@Etp!p)KD%;Sw*)Ryi?*BzQ!atUqF`J|i=4~*w(S1G zW$HOlw)w@L`%H)-r1(v{V$vhKJN5`Cez3cw(03{1WMZB9`<1N+HXWBS4|v~|9yC`B z=~xzu%gIFycfap=ASxonB}Y{ycaGP|Q@vfaHI5MbXhOj)XT*j>3q>SDFFcOVbz>|) zdFglUXbWR-RI_;y(hM)GbR}4lZ|0vcX7$KIbDqK7#K9zRxT-}lCtHx5&ECex7xPA0 z-$~=ZKncKv;^W0njvC?L?X1SK3(2~Avt9^Gmv_oAqRO68QquwEUJ$M?~HtCwIyHN zz|i3FgU8v;$2YH49xa^-G?)2!L*MhT$!KQ5W3>q=Rwy0Qm#u&`v^Y&IkaH?hIj72F zq)|c2vzlz5t{vyQyj$Shcp5vNN6FpX6t@k2jeu`GeTxDVKT?GHxCXe|n|Vu2>&}aeG$7bZwqq5WSJo_o^U7*S=zzcWN^%tM609f`f@(Vd~Y%hKsapqU_a05l?QbNcVfv+BgW9^)8T^|M^gU>ZhA&mMCDzvh=Q2oYTI#;(uonTy9 zi)Jv^PBLCIm300_681#N&aVq2AI&Wz_i^c)_jxsoc@%Yz81Mb?GCi;~x78W7P;GwD zj|}N&Jc|D+MXtgQK1FOw;Q6k}Z=ut_(vY`C-xiQlJ7KN+%GQB5u5zHiLs4TTJ^F4- zQN?A?i;mZlToQMC$!eBilE;A(W$1{0{#B@obH^Z&%|jr=ep5i&s}?2_CZv==PHEi* z-50H2p;?oe;Nj|+8Ph)X1S}G7yz3z5t=N{dn~SDRM@F2o1!G`YZ+2Hc(?bp&b539ZvK;hY6KfkykK+ z*;8zR2+iI_?vn=HO^0}@T>Wn^gjZ`tkpXA8=UQljJznEiB zJI8gvulHU>G=uT;(^-6V{L@U4bD8h4BRF|PX`%~+TS4$ptJtU%G90|2AA2aQpCzn9 z8Y@Ng|E6)9flQhuL8DmZfHTi7uQy3CdhsEQgj zG<8WsaYz?c4v0K%a+ngm^F8c$amo9vbM|^2U8cuW)|urKE7yH&Rv_m zeL#MTu(+ypd1F=Cv*(q5(~4(95tp3eJaV`B8PnZ3zYRTYy?_cfuzS=sI_{U8?fQiY zL0$e$He0NsT347cE69hmu-9xN3e{4uk5IvOkRTXwgd$;KEK;(Oczn8Pj#$y*^IK~t zr5t~ju3Q86#}1;uSU1)6%Zh9I`G@kp4&5y$|6#X*S5U=KDBQ=kue-j%2rD?+Lyindha&3>$lcaJ!$ zy$?}6VNQwE_xP=^a=%3DfKQRZ8k3(BcS??z&`nNqQ)DaJ>Krg6QTjNs(F@wa)4B^{@0rTMq6iMcVY|7d&ZO@Yc_ z%j%itegt)sgHT}-ny@G57U2p!46-7=gM>i}gri48IuxV=+@3B3`1z$`-{)AtqwJnW z`YelGEuDE)nI8W9_*B*Tkj5>C@<)n>?`nN|*RA$NV(<$>&%5(VBxB{tkI=4ryIsnZ zMGqa8&N;5@_v7xzZen><@ziy%*cK)blHss0Q~=5}cy`zk0!{8gw4_QkV%{3zD$VGHY*&)ayHIU-p}%j znNSvs+o$-NbkVMFO}w`{?e0Y4bG?uI@2%8~*txEsT!17JZp1Df8%yy%G8=8@%5!P| z4EI}=HcjD|uvLrTDV7pZQv{7)4ve9u5H@frAS4tl0wO7<$R3Ix;H>P9x;9LPt_`Z_dH^(ezmC8KxUdEGDU^L9^rUaLk`hEu-cS$>DUhjE|M7j+_K-s1 zh3FY|?eH_h)M{?QA7L2o(4x2g;T##y+K!{r;GzKf7NI30^>v#ub`uxt}4O7<~u<&MN^DnTK+f985XJ1pr+aNSGfLK_sVi zziKUwtx4TovGTwq`^wh$Jc&ykW4HUp4H#J=p_5v62Xm*J7G=-nCl*hs#tNFhD-*|! zDijM*E`}oPX{R~Re&_R33Q8X^F*I7`%z3leSekC?Wku@|Srd40`_Oqj4?8&*x1$jm zyxy<~6l(`Ll%kqWrJ!mKI#wOgDgXX}1)+lTGvsVIc#OW2PEfe8Kk@U0nUg<$zPP>Z zNU!6`Lj|hi&m9vP6|cWP_^BbWX0i{puL8hzqm%0^>zto_pO@^8W-wMCo-jK`!r>?T zUN@%n$=Otr*zhO&wL&iSiH$jsQ0w|hqagVR`msw{V^LG`CDFTFv4>rkxmz!_apb$f zRyBjM+-ZP_Aps&U22%$*DC`h|!X*n>>1a3`j%D6(7DaC>^L7*-y6a(iy7aV30zQbq zEs<^2iwf23bF=33o~rRuapCT}c%Z!Ujb_+=!xHhCml-A}*kdB!+%9MGv))RFUq6m& z^~QedKXHP;BNc;ewLy7~7^B46A0mI!Z-XW}5v0+#z@}+@Z+lsQ*!}~_5wzPkcvEv= z0=VW)if2@4$BK{oUEg_@MjW-8Pn&#Dnz!32yP$#pgHxAcnJ=|`inI1Y-DCF(0o$g# z0aBE}0(sjh=U_Nhuj9qu(Cy4@VMRv&hZ9t0NYYeJ%uw?;q(csHxaM;p22~o`KzPbl z2uZ|2Jd`Xh)ImEMoQ$L~%Lf|Am?{khVA&RIA32izh*m=!gBHqn#qDtBJojwfB&;fT z%yHPK$>jM`HHTz$UTRk@wJ3!1_7ffZ{UqIEPSbMf6`a+PM-KC0n=TO@1lIMk-o6aK zyjfh)R?g--BB4*~R%ExKzlA}MmNhHd>k+K<4uHcToGBToHxzpql@;bm;cA(JHF3lP zJdZ^0b~Ej&y?ho*Ki-(1eeCf~O|xFLxU_})P{D1M6X&`ZYoD&Hww(EK=IF{wVuOGN zitCg!#(px}eNpK_&gUQzN}!6(*k*r6#%lC0BqJYY3~VWOcxsx&2Uzy3a4F?G?`RdT zeQA6E+q!?|mEr_|);N+QiW6``p*wMXk?l$W-Ek()J)t-o(R1u^tg$%2vsG%SV zC|dwPUJgXjcY>l4Sg?t-gzeohj-G_e5@Jo;6De;>`M8$&ly%90)0nrrWwV>TFP)CR zGvnGau*o%a*RU-o)f-Jd!`1OYkMq2Q{U1Q*!{PDqMJ8o3MR$;uQ z#D-Yd`B7%PM}51_=*=^DU2Xp(2N=@_8N*una}An&{=KYE_!U(LbYV97VTGWNGnqfn zsYn}%krN&g-FEFYW4*RwOouP^&)YwSE+f9EukIxfGUJ4yA|s;4Rql7t3qy8D8$?2a zL=lJ=lt3cP->ASTY)A^4#0<-NNH7bdMh^-P+V_?`&3wiAIjGaqx9Op5vK^~QhWvm~ z)3VY*-a|Q~)fplJg<1!Dxl0Jo5{dgs`5NdDzv?G;Z>SbWD&-E*q8U%tk*r<5!O*U4 zd&ciN$bCwzU4c)gEgintyXeQ?u@6?)iif3PSPxSI;H5Em7?pwqNojCu2_g+mloG^Y z1y~Lr3jrEwoaG+^Q)6H}hkTErKRc*>s-Z=$9g$pdJ$Ah0L`(G7(JeZM$|TEbxJst# zV_Gq2#ITF=Jys{uezK%#-}8Tz^x@mF<@BR3En<~~BhO6SVLPYCy{@0r6r`y`YwTQh zaLUY6{E7J>?ALwl-1n{9HrJFIE?GBKC#1ovOV`eoCh()D>vp(<1RVRACK~Q%4hVR} z8#)gSf!}%l>D~lvhQL+8spAY@!RNfP^t~*kRl_u;%lX(vSf7UUM`q3S7L^e8*^BGn zUBbnidc5&V91Lcw8s=sl8FrpVHTpQM2t>84GsbKe23i)1=QMFU-k|bdC+6ml;SbOb z)8FAL0!g?i3YsQ2CQ0J~+&FIhE}A%i(cna+VlWvog-C**U*h;|n&_lVe;4<2=a{OT zuZ%I0MO{8sswy`_DTh`4dRXpf536sxo@u-c*R9+&k61Q}eJpO65aTt*r!kMT&(Dxj zyp^ZP;_ut?3ku`)jbg_3_aJQxk3-tWiv<||A*D2AFdkFbRYkJRkRfpqMbc0ZKM6+U zAn}%g5`8-VraY2MsrOhH)j~DppvkqjHrBKG&6()kUyVq9vR#zlnn*)GCBghg5Xohme z_&hRMptF3(HzeGU6`UceB6dEimW8*0lvWclpJYQJB`zBpMt$IVm(Gxt*!9qw$>N1C zU#>(4=`R*$UR~r1yVgvQy#HW``TZ$ByKIwlBoB8Bsla-FiVr1UWUoKFYq>tLik*j| z%uG_+z#EDJqQ}X|6xe{t2M{-aLtE)wa)7|p4>^T`6mT@*_~IYD7tK0;EZtRL$6i+M zx9P1#)d$jNG$YpLczHtho0}1+-Jt`y@|l6f#>wXi#>4^V&V5a%9`-G04eGwsRvefq zO446_wDae?=8K;FsSb)99Ex5!_TI*zhq_rkhx@8G-q$cU(_D0LtF<%t>X&21SD3F> z%at3(XEi1cdy3s=6$HQ>y8f8fI6zabvjcmH~t`$XZT(HVzD7qOblmER%Q@}D9D_s zO;O0;g!5AnE1d(91+Itljf#~O7mq*8D3`&v)V{U4>Gq_|=|w|`+Vhj&CtrQ8@_Mn_ z#hB0Pjs9W$`byEKir!&bgiA&Od$#c%1SG9Uz%8U;Md^MsoUM7~L01?p>v-JU&VHB| zpTADCiM7oazPX>~gV;seRo>P(X7nQAQOE-ak+X5wF^CCD0>`o*e@X!|(!m6=SQwni-W?px7sPA07t4J=u+4h2 z2kfVHQwbMMlE@JU!2WrFfyKg$;5jAe5(6YzqcsLgIqt5#xw4J6D_dizZF= z@`uZolD;38m#CjJm~*$c^uN5_Ph?_`6@*k!m9ybWL%X;{Mm(f$W4#*r^dR=Mtv_S! zHIqih!1BJ6`+ItEq_GAh4?!(XQI4tjLk9J4zdf5bBsTQVnXJNDIjwJB@JMd zj{|sg66kP{uA1mj40m7nt|e{b)O#w`%5zw))zIbI;SaIXE%MS&KG&XGTYC^$({I&s zYyX;F<@M%HZAc9ItjudaiTsX1jHc}-4TX(c-rD3nGe9l*pXbqZ($lJ zR>LkGVVefzts%+ej7g3PqKd6e&w;U{0$)QT<&Mi2T=n#rTe0`vTkZM$fW7;j4wkGP zgRBuzQ~k2x=#N3?ox&Y!pD><$T8Hxd($bf?CnasNe7JiNS-9>=KHt`p=x=>Dd}>== zVHo-$W{QXb0d*`9IFJpP#YH@Xq@y8rVrHcAEsMCOp3^0tQ^o}3^V%2t69+r4Dn1(C@in}crPlGNk-{MmR%*5>ZI*#RL z?vux@z0nzNXos52b^W9P=KR%xm`@E(y!bTZq)HHXR68&CXR8?5-}*^#9Slcvn+r}E z)o6CIC~$Zq0GxWK4- zUYcY{VA_JFJ0~Bx7PHv~>RXw*5)1cdMaj^efec*-A;S&;667X{!7V5i$W2=Nm7+2Z znxQ)_i3w-Ews
    6qIKg@@+pkFGsG#hBjg_T>K3@+pI2M)cX6^LI`SZ|k|9=gB!C zkq-?UirnwMIevr`0`JPM-CjN8xK0z3t(r$are2jmFXAFKko$Qw{SXZ3Jbh8^_9j~t z$Pt4n5MgM56|87gkPnEWD*+t63EV48sIy0GL+c#P21*$LSO+`!un}5XO&h~%glozZ z&Ek(s&5E1T74Lr2(EHdq^H7G0Cpp^}K_0jCq^Pj2Ib6M8GH&)!rn8_odOZzmVXQ9i zkN$b%J#+=Rs&-^jTs;T*74mPb(>S{K01D|sW}#dpvjB5@geL}lGfX5y0i_P>W7)$d zJUbs3CoH358M6@IoORzeqlR;_`?@5bSf+=}-m3lE?34uO9t}s4=X`7nw&XmUh~=#~X8zx$B=nwIqlOZ{0u6$8NSCN52p?vagx~06 zAV&$bsnGz14aZBAZ7Q8mN{o;wSKb~^;fRpUAf6P3oyqeR#%Ah_mt6NsHB**Ouo-`& z>J-&8YG?a3EH7Bfbhyn)XCfr8?lC6YUgDw4>E9q2Bg3x;Bu4S>VM*7$=sHdZgA+B_ zMPwcwaDCCP4D+qU*x`3z;K5pbm^#GZbU7J|Ah8Cs?>CZ=2wsbKisG~pCEhGEv=lNi z-}c!gj`;Kgm#xk1Q|-$=e5Ca8ikEXM4wFH~O)um{GCZj7`C!`c1{MAMeTJ_EZZ>#i z%k00#Trgb+vex)+`Oeh+&5-Lo6N0{fAED#k3}irGP?PvaKk^Q3vCR4_QR%78$=yNN z)xBuu{MEX~)X$HGCby5!L%J+8Cm=D`tK=aTiXjVPk*R$4crUjRhrUuI>2M+VcDAQwgMak{< z=g;wAXVcvoW;zVU%sSTZUxtsj*!mBWg;0H-yp0har7UUWj@V;9d+`+VJL-M14!BOn zQas35Fhe$^;ITk&S_cKlc?WWkjV4^JdU%wOg7Wd~xc=JCZQVYDv9R3Ie*zEnQVwf!*|};F z;2BUw>yX;%%68~A(BXya*TQE3mmDh{D02=m>n=G6q8!gj;KJ(o5x<=>bmg3an@Bv= z*Gm9fh(20|~EVXHT`uZ+UL~7aI%ygRiMPr?0&QExZ)K8H`?Kr#4+%^Fg z@YIu~SCX-y%~)+&ZN9bbc==Xx1@CM zHqVh;(&u|F^ySQCpUzL-tS~L>HDct#dgq*vwx34?(&SdGjMjj}dIV&)?b?Z?S=*9KZKF_eJ9r&3^_cLz(rc=Yed^@dgL z4@bS@UJW>vm{QvP#|T=n(F{$o#8Twv{Ysn`-hfI~mO$X>f`~r)Cx8zIjr=g2p45+* z7ca{}Bu#EB<0K=xTJ-qXY?n-6@F+h5ntkufsFFYM3E#(uR=10E;K9}n3;?ro>lY2rQVS4|MmH3};s z`y2<9N-2Ug=!TFqMG4}hXG5|SDGnfZ(Hr0qXe1dEFib@knIVRKNYrDT(2#i^8XwP@ zBK3{A3Z$B3oqSSY7=izTwA>)4fPvmv(;tFFUBuWxtX`>;HoVYEjvMdAZ8d?WZ z{`h6ohf9suSSL8kwWs5zVHHtoR_Ck1k#o=d>-f z>rV7ql0snO@;MD`SQ}1%L5sVM491Oh7(ar}INFYLys(Ics$Y)p&e=ZBnt{$1`zQYB z?`c`D-{aYtDY;q@kCekQvwm+KKl8eERTJuDA7?Q^^1*@;2tbC_5xGk#cjEy19tI4e z`q*s5MBQ(R@R&=MmsFz0RCT4PZ8r8M-CqSUs;+twKFJ=ZRJD6iNW=dV63(1p3(~}xBZc*V63M(A zPL!VCsk3$f&Ck9Yte41fh~0q9k_~qSMllw_fzh|#etg}bNRweFFGJU&XdQsoOjl?VpxJ2=Yk$;k|=kM#~_C& zyl@L|3=Ak65|`AqE9hJxfL;#kdCJMZ7p@?5SO^bOM6#NfQ8TL0FM?weWC^x!{Ppeb zJ(y6mE8Ch{`sV7VkoPqgtsA_{(BXODlCUavY?Dzit{H{EM0Pv@q5l z^dDR9J+1(~N9@t?Bx!1$pk5)c8)^~sCL9PD;<|ovDabtz&9Th*lkE4Tj+y`PWV+08 zzYT0tR(M5a#-IC}ktIluNJWDfm zan$^w-{vFV+6%RR1}-sVvUf&zGuHG+Th>l~U-Pi8m+yW9pN6LxYQUc(R(iurjH#o; z67zm4zvItBc#HAb?a$Aue6Ebe=coWam5?qsH_5-;le`8}UA3@$%<%bw6O!64t-|0PgVmsha z0y)RoLbVk_gvZEt5HB?hTF!sY)RQ?RPB(@FGMR&VgscGs-OxwKtN=;Y2ECZndPqm0 z!kr#C`_!60tFybX-x}$3xluAP8xs-m#`m>KNGn^vsG7n3W@8p7@>yej?sNWCed>B0 zABKwU06+LlOnm>4&m)`a~&v(^zIDNX+~l1IG&WaoT`B zFb3y%Ohy9{GJ!JOlMtrgs>J5s#KgWoN}>yus2yMHL!7OJ`}IAHFy zYVqV}ZLEJ{(_R;Z1nY>icSV=E?zvL!&32OO4Ezc^OP8OKl?#J$NiIC4h znGC;g@p0~sXs*Cgk&k5S*qvk;%tR0-c7uXHlt_^xx z+wJ?ccQWO}T)yCM4EW6ZH#4%l9M__Mv@MRllT=q3p3qKxLwN-1-j?|y^^Gd~Cc7#( zQx96%wa;q~yD%2kVJsYGXr!&(Sxl0wipMFG;M<6v-J zG8c7@Y!B1~fQ7g*FoC2&qWz{>UwOm1?Io-MJc`>Y@|0j_(m7gVtP*$081XfviJP$< z2*@0jO73-~U;iN=>1iKYGMeMZucMlCdFSJjF9|+QUay$T-yhat{CFR^b~$);Pr&*K zW0;(jlHn=-JlG>J^#yqgyf4V_j21Z*)-A?Tq(cJ2M8koZcMuW?L&C(1;Yr|-6gMxD zN)n)7k13NYca7sFzi1MytqS+}7RY~5;HtAiL~}^Z_(Wz@|Gd$)7iq_N+E6a1s4N6~ z5htZd?~w1b{qPCRraHl;D~y_7Rr|_zv!9o)ru2!@JJMmeHOzC^wlvK;N78!y$)X^A z9Q0!63WF2)yqr$;W!fUV*bsyFGQ3th%CDiKn%2lr>qW^Dn=>TR*GH<@#$h@k4R#V zwDPrkL+pZSuES>tWrRz49HuvqXTF~|d$_&#Q+{fYr_fk!Pm&Qw(wgmM8NQyo8hMI3A8D_zF-h{5njYI5U0+ zW5$v9(FLC>%VEA-u1Oreoy`FZ<`K05>-srGLF(*jTF-wHQl8+M@rUcI!vuH3r8cR} zuCT?{rg5PiJI2{+k>zk3U}J;r1Q~oXL#UG@M_`O>L|#@~hT~z~7_9aC2Ph3PE%0 zh{_!a>75F4Vu}~~)TD%B3G84GWXW8O z69w!L2eF;GiMmAzXjlT~?X)^WAOrc7ON%GkP z@eP8hqg#h0B5U5CdyzT*9yLv|JV#fHnXy*=j8xq56&Cm}CLEbe9I+vPPyFe=pziBy z7k}Md7R~RsHD-=H2>(gmN_k5r024AdMN6M6xDLrW<<3ew+1>d9SI-J3tJUln*s=F@ zn&>uqWTUmgYTL+z#V$s#=~AIX*s)3ZK^1?6M$GHh9725Lk>}kmGQDz-sqZklcH2AZGo8Bhl!wXd73KJ9l4kds$G^X`-zk8-f`}%b(0_cYUp&TYtk; zdu!cyiC@3KPMnYgKJDgY*aAGP=Iyrfs!So*0ZJ@$Jx{ zFN&oh=|AZHnv*b^Xu*oKx;b>=j-cZCSzD@V6xJll>J2hiByj&s!-J*uOI$EF>?s zr}q__OzHp5DF0q;zx4xfup87j0KmtzX7%v^=pbLADFC1;h4i|AZ{Lxw>X`I@Z-{?2 zx_|V;1=FUC0l?hRniXUED+7N8r!e=xDSPR7=IOt2+q3827~KB{(Eia65-^Kmrn?lu zStRp-g}(w6W;gE z+R)D*BKj{iv!S2AbYTCGTO0cMLqz|jW;XQmmk#V7a%)3Be~9S6)Xau{{?dW{LvC&8 t=MNG6mzvqo&tE#Qf5@#3{rn-K|57s>`uR%-_7AzWp`SlQ^j~V`{{R?qE$jdQ literal 42112 zcmeFYXH-*NxA(nw8X*J-J@ilw2)GkKiUJ8uO6V34l+cSH-G*MI&;_I^0Z~JhrXu#9 z08$i@CU!+pM6gg*P{jHQQ#j1 z{!!o`1^!Xs9|itV;2#D4QQ#j1{!!o`1^)k{fZ+FK%HP*w#`ybyn)&+xy!qEbnfI^b zf7@3teoCIZOzARC3#*>~?8>|8a=3`P&F;gMUWGTcWEjZ60RrfNye=MA5GT zsO$cwBCbY7XFab=(qvf`n5YEJ8jb;IXtd637_K&&XRd_U7OCOL z70a`%FXK#X1AK{a;&r0f;&q*-D_3VU%dW>8`y{IOjXh7yhV_qRKT+WCKK7Iu9o1VgJUkiKLsmfMP9g*jC#tBX#lo z7Vwhyjfd8_JaLu#84bT;)=0Z;lu_zpk|tQir*8LXj*AnlmVh%l;{?wU zt$*=@Tr@C)|7oXR|9E07wVWn1)A~hnUX+!3LY2811c??FB;hmM*ap|pN$_+G90tJ0 zrTY}5f+VqEp+p|*?nWMZ{M{^Co0q}uAmH7}MrsNi2oZ#v#23;jc;c(gdU8N+UN$4v zFp0y(WfU{$0X+J)yN?+=M8s1vIvEb}yW0iaaRQ~&Ct#1E>~Zfft2YiyTG1QwtPI<0 zMNdornST(NYSSV3{kRKXJ@NUaK-kJ4GuUR*w*p7#m1qj-l4m+}0M-ckQT~Fw89zy4 zd^EI`k*3cegggf?oU9kcW2O2+z3m-iTySLI*+Z5sJtvb9E>2_}4FMK^1=HJil{GN5 zH;YRFTnf$#t@uJWqcazWX_wEtxwrht(KmHd=L6Ga>1gk!Z;3m7&fn;&|7Am-E@@p& zdHWFWJrwx#)AH!Sq|aYIHGO|BoZavUWS}^oT#*4KOO2yW+MDEx`k*OnC<7eEa`{jS zdKk$d%U@3%sW^_p!R10Uf?T0UCc~W(X#sSpwAbaIABeQC^Zr$pfGs z>U3jaPuu-zACSz$Qcc>O%=!#?3%p2x(Sc>!eV~H5&uaU);n(ZG-0s7;blc_U+<7^JXJV7-~fHXtlRD>Ht1$|=JtWjG0~KI5dtp=gV04#1A-UCSb!;UJ&CS$ zC~u=JC86Ep7Oy6A?A37quqRk6$l!s1ABrw_Y=3Q3bN#UCtnY$!wVE_ImY6^E${liX zgjK@`-5Mfx<$YDQqbIlJx4v^o3mE#H<#8_LPujFqk6HtCRkoz<$7|zT2_;fra?NaX zl3)~ZXg#b1Mfe4smaRckCWs=W=ZRx@f2V;QqA@)=uhQ^$z#d||9mDRZ{PH!loy<7; zpyKh}lE-vB*U2R+H{hmsoHI%{(F$!@QIJkH!d`Hmx>0q-vM@gP+%Uq$vEpY)SKPhZ zaVzaZ#E)NBpsVu3=MN|v+a>y7pg#GBt5);M&x{?<1E1^U`{Q@j-dur+73@Jks?%6IjZ}`ZCN;^x#cvB1~f1RuA zEUUtv)mM~^rS-K)86L3tnE`IsAms9qsCM}@f#CCJXjnGnF0EG`=$ZblDCl#3&+6 zgoF+c0e}R+e*-kUjN2OD_f;c+pBaN6Zr(!c7f4mMKL-BrhOn(XYfy!!hHUBljZEil z0!-@0svSdaE20B}DJAP5V9XO0PUx7nP=X_w?cLi9pY%XlSi8NOA^y?G`Nz(Fdmh_V zRuL7nnL~BY2F|w~^CRsD7bI2Sn%q!K>abC?kWKb)0#LC3Pkk4av&FvU1>n zLpz(NR0lt88g9ZaY2RS9_55MM>(C8d`wi~Zp0;6m#LO$klRtfJi1t45S$E}^Q8fDW z<9b;I!uPwW@Aort(lQJH#-N{Lvm!T}OoSz41A@9q&%$pB`JtBp$UIBR$v=n2cGo-` z-tXZe=O%@Yo1JE!snB}#>QwwmSWp1Yo*v26rzfI9h26dB8(4ek^_i7)51>7Xn?PX{ z7Vd9#W&ur?d4wPh(F`o%-QgSO+sIFASZ%#;aCB9s)7PQvk9OaQTxd)4UT(K@eYHDH zM|ea7l~gD87h5|zDu-65^QsWU4#?%1>)lat1e zA4ML;p+P%=VD1P3fWhHhE4K3X9Ja+mc?shAN{zvXb#}?b1h~ADk`^Mskd1*v;jlvV zFgUU+3E3iZDHWEUK`#vLF0K|ar(L8Yu&Btk5@*%qF-iwu?uj&@t~#kqUBwUodU12)P94T z(K+u(X5DXM#lJ+1#B*Cd{s_w_<3oZs6CwC$Tr9hBi~b?A?|)u7AM-!ctt#bU z_sLESgTwCe5KLYx0pI|THW7UI&%_?4zoT}@Os31P^{CG1zrr=w_<v63Oi{e-3>J zMD-dcAp_(bs92A$&4UF>+WD{L;(zJuib76~;$tv?4yUQ`$TTY`vAY;e$zp)5x|}~J z#*WWR!VM;qvIm|=D?YfbD)i{&5SaQNd#dXf{RwvE{qK^o9>DNkBu042J&8KwPXtoC)CRRob8o^~3Y|$j+`aT`Rwt zqA&-uoqdfbKi=a;Ev>e(u038JdfFv++09n}T=2cif9}03v)yOSqW;v?9>gWuf7l`z z3@rp;cBsQ9aQ_#UY-nqD2^##tmKxDw({lpZ@E%h;1V_wHgo91s$>f1Ecd!lvv5y;6 zMjv_h4cPayYrCjeI&GBa0`ltF_rKdVNg=KwZ^xAd(V4vXkU+&_RxU3FB-H;lZ$I1E1#WN*izvG<9Z%#nnCPK< z*i3r}u}~c>KWMVw>+lh?ls@0VdW{dyKa!o!j+FJE9kdxeXk9EmBoO>~(to+sLFiP3 zZG{f_{6X?UNa{5v*_ULWb(~`k>rEL0jCkj06Y|Xv*lZ>tn-+va(Ju6vSB*)DIPS&z zTCD4x+dGhVrTId{-jB1-!+#6(zduObd2H9<@YlB)tFOzdUY6gKcvZXjJ54x5^Q!0v z3e?7Hwj!gvjWeXzLq{7`NR6l)qJu)~gJVx0#?MA1WJY`WFa_xg?srNP>|-!6qZh3NDxiWxi^b+;UO9o97=Ixijn zHc9jM{atdaix%5%`P>suS+KwW*7NACwsC#Id+;j3xnn*9l^S-6!J>HWtmB~k-BFQ_ zt|N#l(}Z&go|6~7u5WB@D4f*SOPRldG`C#+oZ+OYyH&enlKvdQuGL=Mx3YAh^wiH! zQkL6hJ_g-+bLO?+uNu(<&f^1b6zxK{UL9Bd7CTPG71&|R{^kcjGA-KSe~QO~!u;U- zb7}Ivvna_qSLQM4@%*pnh==SD&jR5v74s6w9$7Z98976dm>>g(&*qwx`{sr*qVf@% z`F1{%wv%ibDF*evJ*cSgB&`H_)+^=Q%le2Sr|cP$RYck*3{nMajQB33Jh3WX8|a~TLN5yc9(>2lKADc-n84aIKP9MG zm3g;$$~ibR$@*oRJ!`D+beu38(BAe6Abw+@w7Q?#v+EXZELc))p8% zRJgH~Fr2<~&e*9o6!-{S*_mOa%`hF<-2lAHI4`xih5IV_#nqoRBWLp5qMXn5_&u0= zk@j3rQwxYG*~6*PE>g*_B|JVi zGnpk?&fiRleVfi6rD@{?eel*@G-=9)J``k#g^x+o)PUoUKery^4rpYkBwcbUJy@?+ zSNgzYNH?&&j6YLzd+gm_MC_#r1)SWbVC}*-45vgvwrV3L8II?13omaVOxUJADy=*| z{b|FxaNSkf-Zu8)nfY}COL2mpISMca!i@FbkCQ~@_(gRlTwq*Va~pXWo+JWfK@Y+B zcx4nQPD1JKKaq(vqHhJl3Y8LxCFOpcV6;kD2A zsJf#j{(Jt&-5YtM;utqpu)}8Mn2n(5QsDPJfq(cXjc>(GR&V*8CVYN`HiQuZTpjS_N7bz%OVM+*r<24MHH2#Ew6DE`xMzXCOAjDlrS(!(Kr+q^>C_ zBl&jm>)um&ZAZyX8>(U^zGy@)MJ-03sIayXIcpSfva7)BaLTpXQ#S5ey&pCT1Vbb| z{LbdrV~b|84oRd5A9)Kc}SaUj3}6%`#pD0dYNh=E!0Wm7%D-70zAu2 zzirX=LAI95ICdif8brBeVTUBzTs{F{?H(P=$=zrpx>t- ziyHI$dzT}Ni@p>JR|~=?{N6H83tpH`W_XPgSa?Xw-*ca}qIuT4R-Pu_iCL9%0E-s~ zoeJ$ZG?|2#p|6Juq-y7;M>6e#&IYc=b+=X62zrt^hq*j;udM0{&)nO1Rg{7%oad%^bM}KO()KKV zTh%*`GB*v6XQ<*;>993^&N7fq))e?k@mTCcLs9CMfD;F;SJ&d7@-=?s$Q-J@sQM^6 zEI6uyk%>kYiRBr6k=5la^o7hI4PbPj!kKtGcF?m1jhqdF+-mnj9BNOYZLBnc&lMA` zlWQePZu_C#UmKr}))cG%sFRVH|6o9LIZOv=O+PHQCtP#TCnCr`uUR_ivsu7t%kx)t z&n%pI@b{ATORJn8|n-gTUpoG;t;!@PKxLy)<^?Z235xarWTZr-wHbFak>b zJF>+eANeAE8qbSp4fb-zZbLgFYRoXTO&1&PzuQvj@am|s^08ZzR?c@fO}*JsA`nbL zHcU&?^Fy-a@LV$}@_~u50+2@X?DjU%XFP-A+BZP<3=|{+$U`uSH3VnBw=oM}sgXuV zvy^rPohiWIz_lXoETK20`CT>+A836WJb&Yj1p3+@i{=KZ<*$>qBo{<>?`&P&QqQ{>m~uz z(3JNb=qnKNLpi}fn47n=SG>paw%{pgu>n%(=V&26ERD(_aMd)P!SAH{;5TzsIU-zD zJceS%5qcq7Ve8_FodxC= zP8b(;TYhl}jDFi&&F=H{p(ux(u#?>({bi}D!;jn!^$H9YE0@CWE(=x**dSzJZ2kG@ zu(b(I3xi<5H!xs!5(W+PHh|19JWtIBhy+UjZCDME4QRo?Xv0rR#7lrf#Y&r^?x*S`46O6l0Ahp6f^ddIcY z%5ydhZ}R(65i}a}CT_}jv2H%r(nc@>S=gFDOxaHIUBCv~k+5H-s6P4ry&?#6R=I?F!L8%a;CNJ4c|Isza1rWfywwMTi7cqE+S$ z1#$PO=`P)KH@C?FVQ9}vCxv5yyR(#{R_#5MTH1e4ZlzF7`9Su1_Q73~ii#l7@r0Gzpv}4F;i^yqz4)D(TbN-UhDn zqS+K%hA~N-_Qim5@m*)5d#3XvJQog7w}bo!OEdViiL`%X(_QZF8b^tEgCV!R5|!RO zDqKz1cE7)bjBe43D;vV@%!7}I)-N?LW%d0PRXSL+@-rHuu|&x`J${nzIVwVKkED#( z_$g)}Y|Vx7v-jB1Zag6^KL9uR^lyF)E4j>7y?7XS4(cy9G)}jbgX~yeAO#)?Lh$?{ zBx5(E2aH*nqa%X@Gyscn=*2?&n3hQ9_RzjTbkPO!>n#f!u$};`FctS3)3)7CsjwLEk_s^$3^0G;qP&QTiOkc%*&%P)b z<38otpuErAJ{oc(vCo&%KtnNN`s8O+oP>TxnV}BBJ5o#(^9blmvORc!WRK3K1LAB( ztITqcXZ8T;=?%^ORM6Mow`4B#(yUuVgbuQ6qizWWA58egKx@-25A@j?+9ptA+dt{A z+wcA3erLc??W&Q7#qC{lE4KtID%p@5wbZygR~LG>Wey_XNaV}RZfIKcw<$<@2wB4N zAay_o0w_VUI(a|?cE1#yQ8-4SfZB~2_s#K_A$yHI3mYbhl76{&Z+BvM=We`Z6&+=& zI2&u4S@C?R>9uHZpLy%8b(x%fPb5b2ad;%nZDA*Y3YzZz)%#l$r zi35U@i^#Y6jGt5EqX?;HW$&{&BY2d{nLW=^RJ1mIx6-_ZI9ba*=e^~W#s>SFH0$ik z(RO!2^>kWlrVsvkw_)haeEui@;W*M0F7Gmk=zobGdvHR*fY)!cu*Oe5fH2nQvsVt$ z*%3nfBjktv6eYAj%wH!p*6dG@1n{Lp4c|Ka5r*a?&=B7Mi01*IffAg_Bx7e^B0AgO z5yV!TX}$$S3YHY!qW`jKG&U z&@n*ZwbHY7IY$@(%dI}_WnnK`%g)p_wOMS_zDLiGUnr^9peceZ@2q2zlD#5)^90*@ zbE4-V)-BJ~rl~Z^TY7N{ha1yKf6n`_=}?aB8!1p+7`UL4%<1YcBw(xR>FKm#3RtOq)2CDQZ*C|N58Lb6b2YH;RNZke;f@Tjy;+?C5=qE2A zf0hI@4vG-3F(xq;<`i#=_X(B6T2}S_xqv6Z0aH#VC`H9eS*tf%UiwMaCcpe!kih%$OD5 zw>{R6JF(>RG0?wRu$aO@QGJzy8ZGgUGeht?HL2yfyc~`n%b{ZLm@!@hI43L8M}{vk z(BLfNhZ6sd44l=KodxRv9%+nBw1TT-BBTbZ!-2Zru41OuPc_#5rhXI+djFMA?3BN{<>T910F7 zSyI0|8d7O^WOUCb^^vp(SFG;|RxiOILz#^U*NiQRJQ>l#q!NQd#JC79y@qC@!UjE+ zYfxX{T&`F(B#ssHWE9{rc^cpo*s#ku5;Lv_mx0+j4C=F};K}}h>04evBz9_})1g(Y z-|{yWG0p=y%mAs+KRUJAkr`VVV+Ygabe*K+fcNbIXQXhaeE3q4 z3x^%W{-TTdFIvf6iMUKmxTa3zax5Rc>U_`UxRw_Thha-2^TO~qd_L5SEWBQn`mJ|+ z{EwhA&U|GNuc|@AM;mA;nFdkudgIJ;G|wF`<8V!Jfc_jW$~%cy9@EDI47n0uH+?fx zIuX*hSC>SbbKAzOr^`w~CNSGqcaUhN`PTEhB=Q*FV_e0y5>sIT?N{!TPk5^AVR$`& zu8&p5tRrVsm>&(=OCA`V*ZnB@C4cs<_sc{j3bc-KiEQhdPrl2thmyq}l9G6}Q2KB* znFtm`hWwA3C>|Zb;7Rm}5_BcRQMn+7p<>*ojTFl zKb8*MyFL5JhVWaai;lMp9b$|o0K6NNE8JtaAyv$ua@T0L$+W7pvYwb8juB{Be*FAw zg-S*s-V}MT+=rLW0UziqX}q(+!^g()(p+OHz=^KGaHpGOh94jt3L*jqnoNq0JW0Dr z&6UkcY}L0>Qy6su)vU}NrP$7E(p>o{>T%IPNbF$cNS zP<-B8>ub%+k$quZ-=_4IM(#MAPX1ZBZ#hpq-ry0Usu9bav!p3$RnQtBTA$hR3ULy_ zGEd@P@elmDvCZG{^Ew5dkRPe+4?iRqn_240fouM&^%R8?!vPptS_c^|%3ed`Ad@G` z8k2UxGFb3JCo1Htd~kUYn{^cNuI~|_&A7bzb7zPBgjr|Y;_ z4mXlRK2hIR45pDO&$xyBD$muzc`wt8<0G|E(K@NBuKjhNyywqYGzSWrTlnd4?r0M1 z`oO%lXZz>%w|lfvXnt7$y6c|Eb>2x7npX^Rd3a$M9|UUic1xLbeVl29xZ5F|tJkxf zT!#$L*Pq3SChqnIWqhBjEbc_q_BU58MGm%1m*I|E*zS#v7kBJ(s2vYdcDfA77Rwmv zezSJ_s(p74dBpjrpU6nal;NV%^0Db79zr_^yuv3LM85DboU|GijsM&J2;*mW^E>cA z(ZcxIj4SkCk_+z_B)>K@7gbL4h4HhD5f;V97?_9b_=OpUm*SG(Dljuo-e#bd$Cjil z%P|SucOq_5*ivod16}FTxm4S2dX(p31|OaSC!0eo1CZ3_r?2{95GL9#(?D8Vv@gx{51UtqnOTN=+a>f3AzDLPv4yhCAG$=DCB_6 zZn88H;|>vFvLGvZA%-ZK14hm3GuD;V?YN^DvQ1{9?n;wq*_%^8Hqmr-7O*s|sW zYxalp{@a@Ufh(Q{SFgllD`?^OpiL9VXP7S0NklM!>_al}H0;1Si3N0@A|P|cZ`7H+ zg-C_r5t@B}W&nA4(W*q}eQ?6?5%Hn0WE||qHG{B}%}S?47e-+X>GbLJ569O@bcEUm zjb6;o{+9To#>RWS9p%8)2Zr-G%lStf?j|hm7c6p=Kv~XNvCMKE9ZCGU+WVaS3~T(B zNOQ$FUI<=+9)aJ$kia7VeY^-A&A|a}s#9noJErVjv;xQQ<($tuPfDbdQOJi|CM-~RBpNaZW8`x0<u24%*UU z7oDLmbvdVo@skCL`{p;Cx#0PB^zxmDft4Pk{!=#1_XHPO_$8R^?pFV2L;GYpJBa$X z`Mcp`{A*U`11Dh5cmj~f-2?}4NZuHx4+Tt0iYcbE<>fVD4|~W*v8;MHTr&OmAF(W_ zDv1Yb_;+ZMsac3)f z9lLD2JVtyLPaOV{cSf*`l_KcU9t`Hw?&0dz_$gu__FCLp<0mI;9es@-!iTA~_*ogR zN)`GqtTHJI`?ggZi_UWGTjEvs!Ey9F=|T}207>-08Pa`-!s-5j2yKA4{(Om}+787+ z_pM?r5{W>(d5fWlduNz-)z1?;f4#PM^n1GJuO2G&#p&||##YH;uDBy~7AhthI1*d- zX+lGObKhHrPFy;rQ*`@t>WKd*@1>OrnhK+k_I3VA-=8)GnrVUhD0A^E2?Y)rL3{(g9D@@d1NMcSRPFK*ch5|8c) zCLwd4g0Vwp9XS#C1IiS6Wqg>D3LQ}ICZkwK>2#Oyh+(Qnb;a9BXaU?s7 zZ3qxd!)`u@ARbpM&O3Dsb*0>(+HqIueCgi$VCGHk4b$Re%8awmGr&FrO+;1xCH;^f z_dE}|C^o9}y8DQ{D@XTgkKIQHwVF76|BpYSAU~?R$ze7YQp~T0wqnjf`>1*lvs1~` zj!_~k=JSVA0C5NeoZvt_eN(PeoI9_ukU_Yve@bM78v4)y4aCB;yb<59qi(~F-S&Ph zZ3VB}c@C+E?m;f18P#9LERShluB5xU1Vw3aDf{vZTs*9u7rm>x%K~cVjvkC?xHgie zRV)xt84MAx5^H~ub=hi-pF98oSy;k6 z&*>%~bF!~WJ2S>4KxQ@~B&P;7&)tN(Ez%vw|a(1$w{vuK0zNTM!G0G zyJA(j)6R%DLkCk(5iQdF->i2AKOKFtVO7<7SZqS$-ZzWYiNNK!+?yJZCG|G6Rb0h* z9nk)7ekekIR(6i1qq-?+%>g*>#0SYmSC&>~B~!=`bXbf<-a*ZUo|g@qyrZf@_TWBY z@(>BZc^VLgvK>-qsaeQsBSi>wi&9HnG^buS!=4b_?9OLWo;zq|Hn>UW6Fyp}END$-E$3T_{IzM)-+vmivz;XU z_VPBqkNE|i%r+$XQga~vfg2{7V3JU*JCF!NFEtNI5bML@)E~1@-RV^XdOBcqkeO|f z%@|}*0`B@r!$5ng_vx}1*%C1E%S}IaH~4H_K6i zAJn~Z8$F(qxFzht?wT~OWA)xkf3{o`3<(K2Mrw3%AggyxLxHrLqz^nCG*$FyqXF+b zWDMG?AwqeI0F5UJ7UGnPdLP0Fxrusm+aSA^E@s4_yDl9-w;s?_yr`Iy!8W>`w0>|F zrz=tWSdUq#>s5U4CjIm&+Q{u}HyTbQG=zRDU~IxZzHfUS{?DR>_jkQRxz?BBEMq^Z z?!JT&F5*%71YpUjwVwfimc6aU?uSs3kr2gDXe!SFn`^j@3vL*XdN3$uILaF0Yn}~{+%&%9_ z-%~?%eZ-0nS3X*|XV;u);OI2@!au{~PA$fJhAi`XuZ`|XN%%*VLzAbMpH-PrcD-4k1W6TW9o}h^LZifxF>A9Snb4|TqPul zK`+ZGL0}Iu;!!!w?SLo)1NbqVz+OhUvffLKHsii~ep#Oh-FQasPRP(QrNQ?A{?oH& zvOXAm(drY+-}=yF1K-1$+q;R|cpCtZ z0|4ab9qz^xSyagJm?ku9YYlxy{W8Bo?Ge`IH-ktrU6QfY1$L3~O}c2DIS_v}!21b5 zQ%MHq;>1-u*EUgLX>bXDXcN*|#5Ua7?)W)0B~0p#*&X1Vd&LC(rIy_B3J&Y98Y+)m zm2)B(>asm__&%bxIpz|wuA$_41om*>i3RkVPIaeiqp8|-2-FOJ!r2{TBT5W9jCm3A zi#bK_+ITg@3E@cJ1n#6)F&yb=VO%c*{6$v~ZHtyEXEM7a!CK9opl;@7nXO^$zof| z(AkEA3-xc9X^jr*=4T#moO~%*(B>H+0(wwNmmig2u>5YLwfeEO|G(lko2|t^1)p?h z{>~qMc(dlev`;JZh4zOhi6*umwm$IUE6S6{L6jmSoEc=0$`FQ!j34NzcZ_nVw9DJj zCVd^lTy=}=^f3w@*veJkUGnpj!p``IOSkG6;$78eZU~=Zr>0{jKFB)RE!EZZT~0Pi zs<@VlZcLU;StCYitLLP$v#LFvgR*O9vskp(OLK8)TGzdwy^D|7t8KA3Je!F zjHWui6&475U!g#FVSN$av4WJGSqq`FQ4j-o2Kg1Mm`qTIptT)lFk3Ak$`T7NtnA+` z+Qq70Y`g%6JGZl9MMW2bwPjH##^>jaH>EwT1AVN+I|5#{33kmzvGbWV$4uR1%SEM zan@s=V*W|sGkj7bC6TdMcS-IU{Z|9Ns43mMxDt;kg~i2MZ$)sEo41pO$aXir2gKHn zbk&LmbmS7ZK?8SHvPF{dy%WtYN4|4^9u^cFDoNhDu;KL%;Z^%F8ZeFQ%?^5bJ+|eH zYNx7f9`YU2WSC!%kpdaD*hwM=2f+XKUkrHHM)S4&^Px{>Kw47(E_>qDTKp`da&nCy zHUKgaX4XYp5%LZhd81f&1h^#+0p$DK1{223QycZ7it6OQB^^ftBbR=KRBRr6>9pJt zbF*!0FKf;-D`|(sF6|rzeDwCDl}}AWX2MiL?q8Sh`cGTsZrU%L=Mde9RUayccaK~Y zdl9q^M1>HT$wAthLt(#|SLq2tQ&+%np-VINxd4NVFCv34>5NvkdLXF;**%je7p`&% zg#_NwbMLx1x*K#=L;Qi!qb44O3GkHVb!%W0mtFa%hfK_`3O>5!RC(u* zxE~BvIRMJ5Dj`Z7!%^M$Yh9``%jeJ4+_PACQq?(o!*;FlQwbn+EUv+STaG1$kItml1xRjwA>_xEdA0l{|7&A>G1AVC=k(QR zs}`_NaA4l#Ys}lU3Xjiy$QbO6Zhh9*4yhJ}YibTVNIUQCnZ4HdS6iTWoOkS&J)`tc z)I^w>b)?}&M-;EmO2l#FW9w9X5l2a;c91tv4{-H0U|i9yYQ5EF=O-(MNlr7lPU4q_ zwqzo+nP;PSRj8sG_p7FSWTu@dA=ES`i*{xfs=ROKDk^i2N8aeI^Edo)wf5AWxj=6> zx7RnhieOB~D-mw{W0`c`5iy{nAFSlR+)~&PjEv_UglY5E0Td2G2E(HLc_Mm87QU_V z@}6*$8$*6g^ocyZOZ3t3xH)EP1Vs?oeXOc6-mwIUl?_Ydsmeq$a02cUcJ1 zE*yM4aOTm{kqRsV0u4WWZzf&Dei-(j)4-W>O;p#9pPpkxYo97L}jmf0K*X_TI1> zd-3}(ttJxBA8?bfUN3!4TQKqa@LTnq%fi|mVU<(s|8SG&|HTg>5bi^$o;EF&l@XyN zldF+63O6pBZxmsp3lWeM&`yw&06G39iO~D4Lv|aO8eWtg8&{{cp10_0`}xKEk9cJc zTUN9Cf`0$z&X_s8@Fd{{ocFn(XzxshggOv17leZA+A}hP!|#enQ?T?Q6c~?j%=kt>#XQ}SzdhG_8Rp2g zHGd+kaI2B;_KS_ltsVj8cB`x2M4W#1XS`ZgG|JI`%Y)B7Uk50NF7GH@XyMrjRQp(P z7upfbI}m$F4h2$Fw1qXAR(}?a%LT{g|EvPMAvhMbR95Toylb(hIMC zNuxg7Wy2VRSBN))6V6nO$=^D&b5#0v_9?lJ-*VAEZ{0D|bZ77vE7o%=i=I@tTugc# zn`^W8>+|pT)_k*I*#%JPXmHG{7un34(dct!%C>h#J-Pv2=5=D^wO~>8K0GD!;VF+N zimGauf}cCHPMW1HJV)M=RC|H6+IVQdCA7+Km#SRUvNaptx*Muc8Q)Q*0&&IQ;*c$5 zx|>ao&lO|f%|&I#Zcbp!IM-!R_LI7f4qE((pAy@*p|_jIB|slP^cq#Ke=;6B`|0%H z^N%|Olfu1i|EJo&`Prp*js42fb8F3snbcetQDV#+$(Tke>6nlo)&JJ~fIx4SuQE!1 zmUX~}6A3Iaml`S_B19Pt;-veqUAh|&xJUnV_i-zb>?lhdefBCVP@u2zF7Wh&@)uzV zA85UC3rEzq2=DVGq=t}@90(!H82TV)2B0lK3F3(w$q&f}JVhkH``N4{>349Od(wC8 zyD;y1EAP2RC#5~Qv;Mu(r0#e9rw_)jIBjsbh<8IdAB{hT7l$vg>6#qiOjK1N#HLUe zeIM4|zacp;?)M~t63O4w(mLhi(-`9KPd&$Z+CShOrDU@V8+EV?OU%n5 zYD8_dq!vS5K(425u|$nE z?3FWPzp&t$u_yO^VZ&2J>;w0RREC;4yeIsbLD`=5{OM30A};+^O-YLyeJ!4Xc| zcO(11-$yuU+0U1^F}8_+d-f-4!u@!1lhQ4p;O!UD=3R7S>D}cO$(*MM52_OKauvt!d=cE%Z2MQp2zaeOk(VOh z%MD)c`{5cKAr<~4(tTbO@mNkAYL!-kUJ`cepZ0!u91&Klf1wG~mHU$&J7KA^__6k> zmhb=ka{fQpTVNLw>OyWoRqlmJCE zbHzUZL!qle!A5Kc?r|EhKm5byp1a)h$Ik4|?w;@O_xsH6aqjHhn*4Hg*E+2EDKX^A zlGnSryT_YM4N1Q0P%bZcx!SL#a$qv)*Vu;*jhk9xN(*1IwF~9j_{Yi2OhEr??Y@>f zhGZfq<-X5(=c{2K*N%K|rL7(&hY$DUx)r_QVE3S@Gdm=RpZUCFrmV6wTRYL+px%G% zLfP8F97%*YVMSD_yuklZ`;zI7DI>e#ABNAw>cor-{4)}a__2Zfly7!*ROgZVk2v$r zI%-TF`V;vu|Fq%FPhNQBFM<7!injOGeR}!*$~@xus-fEhiS7G}8~?Jn9tz@mWxdOi z3fpo=;xx92>s^};vW{L?AQiE||ttB!T7ajh~cNzXd6!-t!H->YFp(OjUsxVNY% z+`P`yShnYraM_gWri?=Y>-XIIdxCgIPS>Naq4)vA#vYO^FIQW?GBPFEnLF;HF&+&h zlJl(9t;ey>KbNn2pZrYfu$)u>&YH+UjiHv(c` zT&oZY(_49DxJ&oV7shNuk}oxF-3HEF<9V8V{=qVJPqR`tD!cNw(E-y+i^|uwrv+!f zDSFDi*tb3D0mM%k?rkQ$n=T95< z{}_63m(XIFB^C2XCiossX0~5OHp*BBWSb zarwIaBVO8=Zbos!>D3UdrRt?P#E&&l5)OdN9XLSpYXHcE_wVJyyMl16RptcNcCVh8 zslr;Io{K~L0N^pp33tGf1fp4!=kEW^C!$nEr^TcjSVL;RS*{s&sg z5mEnN5J>SOoULDYubZ)7<1J9n)ggXNQ2s&up#BHoU%zCES{5K08AbdUQ~3t~D*w2k z`h`C~Bk;6=UG#YX@niV7|DpI{PX6hp&voq1&;y7cG`~O2Kd66ZK>g}h)Uk1-(E|(k z8KCly4Lbh;*!Ryp?C3=!Jx}Bx#1E=pJpAXczM5iPjFeUvF6zxu{xLxP^X&OET+vkj z%p;O+|Xy(_i z1^n>wuU}p{lZmuCaFK7mfFF4MeK!AyVg4b2p#YStT=900WzMh#-U{_w9pXnw)h`b0 ze;_|%3)nvs0Px`X&r-bY&q&2vpT4X6@BU{g#Sa6{pY=UHxZAg&et~)O7t0Us;iFw2 z%Ma}#V6*_1A4Z3dcJVAfw1 Date: Thu, 29 Feb 2024 07:41:11 -0600 Subject: [PATCH 129/228] Tear down the Authentication monolith (#206495) * Tear down the Authentication monolith Major changes: * Turn the usage functions into a proper service `AuthenticationUsageService` * Pull out the access data stuff into its own service `AuthenticationAccessService` * Pull out things that make sense as actions `ManageTrustedExtensionsForAccount` `SignOutOfAccount` * Pull out random registry stuff into a proper authentication contribution * Pull out everything else that is extension specific into its own class (and eventually it should be in MainThreadAuthentication) * Have the new `AuthenticationService` return a provider instead of having specific methods for getting the `label` or `supportsMultipleAccounts` * fix tests * fix tests --- build/lib/i18n.resources.json | 4 + .../api/browser/mainThreadAuthentication.ts | 58 +- .../api/browser/mainThreadLanguageModels.ts | 26 +- .../extHostAuthentication.integrationTest.ts | 8 +- .../browser/parts/globalCompositeBar.ts | 30 +- ...manageTrustedExtensionsForAccountAction.ts | 168 +++ .../browser/actions/signOutOfAccountAction.ts | 54 + .../browser/authentication.contribution.ts | 197 ++++ .../browser/editSessionsStorageService.ts | 6 +- .../remoteTunnel.contribution.ts | 8 +- .../userDataSync/browser/userDataSync.ts | 2 +- .../browser/authenticationAccessService.ts | 101 ++ .../authenticationExtensionsService.ts | 418 ++++++++ .../browser/authenticationService.ts | 972 ++---------------- .../browser/authenticationUsageService.ts | 70 ++ .../authentication/common/authentication.ts | 118 ++- .../browser/authenticationService.test.ts | 209 ++++ .../browser/userDataSyncWorkbenchService.ts | 7 +- src/vs/workbench/workbench.common.main.ts | 6 + 19 files changed, 1485 insertions(+), 977 deletions(-) create mode 100644 src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts create mode 100644 src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts create mode 100644 src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts create mode 100644 src/vs/workbench/services/authentication/browser/authenticationAccessService.ts create mode 100644 src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts create mode 100644 src/vs/workbench/services/authentication/browser/authenticationUsageService.ts create mode 100644 src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 654a4445848..3c804f13816 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -553,6 +553,10 @@ { "name": "vs/workbench/contrib/accountEntitlements", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/authentication", + "project": "vscode-workbench" } ] } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 22c82811bb4..3bb8ef8fbe4 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,18 +6,18 @@ 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 { getAuthenticationProviderActivationEvent, addAccountUsage } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import type { AuthenticationGetSessionOptions } from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; - +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -58,8 +58,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu constructor( extHostContext: IExtHostContext, @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, + @IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService, + @IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService, @IDialogService private readonly dialogService: IDialogService, - @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, @ITelemetryService private readonly telemetryService: ITelemetryService @@ -116,7 +118,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { const sessions = await this.authenticationService.getSessions(providerId, scopes, true); - const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId); + const provider = this.authenticationService.getProvider(providerId); // Error cases if (options.forceNewSession && options.createIfNone) { @@ -131,22 +133,22 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // Check if the sessions we have are valid if (!options.forceNewSession && sessions.length) { - if (supportsMultipleAccounts) { + if (provider.supportsMultipleAccounts) { if (options.clearSessionPreference) { // Clearing the session preference is usually paired with createIfNone, so just remove the preference and // defer to the rest of the logic in this function to choose the session. - this.authenticationService.removeSessionPreference(providerId, extensionId, scopes); + this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); } else { // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. - const existingSessionPreference = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); if (existingSessionPreference) { const matchingSession = sessions.find(session => session.id === existingSessionPreference); - if (matchingSession && this.authenticationService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { + if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { return matchingSession; } } } - } else if (this.authenticationService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { + } else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { return sessions[0]; } } @@ -154,51 +156,41 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We may need to prompt because we don't have a valid session // modal flows if (options.createIfNone || options.forceNewSession) { - const providerName = this.authenticationService.getLabel(providerId); const detail = (typeof options.forceNewSession === 'object') ? options.forceNewSession.detail : undefined; // 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(providerName, extensionName, recreatingSession, detail); + const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, detail); if (!isAllowed) { throw new Error('User did not consent to login.'); } let session; if (sessions?.length && !options.forceNewSession) { - session = supportsMultipleAccounts - ? await this.authenticationService.selectSession(providerId, extensionId, extensionName, scopes, sessions) + session = provider.supportsMultipleAccounts + ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { let sessionToRecreate: AuthenticationSession | undefined; if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; } else { - const sessionIdToRecreate = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; } session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); } - this.authenticationService.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, session); + this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); + this.authenticationExtensionsService.updateSessionPreference(providerId, extensionId, session); return session; } // For the silent flows, if we have a session, even though it may not be the user's preference, we'll return it anyway because it might be for a specific // set of scopes. - const validSession = sessions.find(session => this.authenticationService.isAccessAllowed(providerId, session.account.label, extensionId)); + const validSession = sessions.find(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId)); if (validSession) { - // Migration. If we have a valid session, but no preference, we'll set the preference to the valid session. - // TODO: Remove this after in a few releases. - if (!this.authenticationService.getSessionPreference(providerId, extensionId, scopes)) { - if (this.storageService.get(`${extensionName}-${providerId}`, StorageScope.APPLICATION)) { - this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.APPLICATION); - } - this.authenticationService.updateAllowedExtension(providerId, validSession.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, validSession); - } return validSession; } @@ -207,8 +199,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow, // otherwise request a new one. sessions.length - ? this.authenticationService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) - : await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); + ? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) + : await this.authenticationExtensionsService.requestNewSession(providerId, scopes, extensionId, extensionName); } return undefined; } @@ -218,7 +210,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu if (session) { this.sendProviderUsageTelemetry(extensionId, providerId); - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } return session; @@ -226,11 +218,11 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); - const accessibleSessions = sessions.filter(s => this.authenticationService.isAccessAllowed(providerId, s.account.label, extensionId)); + const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); if (accessibleSessions.length) { this.sendProviderUsageTelemetry(extensionId, providerId); for (const session of accessibleSessions) { - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } } return accessibleSessions; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index a4713d12239..9a886a6713a 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -14,6 +14,7 @@ import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage } 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 { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -33,6 +34,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, @ILogService private readonly _logService: ILogService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService, @IExtensionService private readonly _extensionService: IExtensionService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider); @@ -132,28 +134,8 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { disposables.add(toDisposable(() => { this._authenticationService.unregisterAuthenticationProvider(authProviderId); })); - disposables.add(this._authenticationService.onDidChangeSessions(async (e) => { - if (e.providerId === authProviderId) { - if (e.event.removed?.length) { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); - const extensionsToUpdateAccess = []; - for (const allowed of allowedExtensions) { - const from = await this._extensionService.getExtension(allowed.id); - this._authenticationService.updateAllowedExtension(authProviderId, authProviderId, allowed.id, allowed.name, false); - if (from) { - extensionsToUpdateAccess.push({ - from: from.identifier, - to: extension, - enabled: false - }); - } - } - this._proxy.$updateModelAccesslist(extensionsToUpdateAccess); - } - } - })); - disposables.add(this._authenticationService.onDidChangeExtensionSessionAccess(async (e) => { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); + disposables.add(this._authenticationAccessService.onDidChangeExtensionSessionAccess(async (e) => { + const allowedExtensions = this._authenticationAccessService.readAllowedExtensions(authProviderId, accountLabel); const accessList = []; for (const allowedExtension of allowedExtensions) { const from = await this._extensionService.getExtension(allowedExtension.id); diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index 5e4e8ed1510..dd77886bbf0 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts @@ -19,7 +19,7 @@ import { ExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.pro import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { IActivityService } from 'vs/workbench/services/activity/common/activity'; import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationExtensionsService, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService, nullExtensionDescription as extensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; @@ -28,6 +28,9 @@ import { TestActivityService, TestExtensionService, TestProductService, TestStor import type { AuthenticationProvider, AuthenticationSession } from 'vscode'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IProductService } from 'vs/platform/product/common/productService'; +import { AuthenticationAccessService, IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationUsageService, IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AuthenticationExtensionsService } from 'vs/workbench/services/authentication/browser/authenticationExtensionsService'; class AuthQuickPick { private listener: ((e: IQuickPickDidAcceptEvent) => any) | undefined; @@ -113,9 +116,12 @@ suite('ExtHostAuthentication', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService); instantiationService.stub(IProductService, TestProductService); + instantiationService.stub(IAuthenticationAccessService, instantiationService.createInstance(AuthenticationAccessService)); + instantiationService.stub(IAuthenticationUsageService, instantiationService.createInstance(AuthenticationUsageService)); const rpcProtocol = new TestRPCProtocol(); instantiationService.stub(IAuthenticationService, instantiationService.createInstance(AuthenticationService)); + instantiationService.stub(IAuthenticationExtensionsService, instantiationService.createInstance(AuthenticationExtensionsService)); rpcProtocol.set(MainContext.MainThreadAuthentication, instantiationService.createInstance(MainThreadAuthentication, rpcProtocol)); extHostAuthentication = new ExtHostAuthentication(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication); diff --git a/src/vs/workbench/browser/parts/globalCompositeBar.ts b/src/vs/workbench/browser/parts/globalCompositeBar.ts index 3490432e312..8301e27c643 100644 --- a/src/vs/workbench/browser/parts/globalCompositeBar.ts +++ b/src/vs/workbench/browser/parts/globalCompositeBar.ts @@ -43,6 +43,7 @@ import { isString } from 'vs/base/common/types'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND } from 'vs/workbench/common/theme'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export class GlobalCompositeBar extends Disposable { @@ -309,6 +310,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction @ILogService private readonly logService: ILogService, @IActivityService activityService: IActivityService, @IInstantiationService instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService ) { const action = instantiationService.createInstance(CompositeBarAction, { id: ACCOUNTS_ACTIVITY_ID, @@ -391,7 +393,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction menus.push(noAccountsAvailableAction); break; } - const providerLabel = this.authenticationService.getLabel(providerId); + const providerLabel = this.authenticationService.getProvider(providerId).label; const accounts = this.groupedAccounts.get(providerId); if (!accounts) { if (this.problematicProviders.has(providerId)) { @@ -408,19 +410,22 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction } for (const account of accounts) { - const manageExtensionsAction = disposables.add(new Action(`configureSessions${account.label}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), undefined, true, () => { - return this.authenticationService.manageTrustedExtensionsForAccount(providerId, account.label); - })); + const manageExtensionsAction = toAction({ + id: `configureSessions${account.label}`, + label: localize('manageTrustedExtensions', "Manage Trusted Extensions"), + enabled: true, + run: () => this.commandService.executeCommand('_manageTrustedExtensionsForAccount', { providerId, accountLabel: account.label }) + }); - const providerSubMenuActions: Action[] = [manageExtensionsAction]; + const providerSubMenuActions: IAction[] = [manageExtensionsAction]; if (account.canSignOut) { - const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), undefined, true, async () => { - const allSessions = await this.authenticationService.getSessions(providerId); - const sessionsForAccount = allSessions.filter(s => s.account.label === account.label); - return await this.authenticationService.removeAccountSessions(providerId, account.label, sessionsForAccount); + providerSubMenuActions.push(toAction({ + id: 'signOut', + label: localize('signOut', "Sign Out"), + enabled: true, + run: () => this.commandService.executeCommand('_signOutOfAccount', { providerId, accountLabel: account.label }) })); - providerSubMenuActions.push(signOutAction); } const providerSubMenu = new SubmenuAction('activitybar.submenu', `${account.label} (${providerLabel})`, providerSubMenuActions); @@ -628,7 +633,8 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV @ISecretStorageService secretStorageService: ISecretStorageService, @ILogService logService: ILogService, @IActivityService activityService: IActivityService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @ICommandService commandService: ICommandService ) { super(() => [], { ...options, @@ -638,7 +644,7 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV }), hoverOptions, compact: true, - }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService); + }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService, commandService); } } diff --git a/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts new file mode 100644 index 00000000000..6559535304c --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { fromNow } from 'vs/base/common/date'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export class ManageTrustedExtensionsForAccountAction extends Action2 { + constructor() { + super({ + id: '_manageTrustedExtensionsForAccount', + title: localize('manageTrustedExtensionsForAccount', "Manage Trusted Extensions For Account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const productService = accessor.get(IProductService); + const extensionService = accessor.get(IExtensionService); + const dialogService = accessor.get(IDialogService); + const quickInputService = accessor.get(IQuickInputService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allowedExtensions = authenticationAccessService.readAllowedExtensions(providerId, accountLabel); + const trustedExtensionAuthAccess = productService.trustedExtensionAuthAccess; + const trustedExtensionIds = + // Case 1: trustedExtensionAuthAccess is an array + Array.isArray(trustedExtensionAuthAccess) + ? trustedExtensionAuthAccess + // Case 2: trustedExtensionAuthAccess is an object + : typeof trustedExtensionAuthAccess === 'object' + ? trustedExtensionAuthAccess[providerId] ?? [] + : []; + for (const extensionId of trustedExtensionIds) { + const allowedExtension = allowedExtensions.find(ext => ext.id === extensionId); + if (!allowedExtension) { + // Add the extension to the allowedExtensions list + const extension = await extensionService.getExtension(extensionId); + if (extension) { + allowedExtensions.push({ + id: extensionId, + name: extension.displayName || extension.name, + allowed: true, + trusted: true + }); + } + } else { + // Update the extension to be allowed + allowedExtension.allowed = true; + allowedExtension.trusted = true; + } + } + + if (!allowedExtensions.length) { + dialogService.info(localize('noTrustedExtensions', "This account has not been used by any extensions.")); + return; + } + + interface TrustedExtensionsQuickPickItem extends IQuickPickItem { + extension: AllowedExtension; + lastUsed?: number; + } + + const disposableStore = new DisposableStore(); + const quickPick = disposableStore.add(quickInputService.createQuickPick()); + quickPick.canSelectMany = true; + quickPick.customButton = true; + quickPick.customLabel = localize('manageTrustedExtensions.cancel', 'Cancel'); + const usages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + const trustedExtensions = []; + const otherExtensions = []; + for (const extension of allowedExtensions) { + const usage = usages.find(usage => extension.id === usage.extensionId); + extension.lastUsed = usage?.lastUsed; + if (extension.trusted) { + trustedExtensions.push(extension); + } else { + otherExtensions.push(extension); + } + } + + const sortByLastUsed = (a: AllowedExtension, b: AllowedExtension) => (b.lastUsed || 0) - (a.lastUsed || 0); + const toQuickPickItem = function (extension: AllowedExtension) { + const lastUsed = extension.lastUsed; + const description = lastUsed + ? localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(lastUsed, true)) + : localize('notUsed', "Has not used this account"); + let tooltip: string | undefined; + if (extension.trusted) { + tooltip = localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account"); + } + return { + label: extension.name, + extension, + description, + tooltip + }; + }; + const items: Array = [ + ...otherExtensions.sort(sortByLastUsed).map(toQuickPickItem), + { type: 'separator', label: localize('trustedExtensions', "Trusted by Microsoft") }, + ...trustedExtensions.sort(sortByLastUsed).map(toQuickPickItem) + ]; + + quickPick.items = items; + quickPick.selectedItems = items.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator' && (item.extension.allowed === undefined || item.extension.allowed)); + quickPick.title = localize('manageTrustedExtensions', "Manage Trusted Extensions"); + quickPick.placeholder = localize('manageExtensions', "Choose which extensions can access this account"); + + disposableStore.add(quickPick.onDidAccept(() => { + const updatedAllowedList = quickPick.items + .filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator') + .map(i => i.extension); + authenticationAccessService.updateAllowedExtensions(providerId, accountLabel, updatedAllowedList); + quickPick.hide(); + })); + + disposableStore.add(quickPick.onDidChangeSelection((changed) => { + const trustedItems = new Set(); + quickPick.items.forEach(item => { + const trustItem = item as TrustedExtensionsQuickPickItem; + if (trustItem.extension) { + if (trustItem.extension.trusted) { + trustedItems.add(trustItem); + } else { + trustItem.extension.allowed = false; + } + } + }); + changed.forEach((item) => { + item.extension.allowed = true; + trustedItems.delete(item); + }); + + // reselect trusted items if a user tried to unselect one since quick pick doesn't support forcing selection + if (trustedItems.size) { + quickPick.selectedItems = [...changed, ...trustedItems]; + } + })); + + disposableStore.add(quickPick.onDidHide(() => { + disposableStore.dispose(); + })); + + disposableStore.add(quickPick.onDidCustom(() => { + quickPick.hide(); + })); + + quickPick.show(); + } + +} diff --git a/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts new file mode 100644 index 00000000000..87afd379e24 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.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 Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; + +export class SignOutOfAccountAction extends Action2 { + constructor() { + super({ + id: '_signOutOfAccount', + title: localize('signOutOfAccount', "Sign out of account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const authenticationService = accessor.get(IAuthenticationService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + const dialogService = accessor.get(IDialogService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allSessions = await authenticationService.getSessions(providerId); + const sessions = allSessions.filter(s => s.account.label === accountLabel); + + const accountUsages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + + const { confirmed } = await dialogService.confirm({ + type: Severity.Info, + message: accountUsages.length + ? localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountLabel, accountUsages.map(usage => usage.extensionName).join('\n')) + : localize('signOutMessageSimple', "Sign out of '{0}'?", accountLabel), + primaryButton: localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out") + }); + + if (confirmed) { + const removeSessionPromises = sessions.map(session => authenticationService.removeSession(providerId, session.id)); + await Promise.all(removeSessionPromises); + authenticationUsageService.removeAccountUsage(providerId, accountLabel); + authenticationAccessService.removeAllowedExtensions(providerId, accountLabel); + } + } +} diff --git a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts new file mode 100644 index 00000000000..90649d2f358 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { SignOutOfAccountAction } from 'vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction'; +import { AuthenticationProviderInformation, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; +import { Extensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ManageTrustedExtensionsForAccountAction } from './actions/manageTrustedExtensionsForAccountAction'; + +const codeExchangeProxyCommand = CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { + const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); + return environmentService.options?.codeExchangeProxyEndpoints; +}); + +const authenticationDefinitionSchema: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + id: { + type: 'string', + description: localize('authentication.id', 'The id of the authentication provider.') + }, + label: { + type: 'string', + description: localize('authentication.label', 'The human readable name of the authentication provider.'), + } + } +}; + +const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'authentication', + jsonSchema: { + description: localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), + type: 'array', + items: authenticationDefinitionSchema + }, + activationEventsGenerator: (authenticationProviders, result) => { + for (const authenticationProvider of authenticationProviders) { + if (authenticationProvider.id) { + result.push(`onAuthenticationRequest:${authenticationProvider.id}`); + } + } + } +}); + +class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + + readonly type = 'table'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.authentication; + } + + render(manifest: IExtensionManifest): IRenderedData { + const authentication = manifest.contributes?.authentication || []; + if (!authentication.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('authenticationlabel', "Label"), + localize('authenticationid', "ID"), + ]; + + const rows: IRowData[][] = authentication + .sort((a, b) => a.label.localeCompare(b.label)) + .map(auth => { + return [ + auth.label, + auth.id, + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +const extensionFeature = Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'authentication', + label: localize('authentication', "Authentication"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(AuthenticationDataRenderer), +}); + +export class AuthenticationContribution extends Disposable implements IWorkbenchContribution { + static ID = 'workbench.contrib.authentication'; + + private _placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('authentication.Placeholder', "No accounts requested yet..."), + precondition: ContextKeyExpr.false() + }, + }); + + constructor( + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService + ) { + super(); + this._register(codeExchangeProxyCommand); + this._register(extensionFeature); + + this._registerHandlers(); + this._registerAuthenticationExtentionPointHandler(); + this._registerEnvContributedAuthenticationProviders(); + this._registerActions(); + } + + private _registerAuthenticationExtentionPointHandler(): void { + authenticationExtPoint.setHandler((extensions, { added, removed }) => { + added.forEach(point => { + for (const provider of point.value) { + if (isFalsyOrWhitespace(provider.id)) { + point.collector.error(localize('authentication.missingId', 'An authentication contribution must specify an id.')); + continue; + } + + if (isFalsyOrWhitespace(provider.label)) { + point.collector.error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + continue; + } + + if (!this._authenticationService.declaredProviders.some(p => p.id === provider.id)) { + this._authenticationService.registerDeclaredAuthenticationProvider(provider); + } else { + point.collector.error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + } + }); + + const removedExtPoints = removed.flatMap(r => r.value); + removedExtPoints.forEach(point => { + const provider = this._authenticationService.declaredProviders.find(provider => provider.id === point.id); + if (provider) { + this._authenticationService.unregisterDeclaredAuthenticationProvider(provider.id); + } + }); + }); + } + + private _registerEnvContributedAuthenticationProviders(): void { + if (!this._environmentService.options?.authenticationProviders?.length) { + return; + } + for (const provider of this._environmentService.options.authenticationProviders) { + this._authenticationService.registerAuthenticationProvider(provider.id, provider); + } + } + + private _registerHandlers(): void { + this._register(this._authenticationService.onDidRegisterAuthenticationProvider(_e => { + this._placeholderMenuItem?.dispose(); + this._placeholderMenuItem = undefined; + })); + this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(_e => { + if (!this._authenticationService.getProviderIds().length) { + this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('loading', "Loading..."), + precondition: ContextKeyExpr.false() + } + }); + } + })); + } + + private _registerActions(): void { + registerAction2(SignOutOfAccountAction); + registerAction2(ManageTrustedExtensionsForAccountAction); + } +} + +registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 6ba3511f0ee..f612c22c3e3 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -346,8 +346,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.session.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + if (!signedInForProvider || this.authenticationService.getProvider(authenticationProvider.id).supportsMultipleAccounts) { + const providerName = this.authenticationService.getProvider(authenticationProvider.id).label; options.push({ label: localize('sign in using account', "Sign in with {0}", providerName), provider: authenticationProvider }); } } @@ -370,7 +370,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const session of sessions) { const item = { label: session.account.label, - description: this.authenticationService.getLabel(provider.id), + description: this.authenticationService.getProvider(provider.id).label, session: { ...session, providerId: provider.id } }; accounts.set(item.session.account.id, item); diff --git a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index 305f9bae1c5..2afacd74992 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -395,7 +395,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo private createExistingSessionItem(session: AuthenticationSession, providerId: string): ExistingSessionItem { return { label: session.account.label, - description: this.authenticationService.getLabel(providerId), + description: this.authenticationService.getProvider(providerId).label, session, providerId }; @@ -412,9 +412,9 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); - options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", providerName), provider: authenticationProvider }); + const provider = this.authenticationService.getProvider(authenticationProvider.id); + if (!signedInForProvider || provider.supportsMultipleAccounts) { + options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", provider.label), provider: authenticationProvider }); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 0787368edba..08c30bd1f30 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -918,7 +918,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: syncNowCommand.id, label: `${SYNC_TITLE.value}: ${syncNowCommand.title.original}`, description: syncNowCommand.description(that.userDataSyncService) }); if (that.userDataSyncEnablementService.canToggleEnablement()) { const account = that.userDataSyncWorkbenchService.current; - items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getLabel(account.authenticationProviderId)})` : undefined }); + items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getProvider(account.authenticationProviderId).label})` : undefined }); } quickPick.items = items; disposables.add(quickPick.onDidAccept(() => { diff --git a/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts b/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts new file mode 100644 index 00000000000..565821fcb50 --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; + +export const IAuthenticationAccessService = createDecorator('IAuthenticationAccessService'); +export interface IAuthenticationAccessService { + readonly _serviceBrand: undefined; + + readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }>; + + /** + * Check extension access to an account + * @param providerId The id of the authentication provider + * @param accountName The account name that access is checked for + * @param extensionId The id of the extension requesting access + * @returns Returns true or false if the user has opted to permanently grant or disallow access, and undefined + * if they haven't made a choice yet + */ + isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined; + readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[]; + updateAllowedExtensions(providerId: string, accountName: string, extensions: AllowedExtension[]): void; + removeAllowedExtensions(providerId: string, accountName: string): void; +} + +// TODO@TylerLeonhardt: Move this class to MainThreadAuthentication +export class AuthenticationAccessService extends Disposable implements IAuthenticationAccessService { + _serviceBrand: undefined; + + private _onDidChangeExtensionSessionAccess: Emitter<{ providerId: string; accountName: string }> = this._register(new Emitter<{ providerId: string; accountName: string }>()); + readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }> = this._onDidChangeExtensionSessionAccess.event; + + constructor( + @IStorageService private readonly _storageService: IStorageService, + @IProductService private readonly _productService: IProductService + ) { + super(); + } + + isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined { + const trustedExtensionAuthAccess = this._productService.trustedExtensionAuthAccess; + if (Array.isArray(trustedExtensionAuthAccess)) { + if (trustedExtensionAuthAccess.includes(extensionId)) { + return true; + } + } else if (trustedExtensionAuthAccess?.[providerId]?.includes(extensionId)) { + return true; + } + + const allowList = this.readAllowedExtensions(providerId, accountName); + const extensionData = allowList.find(extension => extension.id === extensionId); + if (!extensionData) { + return undefined; + } + // This property didn't exist on this data previously, inclusion in the list at all indicates allowance + return extensionData.allowed !== undefined + ? extensionData.allowed + : true; + } + + readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[] { + let trustedExtensions: AllowedExtension[] = []; + try { + const trustedExtensionSrc = this._storageService.get(`${providerId}-${accountName}`, StorageScope.APPLICATION); + if (trustedExtensionSrc) { + trustedExtensions = JSON.parse(trustedExtensionSrc); + } + } catch (err) { } + + return trustedExtensions; + } + + updateAllowedExtensions(providerId: string, accountName: string, extensions: AllowedExtension[]): void { + const allowList = this.readAllowedExtensions(providerId, accountName); + for (const extension of extensions) { + const index = allowList.findIndex(e => e.id === extension.id); + if (index === -1) { + allowList.push(extension); + } else { + allowList[index].allowed = extension.allowed; + } + } + this._storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.APPLICATION, StorageTarget.USER); + this._onDidChangeExtensionSessionAccess.fire({ providerId, accountName }); + } + + removeAllowedExtensions(providerId: string, accountName: string): void { + this._storageService.remove(`${providerId}-${accountName}`, StorageScope.APPLICATION); + this._onDidChangeExtensionSessionAccess.fire({ providerId, accountName }); + } +} + +registerSingleton(IAuthenticationAccessService, AuthenticationAccessService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts b/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts new file mode 100644 index 00000000000..eaec30e9f18 --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts @@ -0,0 +1,418 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import * as nls from 'vs/nls'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AuthenticationSession, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; + +// OAuth2 spec prohibits space in a scope, so use that to join them. +const SCOPESLIST_SEPARATOR = ' '; + +interface SessionRequest { + disposables: IDisposable[]; + requestingExtensionIds: string[]; +} + +interface SessionRequestInfo { + [scopesList: string]: SessionRequest; +} + +// TODO@TylerLeonhardt: This should all go in MainThreadAuthentication +export class AuthenticationExtensionsService extends Disposable implements IAuthenticationExtensionsService { + declare readonly _serviceBrand: undefined; + private _signInRequestItems = new Map(); + private _sessionAccessRequestItems = new Map(); + private _accountBadgeDisposable = this._register(new MutableDisposable()); + + constructor( + @IActivityService private readonly activityService: IActivityService, + @IStorageService private readonly storageService: IStorageService, + @IDialogService private readonly dialogService: IDialogService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IAuthenticationUsageService private readonly _authenticationUsageService: IAuthenticationUsageService, + @IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService + ) { + super(); + this.registerListeners(); + } + + private registerListeners() { + this._register(this._authenticationService.onDidChangeSessions(async e => { + if (e.event.added?.length) { + await this.updateNewSessionRequests(e.providerId, e.event.added); + } + if (e.event.removed?.length) { + await this.updateAccessRequests(e.providerId, e.event.removed); + } + this.updateBadgeCount(); + })); + + this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(e => { + const accessRequests = this._sessionAccessRequestItems.get(e.id) || {}; + Object.keys(accessRequests).forEach(extensionId => { + this.removeAccessRequest(e.id, extensionId); + }); + })); + } + + private async updateNewSessionRequests(providerId: string, addedSessions: readonly AuthenticationSession[]): Promise { + const existingRequestsForProvider = this._signInRequestItems.get(providerId); + if (!existingRequestsForProvider) { + return; + } + + Object.keys(existingRequestsForProvider).forEach(requestedScopes => { + if (addedSessions.some(session => session.scopes.slice().join(SCOPESLIST_SEPARATOR) === requestedScopes)) { + const sessionRequest = existingRequestsForProvider[requestedScopes]; + sessionRequest?.disposables.forEach(item => item.dispose()); + + delete existingRequestsForProvider[requestedScopes]; + if (Object.keys(existingRequestsForProvider).length === 0) { + this._signInRequestItems.delete(providerId); + } else { + this._signInRequestItems.set(providerId, existingRequestsForProvider); + } + } + }); + } + + private async updateAccessRequests(providerId: string, removedSessions: readonly AuthenticationSession[]) { + const providerRequests = this._sessionAccessRequestItems.get(providerId); + if (providerRequests) { + Object.keys(providerRequests).forEach(extensionId => { + removedSessions.forEach(removed => { + const indexOfSession = providerRequests[extensionId].possibleSessions.findIndex(session => session.id === removed.id); + if (indexOfSession) { + providerRequests[extensionId].possibleSessions.splice(indexOfSession, 1); + } + }); + + if (!providerRequests[extensionId].possibleSessions.length) { + this.removeAccessRequest(providerId, extensionId); + } + }); + } + } + + private updateBadgeCount(): void { + this._accountBadgeDisposable.clear(); + + let numberOfRequests = 0; + this._signInRequestItems.forEach(providerRequests => { + Object.keys(providerRequests).forEach(request => { + numberOfRequests += providerRequests[request].requestingExtensionIds.length; + }); + }); + + this._sessionAccessRequestItems.forEach(accessRequest => { + numberOfRequests += Object.keys(accessRequest).length; + }); + + if (numberOfRequests > 0) { + const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested")); + this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); + } + } + + private removeAccessRequest(providerId: string, extensionId: string): void { + const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; + if (providerRequests[extensionId]) { + dispose(providerRequests[extensionId].disposables); + delete providerRequests[extensionId]; + this.updateBadgeCount(); + } + } + + //#region Session Preference + + updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${session.scopes.join(' ')}`; + + // Store the preference in the workspace and application storage. This allows new workspaces to + // have a preference set already to limit the number of prompts that are shown... but also allows + // a specific workspace to override the global preference. + this.storageService.store(key, session.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); + this.storageService.store(key, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE); + } + + getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; + + // If a preference is set in the workspace, use that. Otherwise, use the global preference. + return this.storageService.get(key, StorageScope.WORKSPACE) ?? this.storageService.get(key, StorageScope.APPLICATION); + } + + removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; + + // This won't affect any other workspaces that have a preference set, but it will remove the preference + // for this workspace and the global preference. This is only paired with a call to updateSessionPreference... + // so we really don't _need_ to remove them as they are about to be overridden anyway... but it's more correct + // to remove them first... and in case this gets called from somewhere else in the future. + this.storageService.remove(key, StorageScope.WORKSPACE); + this.storageService.remove(key, StorageScope.APPLICATION); + } + + //#endregion + + private async showGetSessionPrompt(provider: IAuthenticationProvider, accountName: string, extensionId: string, extensionName: string): Promise { + enum SessionPromptChoice { + Allow = 0, + Deny = 1, + Cancel = 2 + } + const { result } = await this.dialogService.prompt({ + type: Severity.Info, + message: nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, provider.label, accountName), + buttons: [ + { + label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), + run: () => SessionPromptChoice.Allow + }, + { + label: nls.localize({ key: 'deny', comment: ['&& denotes a mnemonic'] }, "&&Deny"), + run: () => SessionPromptChoice.Deny + } + ], + cancelButton: { + run: () => SessionPromptChoice.Cancel + } + }); + + if (result !== SessionPromptChoice.Cancel) { + this._authenticationAccessService.updateAllowedExtensions(provider.id, accountName, [{ id: extensionId, name: extensionName, allowed: result === SessionPromptChoice.Allow }]); + this.removeAccessRequest(provider.id, extensionId); + } + + return result === SessionPromptChoice.Allow; + } + + async selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], availableSessions: AuthenticationSession[]): Promise { + return new Promise((resolve, reject) => { + // This function should be used only when there are sessions to disambiguate. + if (!availableSessions.length) { + reject('No available sessions'); + return; + } + + const quickPick = this.quickInputService.createQuickPick<{ label: string; session?: AuthenticationSession }>(); + quickPick.ignoreFocusOut = true; + const items: { label: string; session?: AuthenticationSession }[] = availableSessions.map(session => { + return { + label: session.account.label, + session: session + }; + }); + + items.push({ + label: nls.localize('useOtherAccount', "Sign in to another account") + }); + + quickPick.items = items; + + quickPick.title = nls.localize( + { + key: 'selectAccount', + comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.'] + }, + "The extension '{0}' wants to access a {1} account", + extensionName, + this._authenticationService.getProvider(providerId).label); + quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); + + quickPick.onDidAccept(async _ => { + const session = quickPick.selectedItems[0].session ?? await this._authenticationService.createSession(providerId, scopes); + const accountName = session.account.label; + + this._authenticationAccessService.updateAllowedExtensions(providerId, accountName, [{ id: extensionId, name: extensionName, allowed: true }]); + this.updateSessionPreference(providerId, extensionId, session); + this.removeAccessRequest(providerId, extensionId); + + quickPick.dispose(); + resolve(session); + }); + + quickPick.onDidHide(_ => { + if (!quickPick.selectedItems[0]) { + reject('User did not consent to account access'); + } + + quickPick.dispose(); + }); + + quickPick.show(); + }); + } + + private async completeSessionAccessRequest(provider: IAuthenticationProvider, extensionId: string, extensionName: string, scopes: string[]): Promise { + const providerRequests = this._sessionAccessRequestItems.get(provider.id) || {}; + const existingRequest = providerRequests[extensionId]; + if (!existingRequest) { + return; + } + + if (!provider) { + return; + } + const possibleSessions = existingRequest.possibleSessions; + + let session: AuthenticationSession | undefined; + if (provider.supportsMultipleAccounts) { + try { + session = await this.selectSession(provider.id, extensionId, extensionName, scopes, possibleSessions); + } catch (_) { + // ignore cancel + } + } else { + const approved = await this.showGetSessionPrompt(provider, possibleSessions[0].account.label, extensionId, extensionName); + if (approved) { + session = possibleSessions[0]; + } + } + + if (session) { + this._authenticationUsageService.addAccountUsage(provider.id, session.account.label, extensionId, extensionName); + } + } + + requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: AuthenticationSession[]): void { + const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; + const hasExistingRequest = providerRequests[extensionId]; + if (hasExistingRequest) { + return; + } + + const provider = this._authenticationService.getProvider(providerId); + const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + group: '3_accessRequests', + command: { + id: `${providerId}${extensionId}Access`, + title: nls.localize({ + key: 'accessRequest', + comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`] + }, + "Grant access to {0} for {1}... (1)", + provider.label, + extensionName) + } + }); + + const accessCommand = CommandsRegistry.registerCommand({ + id: `${providerId}${extensionId}Access`, + handler: async (accessor) => { + this.completeSessionAccessRequest(provider, extensionId, extensionName, scopes); + } + }); + + providerRequests[extensionId] = { possibleSessions, disposables: [menuItem, accessCommand] }; + this._sessionAccessRequestItems.set(providerId, providerRequests); + this.updateBadgeCount(); + } + + async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { + if (!this._authenticationService.isAuthenticationProviderRegistered(providerId)) { + // Activate has already been called for the authentication provider, but it cannot block on registering itself + // since this is sync and returns a disposable. So, wait for registration event to fire that indicates the + // provider is now in the map. + await new Promise((resolve, _) => { + const dispose = this._authenticationService.onDidRegisterAuthenticationProvider(e => { + if (e.id === providerId) { + dispose.dispose(); + resolve(); + } + }); + }); + } + + let provider: IAuthenticationProvider; + try { + provider = this._authenticationService.getProvider(providerId); + } catch (_e) { + return; + } + + const providerRequests = this._signInRequestItems.get(providerId); + const scopesList = scopes.join(SCOPESLIST_SEPARATOR); + const extensionHasExistingRequest = providerRequests + && providerRequests[scopesList] + && providerRequests[scopesList].requestingExtensionIds.includes(extensionId); + + if (extensionHasExistingRequest) { + return; + } + + // Construct a commandId that won't clash with others generated here, nor likely with an extension's command + const commandId = `${providerId}:${extensionId}:signIn${Object.keys(providerRequests || []).length}`; + const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + group: '2_signInRequests', + command: { + id: commandId, + title: nls.localize({ + key: 'signInRequest', + comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`] + }, + "Sign in with {0} to use {1} (1)", + provider.label, + extensionName) + } + }); + + const signInCommand = CommandsRegistry.registerCommand({ + id: commandId, + handler: async (accessor) => { + const authenticationService = accessor.get(IAuthenticationService); + const session = await authenticationService.createSession(providerId, scopes); + + this._authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); + this.updateSessionPreference(providerId, extensionId, session); + } + }); + + + if (providerRequests) { + const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] }; + + providerRequests[scopesList] = { + disposables: [...existingRequest.disposables, menuItem, signInCommand], + requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId] + }; + this._signInRequestItems.set(providerId, providerRequests); + } else { + this._signInRequestItems.set(providerId, { + [scopesList]: { + disposables: [menuItem, signInCommand], + requestingExtensionIds: [extensionId] + } + }); + } + + this.updateBadgeCount(); + } +} + +registerSingleton(IAuthenticationExtensionsService, AuthenticationExtensionsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 524b7402f04..6c22b70cd63 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -3,85 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { fromNow } from 'vs/base/common/date'; import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable, isDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable } from 'vs/base/common/lifecycle'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { isString } from 'vs/base/common/types'; -import * as nls from 'vs/nls'; -import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { localize } from 'vs/nls'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { Severity } from 'vs/platform/notification/common/notification'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; -import { Registry } from 'vs/platform/registry/common/platform'; import { ISecretStorageService } from 'vs/platform/secrets/common/secrets'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IAuthenticationCreateSessionOptions, AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; -import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; -import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; } -interface IAccountUsage { - extensionId: string; - extensionName: string; - lastUsed: number; -} - -// TODO: make this account usage stuff a service - -function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { - const accountKey = `${providerId}-${accountName}-usages`; - const storedUsages = storageService.get(accountKey, StorageScope.APPLICATION); - let usages: IAccountUsage[] = []; - if (storedUsages) { - try { - usages = JSON.parse(storedUsages); - } catch (e) { - // ignore - } - } - - return usages; -} - -function removeAccountUsage(storageService: IStorageService, providerId: string, accountName: string): void { - const accountKey = `${providerId}-${accountName}-usages`; - storageService.remove(accountKey, StorageScope.APPLICATION); -} - -export function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) { - const accountKey = `${providerId}-${accountName}-usages`; - const usages = readAccountUsages(storageService, providerId, accountName); - - const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); - if (existingUsageIndex > -1) { - usages.splice(existingUsageIndex, 1, { - extensionId, - extensionName, - lastUsed: Date.now() - }); - } else { - usages.push({ - extensionId, - extensionName, - lastUsed: Date.now() - }); - } - - storageService.store(accountKey, JSON.stringify(usages), StorageScope.APPLICATION, StorageTarget.MACHINE); -} - // TODO: pull this out into its own service export type AuthenticationSessionInfo = { readonly id: string; readonly accessToken: string; readonly providerId: string; readonly canSignOut?: boolean }; export async function getCurrentAuthenticationSessionInfo( @@ -107,122 +42,8 @@ export async function getCurrentAuthenticationSessionInfo( return undefined; } -// OAuth2 spec prohibits space in a scope, so use that to join them. -const SCOPESLIST_SEPARATOR = ' '; - -interface SessionRequest { - disposables: IDisposable[]; - requestingExtensionIds: string[]; -} - -interface SessionRequestInfo { - [scopesList: string]: SessionRequest; -} - -CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { - const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); - return environmentService.options?.codeExchangeProxyEndpoints; -}); - -const authenticationDefinitionSchema: IJSONSchema = { - type: 'object', - additionalProperties: false, - properties: { - id: { - type: 'string', - description: nls.localize('authentication.id', 'The id of the authentication provider.') - }, - label: { - type: 'string', - description: nls.localize('authentication.label', 'The human readable name of the authentication provider.'), - } - } -}; - -const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'authentication', - jsonSchema: { - description: nls.localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), - type: 'array', - items: authenticationDefinitionSchema - }, - activationEventsGenerator: (authenticationProviders, result) => { - for (const authenticationProvider of authenticationProviders) { - if (authenticationProvider.id) { - result.push(`onAuthenticationRequest:${authenticationProvider.id}`); - } - } - } -}); - -class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { - - readonly type = 'table'; - - shouldRender(manifest: IExtensionManifest): boolean { - return !!manifest.contributes?.authentication; - } - - render(manifest: IExtensionManifest): IRenderedData { - const authentication = manifest.contributes?.authentication || []; - if (!authentication.length) { - return { data: { headers: [], rows: [] }, dispose: () => { } }; - } - - const headers = [ - nls.localize('authenticationlabel', "Label"), - nls.localize('authenticationid', "ID"), - ]; - - const rows: IRowData[][] = authentication - .sort((a, b) => a.label.localeCompare(b.label)) - .map(auth => { - return [ - auth.label, - auth.id, - ]; - }); - - return { - data: { - headers, - rows - }, - dispose: () => { } - }; - } -} - -Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ - id: 'authentication', - label: nls.localize('authentication', "Authentication"), - access: { - canToggle: false - }, - renderer: new SyncDescriptor(AuthenticationDataRenderer), -}); - -let placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - command: { - id: 'noAuthenticationProviders', - title: nls.localize('authentication.Placeholder', "No accounts requested yet..."), - precondition: ContextKeyExpr.false() - }, -}); - export class AuthenticationService extends Disposable implements IAuthenticationService { declare readonly _serviceBrand: undefined; - private _signInRequestItems = new Map(); - private _sessionAccessRequestItems = new Map(); - private _accountBadgeDisposable = this._register(new MutableDisposable()); - - private _authenticationProviders: Map = new Map(); - private _authenticationProviderDisposables: DisposableMap = this._register(new DisposableMap()); - - /** - * All providers that have been statically declared by extensions. These may not be registered. - */ - declaredProviders: AuthenticationProviderInformation[] = []; private _onDidRegisterAuthenticationProvider: Emitter = this._register(new Emitter()); readonly onDidRegisterAuthenticationProvider: Event = this._onDidRegisterAuthenticationProvider.event; @@ -233,55 +54,86 @@ export class AuthenticationService extends Disposable implements IAuthentication private _onDidChangeSessions: Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>()); readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event; - private _onDidChangeDeclaredProviders: Emitter = this._register(new Emitter()); - readonly onDidChangeDeclaredProviders: Event = this._onDidChangeDeclaredProviders.event; + private _onDidChangeDeclaredProviders: Emitter = this._register(new Emitter()); + readonly onDidChangeDeclaredProviders: Event = this._onDidChangeDeclaredProviders.event; - private _onDidChangeExtensionSessionAccess: Emitter<{ providerId: string; accountName: string }> = this._register(new Emitter<{ providerId: string; accountName: string }>()); - readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }> = this._onDidChangeExtensionSessionAccess.event; + private _authenticationProviders: Map = new Map(); + private _authenticationProviderDisposables: DisposableMap = this._register(new DisposableMap()); constructor( - @IActivityService private readonly activityService: IActivityService, - @IExtensionService private readonly extensionService: IExtensionService, - @IStorageService private readonly storageService: IStorageService, - @IDialogService private readonly dialogService: IDialogService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IProductService private readonly productService: IProductService, - @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IAuthenticationAccessService authenticationAccessService: IAuthenticationAccessService ) { super(); - environmentService.options?.authenticationProviders?.forEach(provider => this.registerAuthenticationProvider(provider.id, provider)); - authenticationExtPoint.setHandler((extensions, { added, removed }) => { - added.forEach(point => { - for (const provider of point.value) { - if (isFalsyOrWhitespace(provider.id)) { - point.collector.error(nls.localize('authentication.missingId', 'An authentication contribution must specify an id.')); - continue; - } - - if (isFalsyOrWhitespace(provider.label)) { - point.collector.error(nls.localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); - continue; - } - - if (!this.declaredProviders.some(p => p.id === provider.id)) { - this.declaredProviders.push(provider); - } else { - point.collector.error(nls.localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); - } + this._register(authenticationAccessService.onDidChangeExtensionSessionAccess(e => { + // The access has changed, not the actual session itself but extensions depend on this event firing + // when they have gained access to an account so this fires that event. + this._onDidChangeSessions.fire({ + providerId: e.providerId, + label: e.accountName, + event: { + added: [], + changed: [], + removed: [] } }); + })); + } - const removedExtPoints = removed.flatMap(r => r.value); - removedExtPoints.forEach(point => { - const index = this.declaredProviders.findIndex(provider => provider.id === point.id); - if (index > -1) { - this.declaredProviders.splice(index, 1); - } - }); + private _declaredProviders: AuthenticationProviderInformation[] = []; + get declaredProviders(): AuthenticationProviderInformation[] { + return this._declaredProviders; + } - this._onDidChangeDeclaredProviders.fire(this.declaredProviders); - }); + registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void { + if (isFalsyOrWhitespace(provider.id)) { + throw new Error(localize('authentication.missingId', 'An authentication contribution must specify an id.')); + } + if (isFalsyOrWhitespace(provider.label)) { + throw new Error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + } + if (this.declaredProviders.some(p => p.id === provider.id)) { + throw new Error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + this._declaredProviders.push(provider); + this._onDidChangeDeclaredProviders.fire(); + } + + unregisterDeclaredAuthenticationProvider(id: string): void { + const index = this.declaredProviders.findIndex(provider => provider.id === id); + if (index > -1) { + this.declaredProviders.splice(index, 1); + } + this._onDidChangeDeclaredProviders.fire(); + } + + isAuthenticationProviderRegistered(id: string): boolean { + return this._authenticationProviders.has(id); + } + + registerAuthenticationProvider(id: string, authenticationProvider: IAuthenticationProvider): void { + this._authenticationProviders.set(id, authenticationProvider); + const disposableStore = new DisposableStore(); + disposableStore.add(authenticationProvider.onDidChangeSessions(e => this._onDidChangeSessions.fire({ + providerId: id, + label: authenticationProvider.label, + event: e + }))); + if (isDisposable(authenticationProvider)) { + disposableStore.add(authenticationProvider); + } + this._authenticationProviderDisposables.set(id, disposableStore); + this._onDidRegisterAuthenticationProvider.fire({ id, label: authenticationProvider.label }); + } + + unregisterAuthenticationProvider(id: string): void { + const provider = this._authenticationProviders.get(id); + if (provider) { + this._authenticationProviders.delete(id); + this._onDidUnregisterAuthenticationProvider.fire({ id, label: provider.label }); + } + this._authenticationProviderDisposables.deleteAndDispose(id); } getProviderIds(): string[] { @@ -292,495 +144,11 @@ export class AuthenticationService extends Disposable implements IAuthentication return providerIds; } - isAuthenticationProviderRegistered(id: string): boolean { - return this._authenticationProviders.has(id); - } - - registerAuthenticationProvider(id: string, authenticationProvider: IAuthenticationProvider): void { - this._authenticationProviders.set(id, authenticationProvider); - const disposableStore = new DisposableStore(); - disposableStore.add(authenticationProvider.onDidChangeSessions(e => this.sessionsUpdate(authenticationProvider, e))); - if (isDisposable(authenticationProvider)) { - disposableStore.add(authenticationProvider); + getProvider(id: string): IAuthenticationProvider { + if (this._authenticationProviders.has(id)) { + return this._authenticationProviders.get(id)!; } - this._authenticationProviderDisposables.set(id, disposableStore); - this._onDidRegisterAuthenticationProvider.fire({ id, label: authenticationProvider.label }); - - if (placeholderMenuItem) { - placeholderMenuItem.dispose(); - placeholderMenuItem = undefined; - } - } - - unregisterAuthenticationProvider(id: string): void { - const provider = this._authenticationProviders.get(id); - if (provider) { - this._authenticationProviders.delete(id); - this._onDidUnregisterAuthenticationProvider.fire({ id, label: provider.label }); - - const accessRequests = this._sessionAccessRequestItems.get(id) || {}; - Object.keys(accessRequests).forEach(extensionId => { - this.removeAccessRequest(id, extensionId); - }); - } - this._authenticationProviderDisposables.deleteAndDispose(id); - - if (!this._authenticationProviders.size) { - placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - command: { - id: 'noAuthenticationProviders', - title: nls.localize('loading', "Loading..."), - precondition: ContextKeyExpr.false() - }, - }); - } - } - - private async sessionsUpdate(provider: IAuthenticationProvider, event: AuthenticationSessionsChangeEvent): Promise { - this._onDidChangeSessions.fire({ providerId: provider.id, label: provider.label, event }); - if (event.added?.length) { - await this.updateNewSessionRequests(provider, event.added); - } - if (event.removed?.length) { - await this.updateAccessRequests(provider.id, event.removed); - } - this.updateBadgeCount(); - } - - private async updateNewSessionRequests(provider: IAuthenticationProvider, addedSessions: readonly AuthenticationSession[]): Promise { - const existingRequestsForProvider = this._signInRequestItems.get(provider.id); - if (!existingRequestsForProvider) { - return; - } - - Object.keys(existingRequestsForProvider).forEach(requestedScopes => { - if (addedSessions.some(session => session.scopes.slice().join(SCOPESLIST_SEPARATOR) === requestedScopes)) { - const sessionRequest = existingRequestsForProvider[requestedScopes]; - sessionRequest?.disposables.forEach(item => item.dispose()); - - delete existingRequestsForProvider[requestedScopes]; - if (Object.keys(existingRequestsForProvider).length === 0) { - this._signInRequestItems.delete(provider.id); - } else { - this._signInRequestItems.set(provider.id, existingRequestsForProvider); - } - } - }); - } - - private async updateAccessRequests(providerId: string, removedSessions: readonly AuthenticationSession[]) { - const providerRequests = this._sessionAccessRequestItems.get(providerId); - if (providerRequests) { - Object.keys(providerRequests).forEach(extensionId => { - removedSessions.forEach(removed => { - const indexOfSession = providerRequests[extensionId].possibleSessions.findIndex(session => session.id === removed.id); - if (indexOfSession) { - providerRequests[extensionId].possibleSessions.splice(indexOfSession, 1); - } - }); - - if (!providerRequests[extensionId].possibleSessions.length) { - this.removeAccessRequest(providerId, extensionId); - } - }); - } - } - - private updateBadgeCount(): void { - this._accountBadgeDisposable.clear(); - - let numberOfRequests = 0; - this._signInRequestItems.forEach(providerRequests => { - Object.keys(providerRequests).forEach(request => { - numberOfRequests += providerRequests[request].requestingExtensionIds.length; - }); - }); - - this._sessionAccessRequestItems.forEach(accessRequest => { - numberOfRequests += Object.keys(accessRequest).length; - }); - - if (numberOfRequests > 0) { - const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested")); - this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); - } - } - - private removeAccessRequest(providerId: string, extensionId: string): void { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - if (providerRequests[extensionId]) { - dispose(providerRequests[extensionId].disposables); - delete providerRequests[extensionId]; - this.updateBadgeCount(); - } - } - - /** - * Check extension access to an account - * @param providerId The id of the authentication provider - * @param accountName The account name that access is checked for - * @param extensionId The id of the extension requesting access - * @returns Returns true or false if the user has opted to permanently grant or disallow access, and undefined - * if they haven't made a choice yet - */ - isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined { - const trustedExtensionAuthAccess = this.productService.trustedExtensionAuthAccess; - if (Array.isArray(trustedExtensionAuthAccess)) { - if (trustedExtensionAuthAccess.includes(extensionId)) { - return true; - } - } else if (trustedExtensionAuthAccess?.[providerId]?.includes(extensionId)) { - return true; - } - - const allowList = this.readAllowedExtensions(providerId, accountName); - const extensionData = allowList.find(extension => extension.id === extensionId); - if (!extensionData) { - return undefined; - } - // This property didn't exist on this data previously, inclusion in the list at all indicates allowance - return extensionData.allowed !== undefined - ? extensionData.allowed - : true; - } - - updateAllowedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string, isAllowed: boolean): void { - const allowList = this.readAllowedExtensions(providerId, accountName); - const index = allowList.findIndex(extension => extension.id === extensionId); - if (index === -1) { - allowList.push({ id: extensionId, name: extensionName, allowed: isAllowed }); - } else { - allowList[index].allowed = isAllowed; - } - - this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.APPLICATION, StorageTarget.USER); - } - - //#region Session Preference - - updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${session.scopes.join(' ')}`; - - // Store the preference in the workspace and application storage. This allows new workspaces to - // have a preference set already to limit the number of prompts that are shown... but also allows - // a specific workspace to override the global preference. - this.storageService.store(key, session.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); - this.storageService.store(key, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE); - } - - getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; - - // If a preference is set in the workspace, use that. Otherwise, use the global preference. - return this.storageService.get(key, StorageScope.WORKSPACE) ?? this.storageService.get(key, StorageScope.APPLICATION); - } - - removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; - - // This won't affect any other workspaces that have a preference set, but it will remove the preference - // for this workspace and the global preference. This is only paired with a call to updateSessionPreference... - // so we really don't _need_ to remove them as they are about to be overridden anyway... but it's more correct - // to remove them first... and in case this gets called from somewhere else in the future. - this.storageService.remove(key, StorageScope.WORKSPACE); - this.storageService.remove(key, StorageScope.APPLICATION); - } - - //#endregion - - async showGetSessionPrompt(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise { - const providerName = this.getLabel(providerId); - enum SessionPromptChoice { - Allow = 0, - Deny = 1, - Cancel = 2 - } - const { result } = await this.dialogService.prompt({ - type: Severity.Info, - message: nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, providerName, accountName), - buttons: [ - { - label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), - run: () => SessionPromptChoice.Allow - }, - { - label: nls.localize({ key: 'deny', comment: ['&& denotes a mnemonic'] }, "&&Deny"), - run: () => SessionPromptChoice.Deny - } - ], - cancelButton: { - run: () => SessionPromptChoice.Cancel - } - }); - - if (result !== SessionPromptChoice.Cancel) { - this.updateAllowedExtension(providerId, accountName, extensionId, extensionName, result === SessionPromptChoice.Allow); - this.removeAccessRequest(providerId, extensionId); - } - - return result === SessionPromptChoice.Allow; - } - - async selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], availableSessions: AuthenticationSession[]): Promise { - return new Promise((resolve, reject) => { - // This function should be used only when there are sessions to disambiguate. - if (!availableSessions.length) { - reject('No available sessions'); - } - - const quickPick = this.quickInputService.createQuickPick<{ label: string; session?: AuthenticationSession }>(); - quickPick.ignoreFocusOut = true; - const items: { label: string; session?: AuthenticationSession }[] = availableSessions.map(session => { - return { - label: session.account.label, - session: session - }; - }); - - items.push({ - label: nls.localize('useOtherAccount', "Sign in to another account") - }); - - const providerName = this.getLabel(providerId); - - quickPick.items = items; - - quickPick.title = nls.localize( - { - key: 'selectAccount', - comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.'] - }, - "The extension '{0}' wants to access a {1} account", - extensionName, - providerName); - quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); - - quickPick.onDidAccept(async _ => { - const session = quickPick.selectedItems[0].session ?? await this.createSession(providerId, scopes); - const accountName = session.account.label; - - this.updateAllowedExtension(providerId, accountName, extensionId, extensionName, true); - this.updateSessionPreference(providerId, extensionId, session); - this.removeAccessRequest(providerId, extensionId); - - quickPick.dispose(); - resolve(session); - }); - - quickPick.onDidHide(_ => { - if (!quickPick.selectedItems[0]) { - reject('User did not consent to account access'); - } - - quickPick.dispose(); - }); - - quickPick.show(); - }); - } - - async completeSessionAccessRequest(providerId: string, extensionId: string, extensionName: string, scopes: string[]): Promise { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - const existingRequest = providerRequests[extensionId]; - if (!existingRequest) { - return; - } - - const possibleSessions = existingRequest.possibleSessions; - const supportsMultipleAccounts = this.supportsMultipleAccounts(providerId); - - let session: AuthenticationSession | undefined; - if (supportsMultipleAccounts) { - try { - session = await this.selectSession(providerId, extensionId, extensionName, scopes, possibleSessions); - } catch (_) { - // ignore cancel - } - } else { - const approved = await this.showGetSessionPrompt(providerId, possibleSessions[0].account.label, extensionId, extensionName); - if (approved) { - session = possibleSessions[0]; - } - } - - if (session) { - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); - const providerName = this.getLabel(providerId); - this._onDidChangeSessions.fire({ providerId, label: providerName, event: { added: [], removed: [], changed: [session] } }); - } - } - - requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: AuthenticationSession[]): void { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - const hasExistingRequest = providerRequests[extensionId]; - if (hasExistingRequest) { - return; - } - - const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '3_accessRequests', - command: { - id: `${providerId}${extensionId}Access`, - title: nls.localize({ - key: 'accessRequest', - comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`] - }, - "Grant access to {0} for {1}... (1)", - this.getLabel(providerId), - extensionName) - } - }); - - const accessCommand = CommandsRegistry.registerCommand({ - id: `${providerId}${extensionId}Access`, - handler: async (accessor) => { - const authenticationService = accessor.get(IAuthenticationService); - authenticationService.completeSessionAccessRequest(providerId, extensionId, extensionName, scopes); - } - }); - - providerRequests[extensionId] = { possibleSessions, disposables: [menuItem, accessCommand] }; - this._sessionAccessRequestItems.set(providerId, providerRequests); - this.updateBadgeCount(); - } - - async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { - let provider = this._authenticationProviders.get(providerId); - if (!provider) { - // Activate has already been called for the authentication provider, but it cannot block on registering itself - // since this is sync and returns a disposable. So, wait for registration event to fire that indicates the - // provider is now in the map. - await new Promise((resolve, _) => { - const dispose = this.onDidRegisterAuthenticationProvider(e => { - if (e.id === providerId) { - provider = this._authenticationProviders.get(providerId); - dispose.dispose(); - resolve(); - } - }); - }); - } - - if (!provider) { - return; - } - - const providerRequests = this._signInRequestItems.get(providerId); - const scopesList = scopes.join(SCOPESLIST_SEPARATOR); - const extensionHasExistingRequest = providerRequests - && providerRequests[scopesList] - && providerRequests[scopesList].requestingExtensionIds.includes(extensionId); - - if (extensionHasExistingRequest) { - return; - } - - // Construct a commandId that won't clash with others generated here, nor likely with an extension's command - const commandId = `${providerId}:${extensionId}:signIn${Object.keys(providerRequests || []).length}`; - const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '2_signInRequests', - command: { - id: commandId, - title: nls.localize({ - key: 'signInRequest', - comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`] - }, - "Sign in with {0} to use {1} (1)", - provider.label, - extensionName) - } - }); - - const signInCommand = CommandsRegistry.registerCommand({ - id: commandId, - handler: async (accessor) => { - const authenticationService = accessor.get(IAuthenticationService); - const session = await authenticationService.createSession(providerId, scopes); - - this.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); - this.updateSessionPreference(providerId, extensionId, session); - } - }); - - - if (providerRequests) { - const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] }; - - providerRequests[scopesList] = { - disposables: [...existingRequest.disposables, menuItem, signInCommand], - requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId] - }; - this._signInRequestItems.set(providerId, providerRequests); - } else { - this._signInRequestItems.set(providerId, { - [scopesList]: { - disposables: [menuItem, signInCommand], - requestingExtensionIds: [extensionId] - } - }); - } - - this.updateBadgeCount(); - } - getLabel(id: string): string { - const authProvider = this._authenticationProviders.get(id); - if (authProvider) { - return authProvider.label; - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - supportsMultipleAccounts(id: string): boolean { - const authProvider = this._authenticationProviders.get(id); - if (authProvider) { - return authProvider.supportsMultipleAccounts; - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise { - await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); - let provider = this._authenticationProviders.get(providerId); - if (provider) { - return provider; - } - - // When activate has completed, the extension has made the call to `registerAuthenticationProvider`. - // However, activate cannot block on this, so the renderer may not have gotten the event yet. - const didRegister: Promise = new Promise((resolve, _) => { - this.onDidRegisterAuthenticationProvider(e => { - if (e.id === providerId) { - provider = this._authenticationProviders.get(providerId); - if (provider) { - resolve(provider); - } else { - throw new Error(`No authentication provider '${providerId}' is currently registered.`); - } - } - }); - }); - - const didTimeout: Promise = new Promise((_, reject) => { - setTimeout(() => { - reject('Timed out waiting for authentication provider to register'); - }, 5000); - }); - - return Promise.race([didRegister, didTimeout]); + throw new Error(`No authentication provider '${id}' is currently registered.`); } async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { @@ -812,177 +180,35 @@ export class AuthenticationService extends Disposable implements IAuthentication } } - // TODO: pull this stuff out into its own service - readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[] { - let trustedExtensions: AllowedExtension[] = []; - try { - const trustedExtensionSrc = this.storageService.get(`${providerId}-${accountName}`, StorageScope.APPLICATION); - if (trustedExtensionSrc) { - trustedExtensions = JSON.parse(trustedExtensionSrc); - } - } catch (err) { } - - return trustedExtensions; - } - - // TODO: pull this out into an Action in a contribution - async manageTrustedExtensionsForAccount(id: string, accountName: string): Promise { - const authProvider = this._authenticationProviders.get(id); - if (!authProvider) { - throw new Error(`No authentication provider '${id}' is currently registered.`); + private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise { + await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); + let provider = this._authenticationProviders.get(providerId); + if (provider) { + return provider; } - const allowedExtensions = this.readAllowedExtensions(authProvider.id, accountName); - const trustedExtensionAuthAccess = this.productService.trustedExtensionAuthAccess; - const trustedExtensionIds = - // Case 1: trustedExtensionAuthAccess is an array - Array.isArray(trustedExtensionAuthAccess) - ? trustedExtensionAuthAccess - // Case 2: trustedExtensionAuthAccess is an object - : typeof trustedExtensionAuthAccess === 'object' - ? trustedExtensionAuthAccess[authProvider.id] ?? [] - : []; - for (const extensionId of trustedExtensionIds) { - const allowedExtension = allowedExtensions.find(ext => ext.id === extensionId); - if (!allowedExtension) { - // Add the extension to the allowedExtensions list - const extension = await this.extensionService.getExtension(extensionId); - if (extension) { - allowedExtensions.push({ - id: extensionId, - name: extension.displayName || extension.name, - allowed: true, - trusted: true - }); - } - } else { - // Update the extension to be allowed - allowedExtension.allowed = true; - allowedExtension.trusted = true; - } - } - - if (!allowedExtensions.length) { - this.dialogService.info(nls.localize('noTrustedExtensions', "This account has not been used by any extensions.")); - return; - } - - interface TrustedExtensionsQuickPickItem extends IQuickPickItem { - extension: AllowedExtension; - lastUsed?: number; - } - - const disposableStore = new DisposableStore(); - const quickPick = disposableStore.add(this.quickInputService.createQuickPick()); - quickPick.canSelectMany = true; - quickPick.customButton = true; - quickPick.customLabel = nls.localize('manageTrustedExtensions.cancel', 'Cancel'); - const usages = readAccountUsages(this.storageService, authProvider.id, accountName); - const trustedExtensions = []; - const otherExtensions = []; - for (const extension of allowedExtensions) { - const usage = usages.find(usage => extension.id === usage.extensionId); - extension.lastUsed = usage?.lastUsed; - if (extension.trusted) { - trustedExtensions.push(extension); - } else { - otherExtensions.push(extension); - } - } - - const sortByLastUsed = (a: AllowedExtension, b: AllowedExtension) => (b.lastUsed || 0) - (a.lastUsed || 0); - const toQuickPickItem = function (extension: AllowedExtension) { - const lastUsed = extension.lastUsed; - const description = lastUsed - ? nls.localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(lastUsed, true)) - : nls.localize('notUsed', "Has not used this account"); - let tooltip: string | undefined; - if (extension.trusted) { - tooltip = nls.localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account"); - } - return { - label: extension.name, - extension, - description, - tooltip - }; - }; - const items: Array = [ - ...otherExtensions.sort(sortByLastUsed).map(toQuickPickItem), - { type: 'separator', label: nls.localize('trustedExtensions', "Trusted by Microsoft") }, - ...trustedExtensions.sort(sortByLastUsed).map(toQuickPickItem) - ]; - - quickPick.items = items; - quickPick.selectedItems = items.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator' && (item.extension.allowed === undefined || item.extension.allowed)); - quickPick.title = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"); - quickPick.placeholder = nls.localize('manageExtensions', "Choose which extensions can access this account"); - - disposableStore.add(quickPick.onDidAccept(() => { - const updatedAllowedList = quickPick.items - .filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator') - .map(i => i.extension); - this.storageService.store(`${authProvider.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.APPLICATION, StorageTarget.USER); - this._onDidChangeExtensionSessionAccess.fire({ providerId: authProvider.id, accountName }); - quickPick.hide(); - })); - - disposableStore.add(quickPick.onDidChangeSelection((changed) => { - const trustedItems = new Set(); - quickPick.items.forEach(item => { - const trustItem = item as TrustedExtensionsQuickPickItem; - if (trustItem.extension) { - if (trustItem.extension.trusted) { - trustedItems.add(trustItem); + // When activate has completed, the extension has made the call to `registerAuthenticationProvider`. + // However, activate cannot block on this, so the renderer may not have gotten the event yet. + const didRegister: Promise = new Promise((resolve, _) => { + this.onDidRegisterAuthenticationProvider(e => { + if (e.id === providerId) { + provider = this._authenticationProviders.get(providerId); + if (provider) { + resolve(provider); } else { - trustItem.extension.allowed = false; + throw new Error(`No authentication provider '${providerId}' is currently registered.`); } } }); - changed.forEach((item) => { - item.extension.allowed = true; - trustedItems.delete(item); - }); - - // reselect trusted items if a user tried to unselect one since quick pick doesn't support forcing selection - if (trustedItems.size) { - quickPick.selectedItems = [...changed, ...trustedItems]; - } - })); - - disposableStore.add(quickPick.onDidHide(() => { - disposableStore.dispose(); - })); - - disposableStore.add(quickPick.onDidCustom(() => { - quickPick.hide(); - })); - - quickPick.show(); - } - - async removeAccountSessions(id: string, accountName: string, sessions: AuthenticationSession[]): Promise { - const authProvider = this._authenticationProviders.get(id); - if (!authProvider) { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - - const accountUsages = readAccountUsages(this.storageService, authProvider.id, accountName); - - const { confirmed } = await this.dialogService.confirm({ - type: Severity.Info, - message: accountUsages.length - ? nls.localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName, accountUsages.map(usage => usage.extensionName).join('\n')) - : nls.localize('signOutMessageSimple', "Sign out of '{0}'?", accountName), - primaryButton: nls.localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out") }); - if (confirmed) { - const removeSessionPromises = sessions.map(session => authProvider.removeSession(session.id)); - await Promise.all(removeSessionPromises); - removeAccountUsage(this.storageService, authProvider.id, accountName); - this.storageService.remove(`${authProvider.id}-${accountName}`, StorageScope.APPLICATION); - } + const didTimeout: Promise = new Promise((_, reject) => { + setTimeout(() => { + reject('Timed out waiting for authentication provider to register'); + }, 5000); + }); + + return Promise.race([didRegister, didTimeout]); } } diff --git a/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts new file mode 100644 index 00000000000..8a40ac36958 --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; + +export interface IAccountUsage { + extensionId: string; + extensionName: string; + lastUsed: number; +} + +export const IAuthenticationUsageService = createDecorator('IAuthenticationUsageService'); +export interface IAuthenticationUsageService { + readonly _serviceBrand: undefined; + readAccountUsages(providerId: string, accountName: string,): IAccountUsage[]; + removeAccountUsage(providerId: string, accountName: string): void; + addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void; +} + +export class AuthenticationUsageService implements IAuthenticationUsageService { + _serviceBrand: undefined; + + constructor(@IStorageService private readonly _storageService: IStorageService) { } + + readAccountUsages(providerId: string, accountName: string): IAccountUsage[] { + const accountKey = `${providerId}-${accountName}-usages`; + const storedUsages = this._storageService.get(accountKey, StorageScope.APPLICATION); + let usages: IAccountUsage[] = []; + if (storedUsages) { + try { + usages = JSON.parse(storedUsages); + } catch (e) { + // ignore + } + } + + return usages; + } + removeAccountUsage(providerId: string, accountName: string): void { + const accountKey = `${providerId}-${accountName}-usages`; + this._storageService.remove(accountKey, StorageScope.APPLICATION); + } + addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void { + const accountKey = `${providerId}-${accountName}-usages`; + const usages = this.readAccountUsages(providerId, accountName); + + const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); + if (existingUsageIndex > -1) { + usages.splice(existingUsageIndex, 1, { + extensionId, + extensionName, + lastUsed: Date.now() + }); + } else { + usages.push({ + extensionId, + extensionName, + lastUsed: Date.now() + }); + } + + this._storageService.store(accountKey, JSON.stringify(usages), StorageScope.APPLICATION, StorageTarget.MACHINE); + } +} + +registerSingleton(IAuthenticationUsageService, AuthenticationUsageService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index eaeaffc56ac..6da6e530237 100644 --- a/src/vs/workbench/services/authentication/common/authentication.ts +++ b/src/vs/workbench/services/authentication/common/authentication.ts @@ -58,40 +58,108 @@ export const IAuthenticationService = createDecorator('I export interface IAuthenticationService { readonly _serviceBrand: undefined; + /** + * Fires when an authentication provider has been registered + */ + readonly onDidRegisterAuthenticationProvider: Event; + /** + * Fires when an authentication provider has been unregistered + */ + readonly onDidUnregisterAuthenticationProvider: Event; + + /** + * Fires when the list of sessions for a provider has been added, removed or changed + */ + readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>; + + /** + * Fires when the list of declaredProviders has changed + */ + readonly onDidChangeDeclaredProviders: Event; + + /** + * All providers that have been statically declared by extensions. These may not actually be registered or active yet. + */ + readonly declaredProviders: AuthenticationProviderInformation[]; + + /** + * Registers that an extension has declared an authentication provider in their package.json + * @param provider The provider information to register + */ + registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void; + + /** + * Unregisters a declared authentication provider + * @param id The id of the provider to unregister + */ + unregisterDeclaredAuthenticationProvider(id: string): void; + + /** + * Checks if an authentication provider has been registered + * @param id The id of the provider to check + */ isAuthenticationProviderRegistered(id: string): boolean; - getProviderIds(): string[]; + + /** + * Registers an authentication provider + * @param id The id of the provider + * @param provider The implementation of the provider + */ registerAuthenticationProvider(id: string, provider: IAuthenticationProvider): void; + + /** + * Unregisters an authentication provider + * @param id The id of the provider to unregister + */ unregisterAuthenticationProvider(id: string): void; - isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined; - updateAllowedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string, isAllowed: boolean): void; + + /** + * Gets the provider ids of all registered authentication providers + */ + getProviderIds(): string[]; + + /** + * Gets the provider with the given id. + * @param id The id of the provider to get + * @throws if the provider is not registered + */ + getProvider(id: string): IAuthenticationProvider; + + /** + * Gets all sessions that satisfy the given scopes from the provider with the given id + * @param id The id of the provider to ask for a session + * @param scopes The scopes for the session + * @param activateImmediate If true, the provider should activate immediately if it is not already + */ + getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; + + /** + * Creates an AuthenticationSession with the given provider and scopes + * @param providerId The id of the provider + * @param scopes The scopes to request + * @param options Additional options for creating the session + */ + createSession(providerId: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise; + + /** + * Removes the session with the given id from the provider with the given id + * @param providerId The id of the provider + * @param sessionId The id of the session to remove + */ + removeSession(providerId: string, sessionId: string): Promise; +} + +// TODO: Move this into MainThreadAuthentication +export const IAuthenticationExtensionsService = createDecorator('IAuthenticationExtensionsService'); +export interface IAuthenticationExtensionsService { + readonly _serviceBrand: undefined; + updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void; getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined; removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void; - showGetSessionPrompt(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise; selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): Promise; requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): void; - completeSessionAccessRequest(providerId: string, extensionId: string, extensionName: string, scopes: string[]): Promise; requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; - - readonly onDidRegisterAuthenticationProvider: Event; - readonly onDidUnregisterAuthenticationProvider: Event; - - readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>; - readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }>; - - // TODO completely remove this property - declaredProviders: AuthenticationProviderInformation[]; - readonly onDidChangeDeclaredProviders: Event; - - getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; - getLabel(providerId: string): string; - supportsMultipleAccounts(providerId: string): boolean; - createSession(providerId: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise; - removeSession(providerId: string, sessionId: string): Promise; - - manageTrustedExtensionsForAccount(providerId: string, accountName: string): Promise; - readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[]; - removeAccountSessions(providerId: string, accountName: string, sessions: AuthenticationSession[]): Promise; } export interface IAuthenticationProviderCreateSessionOptions { diff --git a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts new file mode 100644 index 00000000000..180c1ae8a3a --- /dev/null +++ b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter, Event } from 'vs/base/common/event'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { AuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { AuthenticationProviderInformation, AuthenticationSessionsChangeEvent, IAuthenticationProvider } from 'vs/workbench/services/authentication/common/authentication'; +import { TestExtensionService, TestProductService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; + +function createSession() { + return { id: 'session1', accessToken: 'token1', account: { id: 'account', label: 'Account' }, scopes: ['test'] }; +} + +function createProvider(overrides: Partial = {}): IAuthenticationProvider { + return { + supportsMultipleAccounts: false, + onDidChangeSessions: new Emitter().event, + id: 'test', + label: 'Test', + getSessions: async () => [], + createSession: async () => createSession(), + removeSession: async () => { }, + ...overrides + }; +} + +suite('AuthenticationService', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let authenticationService: AuthenticationService; + + setup(() => { + const storageService = disposables.add(new TestStorageService()); + const authenticationAccessService = disposables.add(new AuthenticationAccessService(storageService, TestProductService)); + authenticationService = disposables.add(new AuthenticationService(new TestExtensionService(), authenticationAccessService)); + }); + + teardown(() => { + // Dispose the authentication service after each test + authenticationService.dispose(); + }); + + suite('declaredAuthenticationProviders', () => { + test('registerDeclaredAuthenticationProvider', async () => { + const changed = Event.toPromise(authenticationService.onDidChangeDeclaredProviders); + const provider: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub' + }; + authenticationService.registerDeclaredAuthenticationProvider(provider); + + // Assert that the provider is added to the declaredProviders array and the event fires + assert.equal(authenticationService.declaredProviders.length, 1); + assert.deepEqual(authenticationService.declaredProviders[0], provider); + await changed; + }); + + test('unregisterDeclaredAuthenticationProvider', async () => { + const provider: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub' + }; + authenticationService.registerDeclaredAuthenticationProvider(provider); + const changed = Event.toPromise(authenticationService.onDidChangeDeclaredProviders); + authenticationService.unregisterDeclaredAuthenticationProvider(provider.id); + + // Assert that the provider is removed from the declaredProviders array and the event fires + assert.equal(authenticationService.declaredProviders.length, 0); + await changed; + }); + }); + + suite('authenticationProviders', () => { + test('isAuthenticationProviderRegistered', async () => { + const registered = Event.toPromise(authenticationService.onDidRegisterAuthenticationProvider); + const provider = createProvider(); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), false); + authenticationService.registerAuthenticationProvider(provider.id, provider); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), true); + const result = await registered; + assert.deepEqual(result, { id: provider.id, label: provider.label }); + }); + + test('unregisterAuthenticationProvider', async () => { + const unregistered = Event.toPromise(authenticationService.onDidUnregisterAuthenticationProvider); + const provider = createProvider(); + authenticationService.registerAuthenticationProvider(provider.id, provider); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), true); + authenticationService.unregisterAuthenticationProvider(provider.id); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), false); + const result = await unregistered; + assert.deepEqual(result, { id: provider.id, label: provider.label }); + }); + + test('getProviderIds', () => { + const provider1 = createProvider({ + id: 'provider1', + label: 'Provider 1' + }); + const provider2 = createProvider({ + id: 'provider2', + label: 'Provider 2' + }); + + authenticationService.registerAuthenticationProvider(provider1.id, provider1); + authenticationService.registerAuthenticationProvider(provider2.id, provider2); + + const providerIds = authenticationService.getProviderIds(); + + // Assert that the providerIds array contains the registered provider ids + assert.deepEqual(providerIds, [provider1.id, provider2.id]); + }); + + test('getProvider', () => { + const provider = createProvider(); + + authenticationService.registerAuthenticationProvider(provider.id, provider); + + const retrievedProvider = authenticationService.getProvider(provider.id); + + // Assert that the retrieved provider is the same as the registered provider + assert.deepEqual(retrievedProvider, provider); + }); + }); + + suite('authenticationSessions', () => { + test('getSessions', async () => { + let isCalled = false; + const provider = createProvider({ + getSessions: async () => { + isCalled = true; + return [createSession()]; + }, + }); + authenticationService.registerAuthenticationProvider(provider.id, provider); + const sessions = await authenticationService.getSessions(provider.id); + + assert.equal(sessions.length, 1); + assert.ok(isCalled); + }); + + test('createSession', async () => { + const emitter = new Emitter(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + createSession: async () => { + const session = createSession(); + emitter.fire({ added: [session], removed: [], changed: [] }); + return session; + }, + }); + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + authenticationService.registerAuthenticationProvider(provider.id, provider); + const session = await authenticationService.createSession(provider.id, ['repo']); + + // Assert that the created session matches the expected session and the event fires + assert.ok(session); + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [session], removed: [], changed: [] } + }); + }); + + test('removeSession', async () => { + const emitter = new Emitter(); + const session = createSession(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + removeSession: async () => emitter.fire({ added: [], removed: [session], changed: [] }) + }); + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + authenticationService.registerAuthenticationProvider(provider.id, provider); + await authenticationService.removeSession(provider.id, session.id); + + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [], removed: [session], changed: [] } + }); + }); + + test('onDidChangeSessions', async () => { + const emitter = new Emitter(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + getSessions: async () => [] + }); + authenticationService.registerAuthenticationProvider(provider.id, provider); + + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + const session = createSession(); + emitter.fire({ added: [], removed: [], changed: [session] }); + + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [], removed: [], changed: [session] } + }); + }); + }); +}); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 8b833a0ce2a..40cea524423 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -641,7 +641,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") }); for (const authenticationProvider of authenticationProviders) { const accounts = (allAccounts.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.currentSessionId ? -1 : 1); - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + const providerName = this.authenticationService.getProvider(authenticationProvider.id).label; for (const account of accounts) { quickPickItems.push({ label: `${account.accountName} (${providerName})`, @@ -656,8 +656,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat // Account Providers for (const authenticationProvider of authenticationProviders) { - if (!allAccounts.has(authenticationProvider.id) || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + const provider = this.authenticationService.getProvider(authenticationProvider.id); + if (!allAccounts.has(authenticationProvider.id) || provider.supportsMultipleAccounts) { + const providerName = provider.label; quickPickItems.push({ label: localize('sign in using account', "Sign in with {0}", providerName), authenticationProvider }); } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 61028e388dd..cdc45b0880b 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -104,6 +104,9 @@ import 'vs/workbench/services/views/browser/viewsService'; import 'vs/workbench/services/quickinput/browser/quickInputService'; import 'vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService'; import 'vs/workbench/services/authentication/browser/authenticationService'; +import 'vs/workbench/services/authentication/browser/authenticationExtensionsService'; +import 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import 'vs/workbench/services/authentication/browser/authenticationAccessService'; import 'vs/editor/browser/services/hoverService/hoverService'; import 'vs/workbench/services/assignment/common/assignmentService'; import 'vs/workbench/services/outline/browser/outlineService'; @@ -341,6 +344,9 @@ import 'vs/workbench/contrib/languageDetection/browser/languageDetection.contrib // Language Status import 'vs/workbench/contrib/languageStatus/browser/languageStatus.contribution'; +// Authentication +import 'vs/workbench/contrib/authentication/browser/authentication.contribution'; + // User Data Sync import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; From 00abefa3e27ee1866cf6a6ac825578c1ddafd32c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 29 Feb 2024 11:52:12 -0300 Subject: [PATCH 130/228] Add chatParticipant contribution point (#206474) * Add package.json registration for chat agents * Update for tests * Separate static and dynamic chat agent parts * Handle participant registration correctly * Fix tests * Fix test * Remove commented code * Fix more tests * Pluralize * Pluralize test contribution --- extensions/vscode-api-tests/package.json | 13 ++ .../src/singlefolder-tests/chat.test.ts | 10 -- .../api/browser/mainThreadChatAgents2.ts | 54 +++--- .../workbench/api/common/extHost.protocol.ts | 12 +- .../api/common/extHostChatAgents2.ts | 70 +------- .../contrib/chat/browser/chat.contribution.ts | 56 +++--- .../browser/chatContributionServiceImpl.ts | 99 ++++++++++- .../contrib/chat/browser/chatListRenderer.ts | 4 +- .../browser/contrib/chatInputEditorContrib.ts | 23 +-- .../contrib/chat/common/chatAgents.ts | 166 +++++++++++++----- .../chat/common/chatContributionService.ts | 25 +++ .../contrib/chat/common/chatModel.ts | 7 +- .../contrib/chat/common/chatParserTypes.ts | 4 +- .../contrib/chat/common/chatRequestParser.ts | 5 +- .../contrib/chat/common/chatServiceImpl.ts | 17 +- .../contrib/chat/common/voiceChat.ts | 15 +- ..._agent_and_subcommand_after_newline.0.snap | 8 +- ..._subcommand_with_leading_whitespace.0.snap | 8 +- ...uestParser_agent_with_question_mark.0.snap | 8 +- ...er_agent_with_subcommand_after_text.0.snap | 8 +- ...hatRequestParser_agents__subCommand.0.snap | 8 +- ..._agents_and_variables_and_multiline.0.snap | 8 +- ..._and_variables_and_multiline__part2.0.snap | 8 +- .../__snapshots__/Chat_can_deserialize.0.snap | 5 +- .../__snapshots__/Chat_can_serialize.1.snap | 5 +- .../test/common/chatRequestParser.test.ts | 20 +-- .../chat/test/common/chatService.test.ts | 32 ++-- .../common/mockChatContributionService.ts | 32 ++++ .../chat/test/common/voiceChat.test.ts | 15 +- .../browser/inlineChatController.ts | 4 +- .../vscode.proposed.chatParticipant.d.ts | 53 ------ ...ode.proposed.chatParticipantAdditions.d.ts | 31 ++-- 32 files changed, 477 insertions(+), 356 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index e9323fc9c43..b0b56a3fdd9 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -64,6 +64,19 @@ }, "icon": "media/icon.png", "contributes": { + "chatParticipants": [ + { + "name": "participant", + "description": "test", + "isDefault": true, + "commands": [ + { + "name": "hello", + "description": "Hello" + } + ] + } + ], "configuration": { "type": "object", "title": "Test Config", 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 6fb0262e132..9877eb1dd92 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -45,11 +45,6 @@ suite('chat', () => { return null; }); participant.isDefault = true; - participant.commandProvider = { - provideCommands: (_token) => { - return [{ name: 'hello', description: 'Hello' }]; - } - }; disposables.push(participant); return emitter.event; } @@ -102,11 +97,6 @@ suite('chat', () => { return { metadata: { key: 'value' } }; }); participant.isDefault = true; - participant.commandProvider = { - provideCommands: (_token) => { - return [{ name: 'hello', description: 'Hello' }]; - } - }; participant.followupProvider = { provideFollowups(result, _token) { deferred.complete(result); diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index c143be65078..cf3d1e134dd 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -19,8 +19,8 @@ import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IExtensionCh import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -29,7 +29,6 @@ import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/ext type AgentData = { dispose: () => void; name: string; - hasSlashCommands?: boolean; hasFollowups?: boolean; }; @@ -49,6 +48,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatContributionService private readonly _chatContributionService: IChatContributionService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -76,12 +76,13 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agents.deleteAndDispose(handle); } - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void { - const lastSlashCommands: WeakMap = new WeakMap(); - const d = this._chatAgentService.registerAgent({ - id: name, - extensionId: extension, - metadata: revive(metadata), + $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void { + const staticAgentRegistration = this._chatContributionService.registeredParticipants.find(p => p.extensionId.value === extension.value && p.name === name); + if (!staticAgentRegistration && !allowDynamic) { + throw new Error(`chatParticipant must be declared in package.json: ${name}`); + } + + const impl: IChatAgentImplementation = { invoke: async (request, progress, history, token) => { this._pendingProgress.set(request.requestId, progress); try { @@ -97,31 +98,31 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return this._proxy.$provideFollowups(request, handle, result, token); }, - getLastSlashCommands: (model: IChatModel) => { - return lastSlashCommands.get(model); - }, - provideSlashCommands: async (model, history, token) => { - if (!this._agents.get(handle)?.hasSlashCommands) { - return []; // save an IPC call - } - const commands = await this._proxy.$provideSlashCommands(handle, { history }, token); - if (model) { - lastSlashCommands.set(model, commands); - } - - return commands; - }, provideWelcomeMessage: (token: CancellationToken) => { return this._proxy.$provideWelcomeMessage(handle, token); }, provideSampleQuestions: (token: CancellationToken) => { return this._proxy.$provideSampleQuestions(handle, token); } - }); + }; + + let disposable: IDisposable; + if (!staticAgentRegistration && allowDynamic) { + disposable = this._chatAgentService.registerDynamicAgent( + { + id: name, + extensionId: extension, + metadata: revive(metadata), + slashCommands: [], + }, + impl); + } else { + disposable = this._chatAgentService.registerAgent(name, impl); + } + this._agents.set(handle, { name, - dispose: d.dispose, - hasSlashCommands: metadata.hasSlashCommands, + dispose: disposable.dispose, hasFollowups: metadata.hasFollowups }); } @@ -131,7 +132,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA if (!data) { throw new Error(`No agent with handle ${handle} registered`); } - data.hasSlashCommands = metadataUpdate.hasSlashCommands; data.hasFollowups = metadataUpdate.hasFollowups; this._chatAgentService.updateAgent(data.name, revive(metadataUpdate)); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index dda01854f21..d64dbc0af3f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -50,12 +50,12 @@ import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; -import { IChatAgentCommand, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; -import { IChatDynamicRequest, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatDynamicRequest, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { DebugConfigurationProviderTriggerKind, MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; +import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import { IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatFollowup, IInlineChatProgressItem, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -1196,12 +1196,11 @@ export interface ExtHostLanguageModelsShape { } export interface IExtensionChatAgentMetadata extends Dto { - hasSlashCommands?: boolean; hasFollowups?: boolean; } export interface MainThreadChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void; + $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void; $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1228,7 +1227,6 @@ export type IChatAgentHistoryEntryDto = { export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 8166c1e7d33..13bedf2b1e8 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -21,7 +21,7 @@ import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEnt 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 { IChatAgentCommand, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatFollowup, IChatProgress, 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'; @@ -171,7 +171,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, name, {}); + this._proxy.$registerAgent(handle, extension.identifier, name, {}, isProposedApiEnabled(extension, 'chatParticipantAdditions')); return agent.apiAgent; } @@ -245,23 +245,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._sessionDisposables.deleteAndDispose(sessionId); } - async $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - // this is OK, the agent might have disposed while the request was in flight - return []; - } - - const convertedHistory = await this.prepareHistoryTurns(agent.id, context); - try { - return await agent.provideSlashCommands({ history: convertedHistory }, token); - } catch (err) { - const msg = toErrorMessage(err); - this._logService.error(`[${agent.extension.identifier.value}] [@${agent.id}] Error while providing slash commands: ${msg}`); - return []; - } - } - async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { @@ -276,7 +259,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._agents.values(), a => a.id === f.participant && ExtensionIdentifier.equals(a.extension.identifier, agent.extension.identifier)); if (!isValid) { - this._logService.warn(`[@${agent.id}] ChatFollowup refers to an invalid participant: ${f.participant}`); + this._logService.warn(`[@${agent.id}] ChatFollowup refers to an unknown participant: ${f.participant}`); } return isValid; }) @@ -352,7 +335,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { class ExtHostChatAgent { - private _commandProvider: vscode.ChatCommandProvider | undefined; private _followupProvider: vscode.ChatFollowupProvider | undefined; private _description: string | undefined; private _fullName: string | undefined; @@ -394,30 +376,6 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } - async provideSlashCommands(context: vscode.ChatContext, token: CancellationToken): Promise { - if (!this._commandProvider) { - return []; - } - const result = await this._commandProvider.provideCommands(context, token); - if (!result) { - return []; - } - return result - .map(c => { - if ('isSticky2' in c) { - checkProposedApiEnabled(this.extension, 'chatParticipantAdditions'); - } - - return { - name: c.name, - description: c.description ?? '', - followupPlaceholder: c.isSticky2?.placeholder, - isSticky: c.isSticky2?.isSticky ?? c.isSticky, - sampleRequest: c.sampleRequest - } satisfies IChatAgentCommand; - }); - } - async provideFollowups(result: vscode.ChatResult, token: CancellationToken): Promise { if (!this._followupProvider) { return []; @@ -485,9 +443,7 @@ class ExtHostChatAgent { 'dark' in this._iconPath ? this._iconPath.dark : undefined, themeIcon: this._iconPath instanceof extHostTypes.ThemeIcon ? this._iconPath : undefined, - hasSlashCommands: this._commandProvider !== undefined, hasFollowups: this._followupProvider !== undefined, - isDefault: this._isDefault, isSecondary: this._isSecondary, helpTextPrefix: (!this._helpTextPrefix || typeof this._helpTextPrefix === 'string') ? this._helpTextPrefix : typeConvert.MarkdownString.from(this._helpTextPrefix), helpTextVariablesPrefix: (!this._helpTextVariablesPrefix || typeof this._helpTextVariablesPrefix === 'string') ? this._helpTextVariablesPrefix : typeConvert.MarkdownString.from(this._helpTextVariablesPrefix), @@ -535,13 +491,6 @@ class ExtHostChatAgent { assertType(typeof v === 'function', 'Invalid request handler'); that._requestHandler = v; }, - get commandProvider() { - return that._commandProvider; - }, - set commandProvider(v) { - that._commandProvider = v; - updateMetadataSoon(); - }, get followupProvider() { return that._followupProvider; }, @@ -564,10 +513,6 @@ class ExtHostChatAgent { }, set helpTextPrefix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextPrefix is only available on the default chat agent'); - } - that._helpTextPrefix = v; updateMetadataSoon(); }, @@ -577,10 +522,6 @@ class ExtHostChatAgent { }, set helpTextVariablesPrefix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextVariablesPrefix is only available on the default chat agent'); - } - that._helpTextVariablesPrefix = v; updateMetadataSoon(); }, @@ -590,10 +531,6 @@ class ExtHostChatAgent { }, set helpTextPostfix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextPostfix is only available on the default chat agent'); - } - that._helpTextPostfix = v; updateMetadataSoon(); }, @@ -664,7 +601,6 @@ class ExtHostChatAgent { }, dispose() { disposed = true; - that._commandProvider = undefined; that._followupProvider = undefined; that._onDidReceiveFeedback.dispose(); that._proxy.$unregisterAgent(that._handle); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 0d34b49aff8..000c78ced65 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -3,62 +3,61 @@ * 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 { 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'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; -import { IWorkbenchContributionsRegistry, WorkbenchPhase, Extensions as WorkbenchExtensions, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +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 { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; 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 } 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 { 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'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; import { IChatAccessibilityService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; +import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +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 { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; -import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; -import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; -import { LanguageModelsService, ILanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; -import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; -import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; -import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; -import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -246,7 +245,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { executeImmediately: true }, async (prompt, progress) => { const defaultAgent = chatAgentService.getDefaultAgent(); - const agents = chatAgentService.getAgents(); + const agents = chatAgentService.getRegisteredAgents(); // Report prefix if (defaultAgent?.metadata.helpTextPrefix) { @@ -266,8 +265,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`; - const commands = await a.provideSlashCommands(undefined, [], CancellationToken.None); - const commandText = commands.map(c => { + const commandText = a.slashCommands.map(c => { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${c.description}`; diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 8498292395a..3e70653b2f0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -18,10 +18,9 @@ import { getNewChatAction } from 'vs/workbench/contrib/chat/browser/actions/chat import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { IChatContributionService, IChatProviderContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; - const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'interactiveSession', jsonSchema: { @@ -59,6 +58,71 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi }, }); +const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'chatParticipants', + jsonSchema: { + description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a Chat Participant'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name'], + properties: { + name: { + description: localize('chatParticipantName', "Unique name for this Chat Participant."), + type: 'string' + }, + description: { + 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', + }, + commands: { + markdownDescription: localize('chatCommandsDescription', "Commands available for this Chat Participant, which the user can invoke with a `/`."), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name', 'description'], + properties: { + name: { + description: localize('chatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."), + type: 'string' + }, + description: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + when: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + sampleRequest: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + isSticky: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'boolean' + }, + } + } + } + } + } + }, + activationEventsGenerator: (contributions: IRawChatParticipantContribution[], result: { push(item: string): void }) => { + for (const contrib of contributions) { + result.push(`onChatParticipant:${contrib.name}`); + } + }, +}); + export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; @@ -96,6 +160,20 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { } } }); + + chatParticipantExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const providerDescriptor of extension.value) { + this._chatContributionService.registerChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + } + } + + for (const extension of delta.removed) { + for (const providerDescriptor of extension.value) { + this._chatContributionService.deregisterChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + } + } + }); } private registerViewContainer(): ViewContainer { @@ -156,10 +234,15 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +function getParticipantKey(participant: IChatParticipantContribution): string { + return `${participant.extensionId.value}_${participant.name}`; +} + export class ChatContributionService implements IChatContributionService { declare _serviceBrand: undefined; private _registeredProviders = new Map(); + private _registeredParticipants = new Map(); constructor( ) { } @@ -176,7 +259,19 @@ export class ChatContributionService implements IChatContributionService { this._registeredProviders.delete(providerId); } + public registerChatParticipant(participant: IChatParticipantContribution): void { + this._registeredParticipants.set(getParticipantKey(participant), participant); + } + + public deregisterChatParticipant(participant: IChatParticipantContribution): void { + this._registeredParticipants.delete(getParticipantKey(participant)); + } + public get registeredProviders(): IChatProviderContribution[] { return Array.from(this._registeredProviders.values()); } + + public get registeredParticipants(): IChatParticipantContribution[] { + return Array.from(this._registeredParticipants.values()); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 93b651276e5..94b0a9b2dc1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -360,7 +360,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer !a.metadata.isDefault); + const agents = this.chatAgentService.getRegisteredAgents() + .filter(a => !a.isDefault); return { suggestions: agents.map((c, i) => { const withAt = `@${c.id}`; @@ -399,10 +397,8 @@ class AgentCompletions extends Disposable { } const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; - const commands = await usedAgent.agent.provideSlashCommands(widget.viewModel.model, getHistoryEntriesFromModel(widget.viewModel.model), token); // Refresh the cache here - return { - suggestions: commands.map((c, i) => { + suggestions: usedAgent.agent.slashCommands.map((c, i) => { const withSlash = `/${c.name}`; return { label: withSlash, @@ -432,16 +428,9 @@ class AgentCompletions extends Disposable { return null; } - const agents = this.chatAgentService.getAgents(); - const all = agents.map(agent => agent.provideSlashCommands(viewModel.model, getHistoryEntriesFromModel(viewModel.model), token)); - const commands = await raceCancellation(Promise.all(all), token); - - if (!commands) { - return; - } - + const agents = this.chatAgentService.getRegisteredAgents(); const justAgents: CompletionItem[] = agents - .filter(a => !a.metadata.isDefault) + .filter(a => !a.isDefault) .map(agent => { const agentLabel = `${chatAgentLeader}${agent.id}`; return { @@ -456,7 +445,7 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( - agents.flatMap((agent, i) => commands[i].map((c, i) => { + agents.flatMap(agent => agent.slashCommands.map((c, i) => { const agentLabel = `${chatAgentLeader}${agent.id}`; const withSlash = `${chatSubcommandLeader}${c.name}`; return { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index b1ef64dae9d..b9c3a5d4508 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -11,9 +11,11 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatModel, IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc @@ -27,18 +29,21 @@ export interface IChatAgentHistoryEntry { export interface IChatAgentData { id: string; extensionId: ExtensionIdentifier; + /** The agent invoked when no agent is specified */ + isDefault?: boolean; metadata: IChatAgentMetadata; + slashCommands: IChatAgentCommand[]; } -export interface IChatAgent extends IChatAgentData { +export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; - getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined; - provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; provideSampleQuestions?(token: CancellationToken): ProviderResult; } +export type IChatAgent = IChatAgentData & IChatAgentImplementation; + export interface IChatAgentCommand { name: string; description: string; @@ -68,7 +73,6 @@ export interface IChatAgentCommand { export interface IChatAgentMetadata { description?: string; - isDefault?: boolean; // The agent invoked when no agent is specified helpTextPrefix?: string | IMarkdownString; helpTextVariablesPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; @@ -108,17 +112,18 @@ export const IChatAgentService = createDecorator('chatAgentSe export interface IChatAgentService { _serviceBrand: undefined; /** - * undefined when an agent was removed + * undefined when an agent was removed IChatAgent */ readonly onDidChangeAgents: Event; - registerAgent(agent: IChatAgent): IDisposable; + registerAgent(name: string, agent: IChatAgentImplementation): IDisposable; + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; - getAgents(): Array; - getAgent(id: string): IChatAgent | undefined; + getRegisteredAgents(): Array; + getActivatedAgents(): Array; + getRegisteredAgent(id: string): IChatAgentData | undefined; getDefaultAgent(): IChatAgent | undefined; - getSecondaryAgent(): IChatAgent | undefined; - hasAgent(id: string): boolean; + getSecondaryAgent(): IChatAgentData | undefined; updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } @@ -128,79 +133,160 @@ export class ChatAgentService extends Disposable implements IChatAgentService { declare _serviceBrand: undefined; - private readonly _agents = new Map(); + private readonly _agents = new Map(); private readonly _onDidChangeAgents = this._register(new Emitter()); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; + constructor( + @IChatContributionService private chatContributionService: IChatContributionService, + @IContextKeyService private contextKeyService: IContextKeyService, + ) { + super(); + } + override dispose(): void { super.dispose(); this._agents.clear(); } - registerAgent(agent: IChatAgent): IDisposable { - if (this._agents.has(agent.id)) { - throw new Error(`Already registered an agent with id ${agent.id}`); + registerAgent(name: string, agentImpl: IChatAgentImplementation): IDisposable { + if (this._agents.has(name)) { + // TODO not keyed by name, dupes allowed between extensions + throw new Error(`Already registered an agent with id ${name}`); } - this._agents.set(agent.id, { agent }); - this._onDidChangeAgents.fire(agent); + + const data = this.getRegisteredAgent(name); + if (!data) { + throw new Error(`Unknown agent: ${name}`); + } + + const agent = { data: data, impl: agentImpl }; + this._agents.set(name, agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); return toDisposable(() => { - if (this._agents.delete(agent.id)) { + if (this._agents.delete(name)) { + this._onDidChangeAgents.fire(undefined); + } + }); + } + + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { + const agent = { data, impl: agentImpl }; + this._agents.set(data.id, agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); + + return toDisposable(() => { + if (this._agents.delete(data.id)) { this._onDidChangeAgents.fire(undefined); } }); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { - const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id} registered`); + const agent = this._agents.get(id); + if (!agent?.impl) { + throw new Error(`No activated agent with id ${id} registered`); } - data.agent.metadata = { ...data.agent.metadata, ...updateMetadata }; - this._onDidChangeAgents.fire(data.agent); + agent.data.metadata = { ...agent.data.metadata, ...updateMetadata }; + this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); } getDefaultAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isDefault)?.agent; + return this.getActivatedAgents().find(a => !!a.isDefault); } - getSecondaryAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isSecondary)?.agent; + getSecondaryAgent(): IChatAgentData | undefined { + // TODO also static + return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; } - getAgents(): Array { - return Array.from(this._agents.values(), v => v.agent); + getRegisteredAgents(): Array { + const that = this; + return this.chatContributionService.registeredParticipants.map(p => ( + { + extensionId: p.extensionId, + id: p.name, + metadata: { description: p.description }, + isDefault: p.isDefault, + get slashCommands() { + const commands = p.commands ?? []; + return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when))); + } + } satisfies IChatAgentData)); } - hasAgent(id: string): boolean { - return this._agents.has(id); + getActivatedAgents(): IChatAgent[] { + return Array.from(this._agents.values()) + .filter(a => !!a.impl) + .map(a => new MergedChatAgent(a.data, a.impl!)); } - getAgent(id: string): IChatAgent | undefined { - const data = this._agents.get(id); - return data?.agent; + getRegisteredAgent(id: string): IChatAgentData | undefined { + return this.getRegisteredAgents().find(a => a.id === id); } async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + if (!data?.impl) { + throw new Error(`No activated agent with id ${id}`); } - return await data.agent.invoke(request, progress, history, token); + return await data.impl.invoke(request, progress, history, token); } async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + if (!data?.impl) { + throw new Error(`No activated agent with id ${id}`); } - if (!data.agent.provideFollowups) { + if (!data.impl?.provideFollowups) { return []; } - return data.agent.provideFollowups(request, result, token); + return data.impl.provideFollowups(request, result, token); + } +} + +export class MergedChatAgent implements IChatAgent { + constructor( + private readonly data: IChatAgentData, + private readonly impl: IChatAgentImplementation + ) { } + + get id(): string { return this.data.id; } + get extensionId(): ExtensionIdentifier { return this.data.extensionId; } + get isDefault(): boolean | undefined { return this.data.isDefault; } + get metadata(): IChatAgentMetadata { return this.data.metadata; } + get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } + + async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + return this.impl.invoke(request, progress, history, token); + } + + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { + if (this.impl.provideFollowups) { + return this.impl.provideFollowups(request, result, token); + } + + return []; + } + + provideWelcomeMessage(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { + if (this.impl.provideWelcomeMessage) { + return this.impl.provideWelcomeMessage(token); + } + + return undefined; + } + + provideSampleQuestions(token: CancellationToken): ProviderResult { + if (this.impl.provideSampleQuestions) { + return this.impl.provideSampleQuestions(token); + } + + return undefined; } } diff --git a/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/src/vs/workbench/contrib/chat/common/chatContributionService.ts index 2c43c2af703..ca022f35aa0 100644 --- a/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/src/vs/workbench/contrib/chat/common/chatContributionService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IChatProviderContribution { @@ -18,6 +19,10 @@ export interface IChatContributionService { registerChatProvider(provider: IChatProviderContribution): void; deregisterChatProvider(providerId: string): void; getViewIdForProvider(providerId: string): string; + + readonly registeredParticipants: IChatParticipantContribution[]; + registerChatParticipant(participant: IChatParticipantContribution): void; + deregisterChatParticipant(participant: IChatParticipantContribution): void; } export interface IRawChatProviderContribution { @@ -26,3 +31,23 @@ export interface IRawChatProviderContribution { icon?: string; when?: string; } + +export interface IRawChatCommandContribution { + name: string; + description: string; + sampleRequest?: string; + isSticky?: boolean; + when?: string; +} + +export interface IRawChatParticipantContribution { + name: string; + description?: string; + isDefault?: boolean; + commands?: IRawChatCommandContribution[]; +} + +export interface IChatParticipantContribution extends IRawChatParticipantContribution { + // Participant id is extensionId + name + extensionId: ExtensionIdentifier; +} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index c8e47cb8b16..ed06c32958f 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -683,7 +683,7 @@ export class ChatModel extends Disposable implements IChatModel { } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); } else if (progress.kind === 'agentDetection') { - const agent = this.chatAgentService.getAgent(progress.agentName); + const agent = this.chatAgentService.getRegisteredAgent(progress.agentName); if (agent) { request.response.setAgent(agent, progress.command); } @@ -780,7 +780,10 @@ export class ChatModel extends Disposable implements IChatModel { followups: r.response?.followups, isCanceled: r.response?.isCanceled, vote: r.response?.vote, - agent: r.response?.agent ? { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata } : undefined, // May actually be the full IChatAgent instance, just take the data props + agent: r.response?.agent ? + // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. + { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [] } + : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, contentReferences: r.response?.contentReferences diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 4f7654fd56a..e6bb4e56bf9 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 { IChatAgent, IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentCommand, IChatAgentData } 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'; @@ -72,7 +72,7 @@ export class ChatRequestVariablePart implements IParsedChatRequestPart { export class ChatRequestAgentPart implements IParsedChatRequestPart { static readonly Kind = 'agent'; readonly kind = ChatRequestAgentPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgent) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } get text(): string { return `${chatAgentLeader}${this.agent.id}`; diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index aa05f52b6d8..ca2057b6342 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -99,7 +99,7 @@ export class ChatRequestParser { const varRange = new OffsetRange(offset, offset + full.length); const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const agent = this.agentService.getAgent(name); + const agent = this.agentService.getRegisteredAgent(name); if (!agent) { return; } @@ -171,8 +171,7 @@ export class ChatRequestParser { return; } - const subCommands = usedAgent.agent.getLastSlashCommands(model); - const subCommand = subCommands?.find(c => c.name === command); + const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command); if (subCommand) { // Valid agent subcommand return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index e28b1e14e28..a4a77bb0ba2 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -20,15 +20,15 @@ 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'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, 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 { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const serializedChatKey = 'interactive.sessions'; @@ -193,12 +193,6 @@ export class ChatService extends Disposable implements IChatService { } this._register(storageService.onWillSaveState(() => this.saveState())); - - this._register(Event.debounce(this.chatAgentService.onDidChangeAgents, () => { }, 500)(() => { - for (const model of this._sessionModels.values()) { - this.warmSlashCommandCache(model); - } - })); } private saveState(): void { @@ -364,15 +358,9 @@ export class ChatService extends Disposable implements IChatService { this.initializeSession(model, CancellationToken.None); } - private warmSlashCommandCache(model: IChatModel, agent?: IChatAgent) { - const agents = agent ? [agent] : this.chatAgentService.getAgents(); - agents.forEach(agent => agent.provideSlashCommands(model, [], CancellationToken.None)); - } - private async initializeSession(model: ChatModel, token: CancellationToken): Promise { try { this.trace('initializeSession', `Initialize session ${model.sessionId}`); - this.warmSlashCommandCache(model); model.startInitialize(); await this.extensionService.activateByEvent(`onInteractiveSession:${model.providerId}`); @@ -543,6 +531,7 @@ export class ChatService extends Disposable implements IChatService { const defaultAgent = this.chatAgentService.getDefaultAgent(); if (agentPart || (defaultAgent && !commandPart)) { const agent = (agentPart?.agent ?? defaultAgent)!; + await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); const history = getHistoryEntriesFromModel(model); const initVariableData: IChatRequestVariableData = { variables: [] }; diff --git a/src/vs/workbench/contrib/chat/common/voiceChat.ts b/src/vs/workbench/contrib/chat/common/voiceChat.ts index 9c92a44354a..5f7208cdd79 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChat.ts @@ -87,19 +87,16 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private createPhrases(model?: IChatModel): Map { const phrases = new Map(); - for (const agent of this.chatAgentService.getAgents()) { + for (const agent of this.chatAgentService.getActivatedAgents()) { const agentPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]} ${VoiceChatService.CHAT_AGENT_ALIAS.get(agent.id) ?? agent.id}`.toLowerCase(); phrases.set(agentPhrase, { agent: agent.id }); - const commands = model && agent.getLastSlashCommands(model); - if (commands) { - for (const slashCommand of commands) { - const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); - phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); + for (const slashCommand of agent.slashCommands) { + const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); + phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); - const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); - phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); - } + const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); + phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); } } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index 4a241114279..ae1818689d0 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -28,8 +28,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index becd9bf6f31..190c3555054 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -28,8 +28,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index 50c67ea58d0..65d00490273 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index 345e8c874de..6585ff1e0e5 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index 406e20cfe55..fc2a622c9ac 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, 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 31fd0b94e96..6f9eaa531cf 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 @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, 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 85bc82a3136..fee5731f3e3 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 @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap index d58da8b3744..9d4646eb6a9 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap @@ -23,7 +23,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,8 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ] }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap index 3a6b248a792..98d57a6bd2b 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap @@ -22,7 +22,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,8 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ] }, slashCommand: undefined, usedContext: { 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 ba397535bff..e82c0f84940 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -9,7 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ChatAgentService, IChatAgent, IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentService, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -112,12 +112,12 @@ suite('ChatRequestParser', () => { }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return >{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => [], getLastSlashCommands: () => slashCommands }; + return { id: 'agent', metadata: { description: '' }, slashCommands }; }; test('agent with subcommand after text', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -127,7 +127,7 @@ suite('ChatRequestParser', () => { test('agents, subCommand', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -137,7 +137,7 @@ suite('ChatRequestParser', () => { test('agent with question mark', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -147,7 +147,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand with leading whitespace', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -157,7 +157,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand after newline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -167,7 +167,7 @@ suite('ChatRequestParser', () => { test('agent not first', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -177,7 +177,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); @@ -189,7 +189,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline, part2', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 3a2b6abc998..0cd1c90c3ce 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -21,7 +21,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { ChatAgentService, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChat, IChatFollowup, IChatProgress, IChatProvider, IChatRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -32,6 +32,7 @@ import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/ import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; class SimpleTestProvider extends Disposable implements IChatProvider { private static sessionId = 0; @@ -60,12 +61,7 @@ const chatAgentWithUsedContext: IChatAgent = { id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, metadata: {}, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands() { - return []; - }, + slashCommands: [], async invoke(request, progress, history, token) { progress({ documents: [ @@ -109,25 +105,21 @@ suite('Chat', () => { instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IChatContributionService, new MockChatContributionService( + [ + { extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }, + { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId, isDefault: true }, + ])); chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); instantiationService.stub(IChatAgentService, chatAgentService); const agent = { - id: 'testAgent', - extensionId: nullExtensionDescription.identifier, - metadata: { isDefault: true }, async invoke(request, progress, history, token) { return {}; }, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands(token) { - return []; - }, - } as IChatAgent; - testDisposables.add(chatAgentService.registerAgent(agent)); + } satisfies IChatAgentImplementation; + testDisposables.add(chatAgentService.registerAgent('testAgent', agent)); }); test('retrieveSession', async () => { @@ -229,7 +221,7 @@ suite('Chat', () => { }); test('can serialize', async () => { - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); @@ -249,7 +241,7 @@ suite('Chat', () => { test('can deserialize', async () => { let serializedChatData: ISerializableChatData; - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); // create the first service, send request, get response, and serialize the state { // serapate block to not leak variables in outer scope diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts new file mode 100644 index 00000000000..e1adddb2dec --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; + +export class MockChatContributionService implements IChatContributionService { + _serviceBrand: undefined; + + constructor( + public readonly registeredParticipants: IChatParticipantContribution[] = [] + ) { } + + registeredProviders: IChatProviderContribution[] = []; + registerChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + + registerChatProvider(provider: IChatProviderContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatProvider(providerId: string): void { + throw new Error('Method not implemented.'); + } + getViewIdForProvider(providerId: string): string { + throw new Error('Method not implemented.'); + } +} 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 c85e0056d12..864ba72ac0d 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -11,7 +11,7 @@ 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 { IChatAgent, IChatAgentCommand, IChatAgentHistoryEntry, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { 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'; @@ -28,10 +28,8 @@ suite('VoiceChat', () => { extensionId: ExtensionIdentifier = nullExtensionDescription.identifier; - constructor(readonly id: string, private readonly lastSlashCommands: IChatAgentCommand[]) { } - getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined { return this.lastSlashCommands; } + constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); } metadata = {}; } @@ -49,14 +47,15 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; - registerAgent(agent: IChatAgent): IDisposable { throw new Error(); } + registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { throw new Error(); } - getAgents(): Array { return agents; } - getAgent(id: string): IChatAgent | undefined { throw new Error(); } + getRegisteredAgents(): Array { return agents; } + getActivatedAgents(): IChatAgent[] { return agents; } + getRegisteredAgent(id: string): IChatAgent | undefined { throw new Error(); } getDefaultAgent(): IChatAgent | undefined { throw new Error(); } getSecondaryAgent(): IChatAgent | undefined { throw new Error(); } - hasAgent(id: string): boolean { throw new Error(); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 775b62fee68..cfb18aa49f0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -520,8 +520,8 @@ export class InlineChatController implements IEditorContribution { const withoutSubCommandLeader = input.slice(1); const cts = new CancellationTokenSource(); this._sessionStore.add(cts); - for (const agent of this._chatAgentService.getAgents()) { - const commands = await agent.provideSlashCommands(undefined, [], cts.token); + for (const agent of this._chatAgentService.getActivatedAgents()) { + const commands = agent.slashCommands; if (commands.find((command) => withoutSubCommandLeader.startsWith(command.name))) { massagedInput = `${chatAgentLeader}${agent.id} ${input}`; break; diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index 5fcc77cb7b5..b6794145df2 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -143,49 +143,6 @@ declare module 'vscode' { readonly kind: ChatResultFeedbackKind; } - export interface ChatCommand { - /** - * A short name by which this command is referred to in the UI, e.g. `fix` or - * `explain` for commands that fix an issue or explain code. - * - * **Note**: The name should be unique among the commands provided by this participant. - */ - readonly name: string; - - /** - * Human-readable description explaining what this command does. - */ - readonly description: string; - - /** - * When the user clicks this command in `/help`, this text will be submitted to this command - */ - readonly sampleRequest?: string; - - /** - * Whether executing the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message. - */ - readonly isSticky?: boolean; - } - - /** - * A ChatCommandProvider returns {@link ChatCommands commands} that can be invoked on a chat participant using `/`. For example, `@participant /command`. - * These can be used as shortcuts to let the user explicitly invoke different functionalities provided by the participant. - */ - export interface ChatCommandProvider { - /** - * Returns a list of commands that its participant is capable of handling. A command - * can be selected by the user and will then be passed to the {@link ChatRequestHandler handler} - * via the {@link ChatRequest.command command} property. - * - * - * @param token A cancellation token. - * @returns A list of commands. The lack of a result can be signaled by returning `undefined`, `null`, or - * an empty array. - */ - provideCommands(context: ChatContext, token: CancellationToken): ProviderResult; - } - /** * A followup question suggested by the participant. */ @@ -239,11 +196,6 @@ declare module 'vscode' { */ readonly name: string; - /** - * A human-readable description explaining what this participant does. - */ - description?: string; - /** * Icon for the participant shown in UI. */ @@ -263,11 +215,6 @@ declare module 'vscode' { */ requestHandler: ChatRequestHandler; - /** - * This provider will be called to retrieve the participant's commands. - */ - commandProvider?: ChatCommandProvider; - /** * This provider will be called once after each request to retrieve suggested followup questions. */ diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index f9ed2a208bd..3319ce9ca8a 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -22,9 +22,18 @@ declare module 'vscode' { markdownContent: MarkdownString; } + /** + * Now only used for the "intent detection" API below + */ + export interface ChatCommand { + readonly name: string; + readonly description: string; + } + // TODO@API fit this into the stream export interface ChatDetectedParticipant { participant: string; + // TODO@API validate this against statically-declared slash commands? command?: ChatCommand; } @@ -231,20 +240,6 @@ declare module 'vscode' { kind?: string; } - export interface ChatCommand { - readonly isSticky2?: { - /** - * Indicates that the command should be automatically repopulated. - */ - isSticky: true; - - /** - * This can be set to a string to use a different placeholder message in the input box when the command has been repopulated. - */ - placeholder?: string; - }; - } - export interface ChatVariableResolverResponseStream { /** * Push a progress part to this stream. Short-hand for @@ -285,4 +280,12 @@ declare module 'vscode' { */ resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult; } + + export interface ChatParticipant { + /** + * A human-readable description explaining what this participant does. + * Only allow a static description for normal participants. Here where dynamic participants are allowed, the description must be able to be set as well. + */ + description?: string; + } } From dff6d9f391dd448ca92463e755ca34f46fcc5843 Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:09:03 -0600 Subject: [PATCH 131/228] Start adding UI to AI text provider API (#206388) the first steps to integrating the UI for the AI text results --- .../search/browser/searchActionsTopBar.ts | 54 +++++++- .../contrib/search/browser/searchIcons.ts | 3 + .../contrib/search/browser/searchModel.ts | 123 +++++++++++++----- .../contrib/search/browser/searchView.ts | 39 +++++- .../contrib/search/common/constants.ts | 3 + .../search/test/browser/searchActions.test.ts | 4 +- .../browser/searchNotebookHelpers.test.ts | 1 + .../search/test/browser/searchResult.test.ts | 5 +- .../search/test/browser/searchTestCommon.ts | 2 +- .../search/test/browser/searchViewlet.test.ts | 19 +-- .../services/search/common/searchService.ts | 3 + 11 files changed, 205 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts index e7dbb128b7b..dd533aee173 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -8,7 +8,7 @@ import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchSparkleEmpty, searchSparkleFilled, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; import { FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, Match, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; @@ -100,7 +100,7 @@ registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { menu: [{ id: MenuId.ViewTitle, group: 'navigation', - order: 3, + order: 4, when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.SearchContext.HasSearchResults.negate(), Constants.SearchContext.ViewHasSomeCollapsibleKey)), }] }); @@ -122,7 +122,7 @@ registerAction2(class ExpandAllAction extends Action2 { menu: [{ id: MenuId.ViewTitle, group: 'navigation', - order: 3, + order: 4, when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.SearchContext.HasSearchResults, Constants.SearchContext.ViewHasSomeCollapsibleKey.toNegated()), }] }); @@ -205,6 +205,54 @@ registerAction2(class ViewAsListAction extends Action2 { } }); +registerAction2(class ViewAIResultsAction extends Action2 { + constructor() { + super({ + id: Constants.SearchCommandIds.ShowAIResultsActionId, + title: nls.localize2('ViewAIResultsAction.label', "Show AI Results"), + category, + icon: searchSparkleEmpty, + precondition: Constants.SearchContext.AIResultsVisibleKey.toNegated(), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.false(), // disabled for now + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setAIResultsVisible(true); + } + } +}); + +registerAction2(class HideAIResultsAction extends Action2 { + constructor() { + super({ + id: Constants.SearchCommandIds.HideAIResultsActionId, + title: nls.localize2('HideAIResultsAction.label', "Hide AI Results"), + category, + icon: searchSparkleFilled, + precondition: Constants.SearchContext.AIResultsVisibleKey, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.false(), // disabled for now + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setAIResultsVisible(false); + } + } +}); + //#endregion //#region Helpers diff --git a/src/vs/workbench/contrib/search/browser/searchIcons.ts b/src/vs/workbench/contrib/search/browser/searchIcons.ts index f81dc87d394..066fbb8c836 100644 --- a/src/vs/workbench/contrib/search/browser/searchIcons.ts +++ b/src/vs/workbench/contrib/search/browser/searchIcons.ts @@ -29,3 +29,6 @@ export const searchViewIcon = registerIcon('search-view-icon', Codicon.search, l export const searchNewEditorIcon = registerIcon('search-new-editor', Codicon.newFile, localize('searchNewEditorIcon', 'Icon for the action to open a new search editor.')); export const searchOpenInFileIcon = registerIcon('search-open-in-file', Codicon.goToFile, localize('searchOpenInFile', 'Icon for the action to go to the file of the current search result.')); + +export const searchSparkleFilled = registerIcon('search-sparkle-filled', Codicon.sparkleFilled, localize('searchSparkleFilled', 'Icon to show AI results in search.')); +export const searchSparkleEmpty = registerIcon('search-sparkle-empty', Codicon.sparkle, localize('searchSparkleEmpty', 'Icon to hide AI results in search.')); diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 0f280b7f1d0..00f9a268b17 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -41,7 +41,7 @@ import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches, I import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; import { rawCellPrefix, INotebookCellMatchNoModel, isINotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; @@ -55,7 +55,7 @@ export class Match { // For replace private _fullPreviewRange: ISearchRange; - constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange) { + constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, public readonly aiContributed: boolean) { this._oneLinePreviewText = _fullPreviewLines[_fullPreviewRange.startLineNumber]; const adjustedEndCol = _fullPreviewRange.startLineNumber === _fullPreviewRange.endLineNumber ? _fullPreviewRange.endColumn : @@ -289,7 +289,7 @@ export class MatchInNotebook extends Match { private _webviewIndex: number | undefined; constructor(private readonly _cellParent: CellMatch, _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, webviewIndex?: number) { - super(_cellParent.parent, _fullPreviewLines, _fullPreviewRange, _documentRange); + super(_cellParent.parent, _fullPreviewLines, _fullPreviewRange, _documentRange, false); this._id = this._parent.id() + '>' + this._cellParent.cellIndex + (webviewIndex ? '_' + webviewIndex : '') + '_' + this.notebookMatchTypeString() + this._range + this.getMatchString(); this._webviewIndex = webviewIndex; } @@ -426,7 +426,6 @@ export class FileMatch extends Disposable implements IFileMatch { this._name = new Lazy(() => labelService.getUriBasenameLabel(this.resource)); this._cellMatches = new Map(); this._notebookUpdateScheduler = new RunOnceScheduler(this.updateMatchesForEditorWidget.bind(this), 250); - this.createMatches(); } addWebviewMatchesToCell(cellID: string, webviewMatches: ITextSearchMatch[]) { @@ -462,9 +461,14 @@ export class FileMatch extends Disposable implements IFileMatch { return this.matches().some(m => m instanceof MatchInNotebook && m.isReadonly()); } - createMatches(): void { + hasDownstreamNonAIResults(): boolean { + return this.matches().some(m => !m.aiContributed); + } + + createMatches(isAiContributed: boolean): void { const model = this.modelService.getModel(this._resource); - if (model) { + if (model && !isAiContributed) { + // todo: handle better when ai contributed results has model, currently, createMatches does not work for this this.bindModel(model); this.updateMatchesForModel(); } else { @@ -477,7 +481,7 @@ export class FileMatch extends Disposable implements IFileMatch { this.rawMatch.results .filter(resultIsMatch) .forEach(rawMatch => { - textSearchResultToMatches(rawMatch, this) + textSearchResultToMatches(rawMatch, this, isAiContributed) .forEach(m => this.add(m)); }); } @@ -529,7 +533,7 @@ export class FileMatch extends Disposable implements IFileMatch { const matches = this._model .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, true, this._model); + this.updateMatches(matches, true, this._model, false); } @@ -549,17 +553,17 @@ export class FileMatch extends Disposable implements IFileMatch { const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, modelChange, this._model); + this.updateMatches(matches, modelChange, this._model, false); // await this.updateMatchesForEditorWidget(); } - private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel): void { + private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel, isAiContributed: boolean): void { const textSearchResults = editorMatchesToTextSearchResults(matches, model, this._previewOptions); textSearchResults.forEach(textSearchResult => { - textSearchResultToMatches(textSearchResult, this).forEach(match => { + textSearchResultToMatches(textSearchResult, this, isAiContributed).forEach(match => { if (!this._removedTextMatches.has(match.id())) { this.add(match); if (this.isMatchSelected(match)) { @@ -1012,6 +1016,19 @@ export class FolderMatch extends Disposable { } } + hasDownstreamNonAIResults(): boolean { + let recursiveChildren: FileMatch[] = []; + const iterator = this.folderMatchesIterator(); + for (const elem of iterator) { + recursiveChildren = recursiveChildren.concat(elem.allDownstreamFileMatches()); + if (recursiveChildren.some(fileMatch => fileMatch.hasDownstreamNonAIResults())) { + return true; + } + } + + return false; + } + async bindNotebookEditorWidget(editor: NotebookEditorWidget, resource: URI) { const fileMatch = this._fileMatches.get(resource); @@ -1142,7 +1159,7 @@ export class FolderMatch extends Disposable { return this._query; } - addFileMatch(raw: IFileMatch[], silent: boolean, searchInstanceID: string): void { + addFileMatch(raw: IFileMatch[], silent: boolean, searchInstanceID: string, isAiContributed: boolean): void { // when adding a fileMatch that has intermediate directories const added: FileMatch[] = []; const updated: FileMatch[] = []; @@ -1156,7 +1173,7 @@ export class FolderMatch extends Disposable { .results .filter(resultIsMatch) .forEach(m => { - textSearchResultToMatches(m, existingFileMatch) + textSearchResultToMatches(m, existingFileMatch, isAiContributed) .forEach(m => existingFileMatch.add(m)); }); } @@ -1181,7 +1198,7 @@ export class FolderMatch extends Disposable { } } else { if (this instanceof FolderMatchWorkspaceRoot || this instanceof FolderMatchNoRoot) { - const fileMatch = this.createAndConfigureFileMatch(rawFileMatch, searchInstanceID); + const fileMatch = this.createAndConfigureFileMatch(rawFileMatch, searchInstanceID, isAiContributed); added.push(fileMatch); } } @@ -1367,7 +1384,7 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { return this.uriIdentityService.extUri.isEqual(uri1, ur2); } - private createFileMatch(query: IPatternInfo, previewOptions: ITextSearchPreviewOptions | undefined, maxResults: number | undefined, parent: FolderMatch, rawFileMatch: IFileMatch, closestRoot: FolderMatchWorkspaceRoot | null, searchInstanceID: string): FileMatch { + private createFileMatch(query: IPatternInfo, previewOptions: ITextSearchPreviewOptions | undefined, maxResults: number | undefined, parent: FolderMatch, rawFileMatch: IFileMatch, closestRoot: FolderMatchWorkspaceRoot | null, searchInstanceID: string, isAiContributed: boolean): FileMatch { const fileMatch = this.instantiationService.createInstance( FileMatch, @@ -1379,13 +1396,14 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { closestRoot, searchInstanceID ); + fileMatch.createMatches(isAiContributed); parent.doAddFile(fileMatch); const disposable = fileMatch.onChange(({ didRemove }) => parent.onFileChange(fileMatch, didRemove)); this._register(fileMatch.onDispose(() => disposable.dispose())); return fileMatch; } - createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string): FileMatch { + createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string, ai: boolean): FileMatch { if (!this.uriHasParent(this.resource, rawFileMatch.resource)) { throw Error(`${rawFileMatch.resource} is not a descendant of ${this.resource}`); @@ -1413,7 +1431,7 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { parent = folderMatch; } - return this.createFileMatch(this._query.contentPattern, this._query.previewOptions, this._query.maxResults, parent, rawFileMatch, root, searchInstanceID); + return this.createFileMatch(this._query.contentPattern, this._query.previewOptions, this._query.maxResults, parent, rawFileMatch, root, searchInstanceID, ai); } } @@ -1432,7 +1450,7 @@ export class FolderMatchNoRoot extends FolderMatch { super(null, _id, _index, _query, _parent, _parent, null, replaceService, instantiationService, labelService, uriIdentityService); } - createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string): FileMatch { + createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string, isAiContributed: boolean): FileMatch { const fileMatch = this._register(this.instantiationService.createInstance( FileMatch, this._query.contentPattern, @@ -1441,6 +1459,7 @@ export class FolderMatchNoRoot extends FolderMatch { this, rawFileMatch, null, searchInstanceID)); + fileMatch.createMatches(isAiContributed); this.doAddFile(fileMatch); const disposable = fileMatch.onChange(({ didRemove }) => this.onFileChange(fileMatch, didRemove)); this._register(fileMatch.onDispose(() => disposable.dispose())); @@ -1750,7 +1769,7 @@ export class SearchResult extends Disposable { } - add(allRaw: IFileMatch[], searchInstanceID: string, silent: boolean = false): void { + add(allRaw: IFileMatch[], searchInstanceID: string, ai: boolean, silent: boolean = false): void { // Split up raw into a list per folder so we can do a batch add per folder. const { byFolder, other } = this.groupFilesByFolder(allRaw); @@ -1760,10 +1779,10 @@ export class SearchResult extends Disposable { } const folderMatch = this.getFolderMatch(raw[0].resource); - folderMatch?.addFileMatch(raw, silent, searchInstanceID); + folderMatch?.addFileMatch(raw, silent, searchInstanceID, ai); }); - this._otherFilesMatch?.addFileMatch(other, silent, searchInstanceID); + this._otherFilesMatch?.addFileMatch(other, silent, searchInstanceID, ai); this.disposePastResults(); } @@ -1951,6 +1970,7 @@ export class SearchModel extends Disposable { private _preserveCase: boolean = false; private _startStreamDelay: Promise = Promise.resolve(); private readonly _resultQueue: IFileMatch[] = []; + private _aiResultsEnabled = false; private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; @@ -2013,6 +2033,37 @@ export class SearchModel extends Disposable { return this._searchResult; } + async disableAIResults() { + this._aiResultsEnabled = false; + } + async addAIResults(onProgress?: (result: ISearchProgressItem) => void) { + if (this._aiResultsEnabled) { + return; + } else { + if (this._searchQuery) { + const start = Date.now(); + const searchInstanceID = Date.now().toString(); + const asyncAIResults = this.searchService.aiTextSearch( + { ...this._searchQuery, contentPattern: this._searchQuery.contentPattern.pattern, type: QueryType.aiText }, + this.currentCancelTokenSource?.token, async (p: ISearchProgressItem) => { + this.onSearchProgress(p, searchInstanceID, false); + onProgress?.(p); + }); + + + await asyncAIResults.then( + value => { + this.onSearchCompleted(value, Date.now() - start, searchInstanceID, true); + return value; + }, + e => { + this.onSearchError(e, Date.now() - start); + throw e; + }); + } + this._aiResultsEnabled = true; + } + } private doSearch(query: ITextQuery, progressEmitter: Emitter, searchQuery: ITextQuery, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void, callerToken?: CancellationToken): { asyncResults: Promise; @@ -2020,7 +2071,7 @@ export class SearchModel extends Disposable { } { const asyncGenerateOnProgress = async (p: ISearchProgressItem) => { progressEmitter.fire(); - this.onSearchProgress(p, searchInstanceID, false); + this.onSearchProgress(p, searchInstanceID, false, false); onProgress?.(p); }; @@ -2039,6 +2090,15 @@ export class SearchModel extends Disposable { notebookResult.allScannedFiles, ); + const asyncAIResults = this._aiResultsEnabled ? this.searchService.aiTextSearch( + { ...searchQuery, contentPattern: searchQuery.contentPattern.pattern, type: QueryType.aiText }, + this.currentCancelTokenSource.token, async (p: ISearchProgressItem) => { + progressEmitter.fire(); + this.onSearchProgress(p, searchInstanceID, false, true); + onProgress?.(p); + }) : Promise.resolve(undefined); + + const syncResults = textResult.syncResults.results; syncResults.forEach(p => { if (p) { syncGenerateOnProgress(p); } }); @@ -2048,10 +2108,11 @@ export class SearchModel extends Disposable { // resolve async parts of search const allClosedEditorResults = await textResult.asyncResults; const resolvedNotebookResults = await notebookResult.completeData; + const aiResults = await asyncAIResults; tokenSource.dispose(); const searchLength = Date.now() - searchStart; const resolvedResult = { - results: [...allClosedEditorResults.results, ...resolvedNotebookResults.results], + results: [...allClosedEditorResults.results, ...resolvedNotebookResults.results, ...aiResults?.results ?? []], messages: [...allClosedEditorResults.messages, ...resolvedNotebookResults.messages], limitHit: allClosedEditorResults.limitHit || resolvedNotebookResults.limitHit, exit: allClosedEditorResults.exit, @@ -2141,12 +2202,12 @@ export class SearchModel extends Disposable { } } - private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string): ISearchComplete | undefined { + private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string, ai = false): ISearchComplete | undefined { if (!this._searchQuery) { throw new Error('onSearchCompleted must be called after a search is started'); } - this._searchResult.add(this._resultQueue, searchInstanceID); + this._searchResult.add(this._resultQueue, searchInstanceID, ai); this._resultQueue.length = 0; const options: IPatternInfo = Object.assign({}, this._searchQuery.contentPattern); @@ -2195,18 +2256,18 @@ export class SearchModel extends Disposable { } } - private onSearchProgress(p: ISearchProgressItem, searchInstanceID: string, sync = true) { + private onSearchProgress(p: ISearchProgressItem, searchInstanceID: string, sync = true, ai = false) { if ((p).resource) { this._resultQueue.push(p); if (sync) { if (this._resultQueue.length) { - this._searchResult.add(this._resultQueue, searchInstanceID, true); + this._searchResult.add(this._resultQueue, searchInstanceID, ai, true); this._resultQueue.length = 0; } } else { this._startStreamDelay.then(() => { if (this._resultQueue.length) { - this._searchResult.add(this._resultQueue, searchInstanceID, true); + this._searchResult.add(this._resultQueue, searchInstanceID, ai, true); this._resultQueue.length = 0; } }); @@ -2354,16 +2415,16 @@ export class RangeHighlightDecorations implements IDisposable { -function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch): Match[] { +function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch, isAiContributed: boolean): Match[] { const previewLines = rawMatch.preview.text.split('\n'); if (Array.isArray(rawMatch.ranges)) { return rawMatch.ranges.map((r, i) => { const previewRange: ISearchRange = (rawMatch.preview.matches)[i]; - return new Match(fileMatch, previewLines, previewRange, r); + return new Match(fileMatch, previewLines, previewRange, r, isAiContributed); }); } else { const previewRange = rawMatch.preview.matches; - const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges); + const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges, isAiContributed); return [match]; } } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 6ea0cb4542e..17c961be050 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -158,6 +158,7 @@ export class SearchView extends ViewPane { private treeAccessibilityProvider: SearchAccessibilityProvider; private treeViewKey: IContextKey; + private aiResultsVisibleKey: IContextKey; private _visibleMatches: number = 0; @@ -218,6 +219,7 @@ export class SearchView extends ViewPane { this.hasFilePatternKey = Constants.SearchContext.ViewHasFilePatternKey.bindTo(this.contextKeyService); this.hasSomeCollapsibleResultKey = Constants.SearchContext.ViewHasSomeCollapsibleKey.bindTo(this.contextKeyService); this.treeViewKey = Constants.SearchContext.InTreeViewKey.bindTo(this.contextKeyService); + this.aiResultsVisibleKey = Constants.SearchContext.AIResultsVisibleKey.bindTo(this.contextKeyService); // scoped this.contextKeyService = this._register(this.contextKeyService.createScoped(this.container)); @@ -294,6 +296,14 @@ export class SearchView extends ViewPane { this.treeViewKey.set(visible); } + get aiResultsVisible(): boolean { + return this.aiResultsVisibleKey.get() ?? false; + } + + private set aiResultsVisible(visible: boolean) { + this.aiResultsVisibleKey.set(visible); + } + setTreeView(visible: boolean): void { if (visible === this.isTreeLayoutViewVisible) { return; @@ -303,6 +313,19 @@ export class SearchView extends ViewPane { this.refreshTree(); } + async setAIResultsVisible(visible: boolean): Promise { + if (visible === this.aiResultsVisible) { + return; + } + this.aiResultsVisible = visible; + if (visible) { + this.model.addAIResults(); + } else { + this.model.disableAIResults(); + } + this.refreshTree(); + } + private get state(): SearchUIState { return this.searchStateKey.get() ?? SearchUIState.Idle; } @@ -693,7 +716,7 @@ export class SearchView extends ViewPane { private createResultIterator(collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable> { const folderMatches = this.searchResult.folderMatches() - .filter(fm => !fm.isEmpty()) + .filter(fm => !fm.isEmpty() && (this.aiResultsVisible || fm.hasDownstreamNonAIResults())) .sort(searchMatchComparer); if (folderMatches.length === 1) { @@ -712,7 +735,11 @@ export class SearchView extends ViewPane { const matchArray = this.isTreeLayoutViewVisible ? folderMatch.matches() : folderMatch.allDownstreamFileMatches(); const matches = matchArray.sort((a, b) => searchMatchComparer(a, b, sortOrder)); - return Iterable.map(matches, match => { + return Iterable.filter(Iterable.map(matches, match => { + + if (!this.aiResultsVisible && !match.hasDownstreamNonAIResults()) { + return undefined; + } let children; if (match instanceof FileMatch) { children = this.createFileIterator(match); @@ -723,11 +750,15 @@ export class SearchView extends ViewPane { const collapsed = (collapseResults === 'alwaysCollapse' || (match.count() > 10 && collapseResults !== 'alwaysExpand')) ? ObjectTreeElementCollapseState.PreserveOrCollapsed : ObjectTreeElementCollapseState.PreserveOrExpanded; return >{ element: match, children, collapsed, incompressible: (match instanceof FileMatch) ? true : childFolderIncompressible }; - }); + }), (item): item is ICompressedTreeElement => !!item); } private createFileIterator(fileMatch: FileMatch): Iterable> { - const matches = fileMatch.matches().sort(searchMatchComparer); + let matches = fileMatch.matches().sort(searchMatchComparer); + + if (!this.aiResultsVisible) { + matches = matches.filter(e => !e.aiContributed); + } return Iterable.map(matches, r => (>{ element: r, incompressible: true })); } diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index a8f1bc84311..1b5d8a06a93 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -41,6 +41,8 @@ export const enum SearchCommandIds { ClearSearchResultsActionId = 'search.action.clearSearchResults', ViewAsTreeActionId = 'search.action.viewAsTree', ViewAsListActionId = 'search.action.viewAsList', + ShowAIResultsActionId = 'search.action.showAIResults', + HideAIResultsActionId = 'search.action.hideAIResults', ToggleQueryDetailsActionId = 'workbench.action.search.toggleQueryDetails', ExcludeFolderFromSearchId = 'search.action.excludeFromSearch', FocusNextInputActionId = 'search.focus.nextInputBox', @@ -74,4 +76,5 @@ export const SearchContext = { ViewHasFilePatternKey: new RawContextKey('viewHasFilePattern', false), ViewHasSomeCollapsibleKey: new RawContextKey('viewHasSomeCollapsibleResult', false), InTreeViewKey: new RawContextKey('inTreeView', false), + AIResultsVisibleKey: new RawContextKey('AIResultsVisibleKey', false), }; diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 5e39773a0cc..9b3b9e4f4dd 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -125,6 +125,7 @@ suite('Search Actions', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } @@ -145,7 +146,8 @@ suite('Search Actions', () => { startColumn: 0, endLineNumber: line, endColumn: 2 - } + }, + false ); fileMatch.add(match); return match; diff --git a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index 0a591e21457..a90875c1384 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -217,6 +217,7 @@ suite('searchNotebookHelpers', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(folderMatch); store.add(fileMatch); diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index c6d94a0ebf3..e71fab4b131 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -66,7 +66,7 @@ suite('SearchResult', () => { test('Line Match', function () { const fileMatch = aFileMatch('folder/file.txt', null!); - const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5)); + const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5), false); assert.strictEqual(lineMatch.text(), '0 foo bar'); assert.strictEqual(lineMatch.range().startLineNumber, 2); assert.strictEqual(lineMatch.range().endLineNumber, 2); @@ -174,7 +174,7 @@ suite('SearchResult', () => { const searchResult = instantiationService.createInstance(SearchResult, searchModel); store.add(searchResult); const fileMatch = aFileMatch('far/boo', searchResult); - const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3)); + const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3), false); assert(lineMatch.parent() === fileMatch); assert(fileMatch.parent() === searchResult.folderMatches()[0]); @@ -532,6 +532,7 @@ suite('SearchResult', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, root, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; diff --git a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts index 5c5fcd10aab..b6e7dc04bbb 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts @@ -66,5 +66,5 @@ export function stubNotebookEditorService(instantiationService: TestInstantiatio } export function addToSearchResult(searchResult: SearchResult, allRaw: IFileMatch[], searchInstanceID = '') { - searchResult.add(allRaw, searchInstanceID); + searchResult.add(allRaw, searchInstanceID, false); } diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 76b49f5f2bf..4feb3d343ed 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -76,7 +76,7 @@ suite('Search - Viewlet', () => { endColumn: 1 } }] - }], ''); + }], '', false); const fileMatch = result.matches()[0]; const lineMatch = fileMatch.matches()[0]; @@ -89,9 +89,9 @@ suite('Search - Viewlet', () => { const fileMatch1 = aFileMatch('/foo'); const fileMatch2 = aFileMatch('/with/path'); const fileMatch3 = aFileMatch('/with/path/foo'); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); - const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); + const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); assert(searchMatchComparer(fileMatch1, fileMatch2) < 0); assert(searchMatchComparer(fileMatch2, fileMatch1) > 0); @@ -127,13 +127,13 @@ suite('Search - Viewlet', () => { const fileMatch2 = aFileMatch('/with/path.c', folderMatch2); const fileMatch3 = aFileMatch('/with/path/bar.b', folderMatch2); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); /*** * Structure would take the following form: @@ -180,6 +180,7 @@ suite('Search - Viewlet', () => { const fileMatch = instantiation.createInstance(FileMatch, { pattern: '' }, undefined, undefined, parentFolder ?? aFolderMatch('', 0), rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 42ffb1111ed..bb45e53d87b 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -275,6 +275,9 @@ export class SearchService extends Disposable implements ISearchService { return this.getSearchProvider(query.type).has(scheme); }); + if (query.type === QueryType.aiText && !someSchemeHasProvider) { + return []; + } await Promise.all([...fqs.keys()].map(async scheme => { const schemeFQs = fqs.get(scheme)!; let provider = this.getSearchProvider(query.type).get(scheme); From f348847d77b94e2166f94189e30289b517bc6de3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 08:46:51 -0800 Subject: [PATCH 132/228] Update xterm Fixes #204690 --- package.json | 16 +++++----- remote/package.json | 16 +++++----- remote/web/package.json | 14 ++++----- remote/web/yarn.lock | 60 ++++++++++++++++++------------------ remote/yarn.lock | 68 ++++++++++++++++++++--------------------- yarn.lock | 68 ++++++++++++++++++++--------------------- 6 files changed, 121 insertions(+), 121 deletions(-) diff --git a/package.json b/package.json index f9142db514f..03245f8347a 100644 --- a/package.json +++ b/package.json @@ -80,14 +80,14 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.6.0-beta.33", + "@xterm/addon-image": "0.7.0-beta.31", + "@xterm/addon-search": "0.14.0-beta.33", + "@xterm/addon-serialize": "0.12.0-beta.33", + "@xterm/addon-unicode11": "0.7.0-beta.33", + "@xterm/addon-webgl": "0.17.0-beta.33", + "@xterm/headless": "5.4.0-beta.33", + "@xterm/xterm": "5.4.0-beta.33", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/package.json b/remote/package.json index a3af2b99fe4..1070eac356f 100644 --- a/remote/package.json +++ b/remote/package.json @@ -13,14 +13,14 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.6.0-beta.33", + "@xterm/addon-image": "0.7.0-beta.31", + "@xterm/addon-search": "0.14.0-beta.33", + "@xterm/addon-serialize": "0.12.0-beta.33", + "@xterm/addon-unicode11": "0.7.0-beta.33", + "@xterm/addon-webgl": "0.17.0-beta.33", + "@xterm/headless": "5.4.0-beta.33", + "@xterm/xterm": "5.4.0-beta.33", "cookie": "^0.4.0", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", diff --git a/remote/web/package.json b/remote/web/package.json index e891be054dd..16b4ef21ba4 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,13 +7,13 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.6.0-beta.33", + "@xterm/addon-image": "0.7.0-beta.31", + "@xterm/addon-search": "0.14.0-beta.33", + "@xterm/addon-serialize": "0.12.0-beta.33", + "@xterm/addon-unicode11": "0.7.0-beta.33", + "@xterm/addon-webgl": "0.17.0-beta.33", + "@xterm/xterm": "5.4.0-beta.33", "jschardet": "3.0.0", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 1af0f2be779..7ce1173e61e 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -48,40 +48,40 @@ resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== +"@xterm/addon-canvas@0.6.0-beta.33": + version "0.6.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" + integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": +"@xterm/addon-image@0.7.0-beta.31": version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" + integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== +"@xterm/addon-search@0.14.0-beta.33": + version "0.14.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" + integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-serialize@0.12.0-beta.33": + version "0.12.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" + integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== + +"@xterm/addon-unicode11@0.7.0-beta.33": + version "0.7.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" + integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== + +"@xterm/addon-webgl@0.17.0-beta.33": + version "0.17.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" + integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== + +"@xterm/xterm@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" + integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== jschardet@3.0.0: version "3.0.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index bcd7160cadc..63fb04013f6 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -114,45 +114,45 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== +"@xterm/addon-canvas@0.6.0-beta.33": + version "0.6.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" + integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": +"@xterm/addon-image@0.7.0-beta.31": version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" + integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== +"@xterm/addon-search@0.14.0-beta.33": + version "0.14.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" + integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== -"@xterm/headless@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.31.tgz#7727c5c79d3b1b8e59526cf51c75148e13f61694" - integrity sha512-AIMP0ZZozxtvilVTKqquNPYDE5RuKINTsYjOcWzYvjpg7sS75/Tn/RBx20KfZN8Z2oCCwVgj+1mudrV0W4JmMw== +"@xterm/addon-serialize@0.12.0-beta.33": + version "0.12.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" + integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-unicode11@0.7.0-beta.33": + version "0.7.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" + integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== + +"@xterm/addon-webgl@0.17.0-beta.33": + version "0.17.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" + integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== + +"@xterm/headless@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.33.tgz#8accf68a0e58ba03c8a4627aa108df2d690d8713" + integrity sha512-DlGM2qPdaDmeznUSk9doOQfoi4x8uTTfBysSb+Llxm/SyfxBqnNZEV6N1j494RoBJlawZh2UugmV1itQD+Wlzg== + +"@xterm/xterm@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" + integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" diff --git a/yarn.lock b/yarn.lock index f434a0d1e6a..1aa5e794d31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1700,45 +1700,45 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8" integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== +"@xterm/addon-canvas@0.6.0-beta.33": + version "0.6.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" + integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": +"@xterm/addon-image@0.7.0-beta.31": version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" + integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== +"@xterm/addon-search@0.14.0-beta.33": + version "0.14.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" + integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== -"@xterm/headless@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.31.tgz#7727c5c79d3b1b8e59526cf51c75148e13f61694" - integrity sha512-AIMP0ZZozxtvilVTKqquNPYDE5RuKINTsYjOcWzYvjpg7sS75/Tn/RBx20KfZN8Z2oCCwVgj+1mudrV0W4JmMw== +"@xterm/addon-serialize@0.12.0-beta.33": + version "0.12.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" + integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== +"@xterm/addon-unicode11@0.7.0-beta.33": + version "0.7.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" + integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== + +"@xterm/addon-webgl@0.17.0-beta.33": + version "0.17.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" + integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== + +"@xterm/headless@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.33.tgz#8accf68a0e58ba03c8a4627aa108df2d690d8713" + integrity sha512-DlGM2qPdaDmeznUSk9doOQfoi4x8uTTfBysSb+Llxm/SyfxBqnNZEV6N1j494RoBJlawZh2UugmV1itQD+Wlzg== + +"@xterm/xterm@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" + integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== "@xtuc/ieee754@^1.2.0": version "1.2.0" From 32c96cea668ac0cb8bccf6eccf1401ace17cd277 Mon Sep 17 00:00:00 2001 From: mkasenberg Date: Thu, 29 Feb 2024 19:01:09 +0100 Subject: [PATCH 133/228] searchEditor: Add option to peek with a single click (#204413) searchEditor: Add option to peek with single click The option "search.searchEditor.singleClickBehaviour": "peekDefinition" will allow to open a Peek Window in Search Editor with a single click. Co-authored-by: @mkasenberg --- .../contrib/search/browser/search.contribution.ts | 10 ++++++++++ .../contrib/searchEditor/browser/searchEditor.ts | 12 +++++++++++- src/vs/workbench/services/search/common/search.ts | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 0531e3a3776..5d2e48914c0 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -308,6 +308,16 @@ configurationRegistry.registerConfiguration({ ], markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double-clicking a result in a search editor.") }, + 'search.searchEditor.singleClickBehaviour': { + type: 'string', + enum: ['default', 'peekDefinition',], + default: 'default', + enumDescriptions: [ + nls.localize('search.searchEditor.singleClickBehaviour.default', "Single-clicking does nothing."), + nls.localize('search.searchEditor.singleClickBehaviour.peekDefinition', "Single-clicking opens a Peek Definition window."), + ], + markdownDescription: nls.localize('search.searchEditor.singleClickBehaviour', "Configure effect of single-clicking a result in a search editor.") + }, 'search.searchEditor.reusePriorSearchConfiguration': { type: 'boolean', default: false, diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 449b0d60787..ffc69643037 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -248,7 +248,17 @@ export class SearchEditor extends AbstractTextCodeEditor private registerEditorListeners() { this.searchResultEditor.onMouseUp(e => { - if (e.event.detail === 2) { + if (e.event.detail === 1) { + const behaviour = this.searchConfig.searchEditor.singleClickBehaviour; + const position = e.target.position; + if (position && behaviour === 'peekDefinition') { + const line = this.searchResultEditor.getModel()?.getLineContent(position.lineNumber) ?? ''; + if (line.match(FILE_LINE_REGEX) || line.match(RESULT_LINE_REGEX)) { + this.searchResultEditor.setSelection(Range.fromPositions(position)); + this.commandService.executeCommand('editor.action.peekDefinition'); + } + } + } else if (e.event.detail === 2) { const behaviour = this.searchConfig.searchEditor.doubleClickBehaviour; const position = e.target.position; if (position && behaviour !== 'selectWord') { diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 0ebb5a9f69d..c621e4dc2c9 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -426,6 +426,7 @@ export interface ISearchConfigurationProperties { mode: 'view' | 'reuseEditor' | 'newEditor'; searchEditor: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide'; + singleClickBehaviour: 'default' | 'peekDefinition'; reusePriorSearchConfiguration: boolean; defaultNumberOfContextLines: number | null; experimental: {}; From ec2ef287fc2de3d1f307b4aec435d4f21ce79a88 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:02:30 -0800 Subject: [PATCH 134/228] Align check for env var collection on remote Checked all causes before applyToProcessEnvironment was used. Fixes #197187 --- src/vs/platform/terminal/common/terminalEnvironment.ts | 9 +++++++++ src/vs/server/node/remoteTerminalChannel.ts | 3 ++- .../contrib/terminal/browser/terminalProcessManager.ts | 3 ++- .../terminal/electron-sandbox/localTerminalBackend.ts | 3 ++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/terminal/common/terminalEnvironment.ts b/src/vs/platform/terminal/common/terminalEnvironment.ts index 38e8fa2c669..d2925819862 100644 --- a/src/vs/platform/terminal/common/terminalEnvironment.ts +++ b/src/vs/platform/terminal/common/terminalEnvironment.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { OperatingSystem, OS } from 'vs/base/common/platform'; +import type { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; /** * Aggressively escape non-windows paths to prepare for being sent to a shell. This will do some @@ -59,3 +60,11 @@ export function sanitizeCwd(cwd: string): string { } return cwd; } + +/** + * Determines whether the given shell launch config should use the environment variable collection. + * @param slc The shell launch config to check. + */ +export function shouldUseEnvironmentVariableCollection(slc: IShellLaunchConfig): boolean { + return !slc.strictEnv && !slc.hideFromUser; +} diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index caec44f7f3a..657d3e8238a 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -32,6 +32,7 @@ import { IExtensionManagementService } from 'vs/platform/extensionManagement/com import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { promiseWithResolvers } from 'vs/base/common/async'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; class CustomVariableResolver extends AbstractVariableResolverService { constructor( @@ -235,7 +236,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< ); // Apply extension environment variable collections to the environment - if (!shellLaunchConfig.strictEnv) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const entries: [string, IEnvironmentVariableCollection][] = []; for (const [k, v, d] of args.envVariableCollections) { entries.push([k, { map: deserializeEnvironmentVariableCollection(v), descriptionMap: deserializeEnvironmentDescriptionMap(d) }]); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 81770243f91..b731bf456fd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -40,6 +40,7 @@ import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } import { generateUuid } from 'vs/base/common/uuid'; import { getActiveWindow, runWhenWindowIdle } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; const enum ProcessConstants { /** @@ -436,7 +437,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); } const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); - if (!this._isDisposed && !shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (!this._isDisposed && shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index 06779c99e21..cb35e8698c2 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -37,6 +37,7 @@ import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statu import { memoize } from 'vs/base/common/decorators'; import { StopWatch } from 'vs/base/common/stopwatch'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; export class LocalTerminalBackendContribution implements IWorkbenchContribution { @@ -373,7 +374,7 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke const envFromConfigValue = this._configurationService.getValue(`terminal.integrated.env.${platformKey}`); const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment()); const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); - if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const workspaceFolder = terminalEnvironment.getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService); await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver); } From 7d49edbe555d9756ac01d5e55e302399f4df94ba Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:14:07 -0800 Subject: [PATCH 135/228] Make sticky scroll do something on click for partial cmd Fixes #206544 --- src/vs/workbench/contrib/terminal/browser/terminal.ts | 3 ++- .../stickyScroll/browser/terminalStickyScrollOverlay.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index db2791f9bda..229e1b0b124 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -28,6 +28,7 @@ import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/mark import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP_TYPE, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import type { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalEditorService = createDecorator('terminalEditorService'); @@ -117,7 +118,7 @@ export interface IMarkTracker { scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void; scrollToLine(line: number, position: ScrollPosition): void; - revealCommand(command: ITerminalCommand, position?: ScrollPosition): void; + revealCommand(command: ITerminalCommand | ICurrentPartialCommand, position?: ScrollPosition): void; revealRange(range: IBufferRange): void; registerTemporaryDecoration(marker: IMarker, endMarker: IMarker | undefined, showOutline: boolean): void; showCommandGuide(command: ITerminalCommand | undefined): void; diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 04f14751372..8b8ded275ba 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -389,7 +389,7 @@ export class TerminalStickyScrollOverlay extends Disposable { // Scroll to the command on click this._register(addStandardDisposableListener(hoverOverlay, 'click', () => { - if (this._xterm && this._currentStickyCommand && 'getOutput' in this._currentStickyCommand) { + if (this._xterm && this._currentStickyCommand) { this._xterm.markTracker.revealCommand(this._currentStickyCommand); this._instance.focus(); } From b95d96c7c3ca0bd5ccd203720030cb629a48b0e0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:28:19 -0800 Subject: [PATCH 136/228] Refresh sticky scroll content when dimensions differ Fixes #205578 --- .../browser/terminalStickyScrollOverlay.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 04f14751372..8848b6715ff 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -112,6 +112,10 @@ export class TerminalStickyScrollOverlay extends Disposable { this._register(this._themeService.onDidColorThemeChange(() => { this._syncOptions(); })); + this._register(this._xterm.raw.onResize(() => { + this._syncOptions(); + this._throttledRefresh(); + })); this._getSerializeAddonConstructor().then(SerializeAddon => { this._serializeAddon = this._register(new SerializeAddon()); @@ -150,7 +154,7 @@ export class TerminalStickyScrollOverlay extends Disposable { this._xterm.raw.onLineFeed, // Rarely an update may be required after just a cursor move, like when // scrolling horizontally in a pager - this._xterm.raw.onCursorMove + this._xterm.raw.onCursorMove, )(() => this._refresh()), addStandardDisposableListener(this._xterm.raw.element!.querySelector('.xterm-viewport')!, 'scroll', () => this._refresh()), ); @@ -304,7 +308,11 @@ export class TerminalStickyScrollOverlay extends Disposable { } // Write content if it differs - if (content && this._currentContent !== content) { + if ( + content && this._currentContent !== content || + this._stickyScrollOverlay.cols !== xterm.cols || + this._stickyScrollOverlay.rows !== stickyScrollLineCount + ) { this._stickyScrollOverlay.resize(this._stickyScrollOverlay.cols, stickyScrollLineCount); // Clear attrs, reset cursor position, clear right this._stickyScrollOverlay.write('\x1b[0m\x1b[H\x1b[2J'); From be6ae260ba87ea707de66780ab01de01f74ffcf9 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 29 Feb 2024 10:48:11 -0800 Subject: [PATCH 137/228] Update SECURITY.md to latest version (#206550) --- SECURITY.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 4fa5946a867..82db58aa7c8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,34 +1,34 @@ - + ## Security -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: -* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) -* Full paths of source file(s) related to the manifestation of the issue -* The location of the affected source code (tag/branch/commit or direct URL) -* Any special configuration required to reproduce the issue -* Step-by-step instructions to reproduce the issue -* Proof-of-concept or exploit code (if possible) -* Impact of the issue, including how an attacker might exploit the issue + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. ## Preferred Languages @@ -36,6 +36,6 @@ We prefer all communications to be in English. ## Policy -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). From 29aad12b6e9e2737cf6ce008bac943defd88730c Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Thu, 29 Feb 2024 11:59:08 -0800 Subject: [PATCH 138/228] Fix stuck highlights when occurrenceHighlight set 'off' (#206557) fix for #206486 --- .../contrib/wordHighlighter/browser/wordHighlighter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index 8b9790bce21..cae43742902 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -310,6 +310,11 @@ class WordHighlighter { this._onPositionChanged(e); })); this.toUnhook.add(editor.onDidFocusEditorText((e) => { + if (this.occurrencesHighlight === 'off') { + // Early exit if nothing needs to be done + return; + } + if (!this.workerRequest) { this._run(); } From fb6e06a8cf7e6536989321f90aecd08b066b5997 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 12:13:22 -0800 Subject: [PATCH 139/228] fix #203576 --- .../widget/diffEditor/diffEditorWidget.ts | 13 +++++++++++++ .../codeEditor/browser/diffEditorHelper.ts | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 7499fa8e0bc..d7eba7472f9 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -280,6 +280,19 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } })); + this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, (e) => { + if (e?.reason === CursorChangeReason.Explicit) { + const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.original.contains(e.position.lineNumber)); + if (diff?.lineRangeMapping.modified.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff?.lineRangeMapping.original.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); + } + } + })); + const isInitializingDiff = this._diffModel.map(this, (m, reader) => { /** @isInitializingDiff isDiffUpToDate */ if (!m) { return undefined; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 902b335ba4f..6aeddc30e35 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -91,9 +91,6 @@ function createScreenReaderHelp(): IDisposable { const keybindingService = accessor.get(IKeybindingService); const contextKeyService = accessor.get(IContextKeyService); - const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); - if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { return; } @@ -103,11 +100,22 @@ function createScreenReaderHelp(): IDisposable { 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 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('msg3', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), + switchSides, + 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) { From 5d3b37490ee4bb49bd184910031ee2f1cf809228 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 12:20:06 -0800 Subject: [PATCH 140/228] clean up --- .../widget/diffEditor/diffEditorWidget.ts | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index d7eba7472f9..417d75045a3 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -30,7 +30,7 @@ import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; +import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; @@ -267,31 +267,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { ), })); - this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, (e) => { - if (e?.reason === CursorChangeReason.Explicit) { - const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber)); - if (diff?.lineRangeMapping.modified.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff?.lineRangeMapping.original.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); - } - } - })); + this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, true))); + this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, false))); - this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, (e) => { - if (e?.reason === CursorChangeReason.Explicit) { - const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.original.contains(e.position.lineNumber)); - if (diff?.lineRangeMapping.modified.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff?.lineRangeMapping.original.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); - } - } - })); const isInitializingDiff = this._diffModel.map(this, (m, reader) => { /** @isInitializingDiff isDiffUpToDate */ @@ -621,6 +599,19 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } }); } + + private _handleCursorPositionChange(e: ICursorPositionChangedEvent | undefined, isModifiedEditor: boolean): void { + if (e?.reason === CursorChangeReason.Explicit) { + const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => isModifiedEditor ? m.lineRangeMapping.modified.contains(e.position.lineNumber) : m.lineRangeMapping.original.contains(e.position.lineNumber)); + if (diff?.lineRangeMapping.modified.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff?.lineRangeMapping.original.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); + } + } + } } function toLineChanges(state: DiffState): ILineChange[] { From 3f74fb2e33b6a7e622a505117e1b773b722138c0 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 12:39:00 -0800 Subject: [PATCH 141/228] enable by default --- .../accessibility/browser/accessibilityConfiguration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 88493cde471..7ff7922c187 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -550,7 +550,8 @@ const configuration: IConfigurationNode = { 'properties': { 'sound': { 'description': localize('accessibility.signals.voiceRecordingStarted.sound', "Plays a sound when the voice recording has started."), - ...soundFeatureBase + ...soundFeatureBase, + 'default': 'on' }, } }, From 7ed38cae11b5d9bdf91524e8e527fd4aa1176e04 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 12:56:19 -0800 Subject: [PATCH 142/228] replace more instances of audio cues --- .../browser/accessibility.contribution.ts | 4 ++-- ...{saveAudioCue.ts => saveAccessibilitySignal.ts} | 8 +++----- .../accessibilitySignals/browser/commands.ts | 2 +- .../chat/browser/chatAccessibilityService.ts | 14 +++++++------- 4 files changed, 13 insertions(+), 15 deletions(-) rename src/vs/workbench/contrib/accessibility/browser/{saveAudioCue.ts => saveAccessibilitySignal.ts} (73%) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index fb097ebecaa..faa741438ed 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -13,7 +13,7 @@ import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibi import { HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus'; import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; -import { SaveAudioCueContribution } from 'vs/workbench/contrib/accessibility/browser/saveAudioCue'; +import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal'; import { CommentsAccessibilityHelpContribution } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; registerAccessibilityConfiguration(); @@ -29,5 +29,5 @@ workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContri workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually); registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(SaveAudioCueContribution.ID, SaveAudioCueContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(SaveAccessibilitySignalContribution.ID, SaveAccessibilitySignalContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(DynamicSpeechAccessibilityConfiguration.ID, DynamicSpeechAccessibilityConfiguration, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts b/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts similarity index 73% rename from src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts rename to src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts index 87abfd5cb8d..e4df2fcc74e 100644 --- a/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts +++ b/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts @@ -9,17 +9,15 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export class SaveAudioCueContribution extends Disposable implements IWorkbenchContribution { +export class SaveAccessibilitySignalContribution extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.saveAudioCues'; + static readonly ID = 'workbench.contrib.saveAccessibilitySignal'; constructor( @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { super(); - this._register(this._workingCopyService.onDidSave((e) => { - this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }); - })); + this._register(this._workingCopyService.onDidSave(e => this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }))); } } diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts index 329cabe4bf3..ec79963a27f 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts @@ -76,7 +76,7 @@ export class ShowSignalSoundHelp extends Action2 { qp.onDidChangeActive(() => { accessibilitySignalService.playSound(qp.activeItems[0].signal.sound.getSound(true), true); }); - qp.placeholder = localize('audioCues.help.placeholder', 'Select a sound to play and configure'); + qp.placeholder = localize('sounds.help.placeholder', 'Select a sound to play and configure'); qp.canSelectMany = true; await qp.show(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index b6c66797247..73a0ebd76a9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -15,7 +15,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi declare readonly _serviceBrand: undefined; - private _pendingCueMap: DisposableMap = this._register(new DisposableMap()); + private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); private _requestId: number = 0; @@ -25,11 +25,11 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi acceptRequest(): number { this._requestId++; this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true }); - this._pendingCueMap.set(this._requestId, this._instantiationService.createInstance(AudioCueScheduler)); + this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilitySignalScheduler)); return this._requestId; } acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { - this._pendingCueMap.deleteAndDispose(requestId); + this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.asString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); @@ -46,19 +46,19 @@ const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; /** * Schedules an audio cue to play when a chat response is pending for too long. */ -class AudioCueScheduler extends Disposable { +class AccessibilitySignalScheduler extends Disposable { private _scheduler: RunOnceScheduler; - private _audioCueLoop: IDisposable | undefined; + private _signalLoop: IDisposable | undefined; constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService) { super(); this._scheduler = new RunOnceScheduler(() => { - this._audioCueLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); + this._signalLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); }, CHAT_RESPONSE_PENDING_ALLOWANCE_MS); this._scheduler.schedule(); } override dispose(): void { super.dispose(); - this._audioCueLoop?.dispose(); + this._signalLoop?.dispose(); this._scheduler.cancel(); this._scheduler.dispose(); } From a685b549ce8248bca22a2cf9aeddf39166413661 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:59:31 -0800 Subject: [PATCH 143/228] Check detected link suffix when opening search links This pulls in some of the smarts from verified links into search links by detecting links with suffixes, seeing if it matches and then using the suffix when opening the quick pick. This allows opening the link `foo` on the line `'foo', line 10` and it will open a search for `foo:10`. Fixes #190847 --- .../terminalContrib/links/browser/links.ts | 5 +++++ .../links/browser/terminalLinkOpeners.ts | 19 ++++++++++++++++++- .../links/browser/terminalWordLinkDetector.ts | 3 ++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts index b680864e23d..389f40afc6e 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts @@ -83,6 +83,11 @@ export interface ITerminalSimpleLink { */ uri?: URI; + /** + * An optional full line to be used for context when resolving. + */ + contextLine?: string; + /** * The location or selection range of the link. */ diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts index 6b5af707012..d4247bbd835 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts @@ -22,7 +22,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; +import { detectLinks, getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { @@ -98,10 +98,27 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { async open(link: ITerminalSimpleLink): Promise { const osPath = osPathModule(this._getOS()); const pathSeparator = osPath.sep; + // Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format let text = link.text.replace(/^file:\/\/\/?/, ''); text = osPath.normalize(text).replace(/^(\.+[\\/])+/, ''); + // Try extract any trailing line and column numbers by matching the text against parsed + // links. This will give a search link `foo` on a line like `"foo", line 10` to open the + // quick pick with `foo:10` as the contents. + if (link.contextLine) { + const parsedLinks = detectLinks(link.contextLine, this._getOS()); + const matchingParsedLink = parsedLinks.find(parsedLink => parsedLink.suffix && link.text === parsedLink.path.text); + if (matchingParsedLink) { + if (matchingParsedLink.suffix?.row !== undefined) { + text += `:${matchingParsedLink.suffix.row}`; + if (matchingParsedLink.suffix?.col !== undefined) { + text += `:${matchingParsedLink.suffix.col}`; + } + } + } + } + // Remove `:` from the end of the link. // Examples: // - Ruby stack traces: :in ... diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts index 1675d0fac73..02a9b2cc39f 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts @@ -103,7 +103,8 @@ export class TerminalWordLinkDetector extends Disposable implements ITerminalLin links.push({ text: word.text, bufferRange, - type: TerminalBuiltinLinkType.Search + type: TerminalBuiltinLinkType.Search, + contextLine: text }); } From dbc6c2972fae62097f133815c50fe5d16ea2ac83 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 13:24:40 -0800 Subject: [PATCH 144/228] Support new ' FILE ...' link format Fixes #200166 --- .../links/browser/terminalLocalLinkDetector.ts | 2 ++ .../links/test/browser/terminalLocalLinkDetector.test.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts index 259df08a249..adee3ee9f1f 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts @@ -37,6 +37,8 @@ const enum Constants { const fallbackMatchers: RegExp[] = [ // Python style error: File "", line /^ *File (?"(?.+)"(, line (?\d+))?)/, + // Unknown tool #200166: FILE :: + /^ +FILE +(?(?.+)(?::(?\d+)(?::(?\d+))?)?)/, // Some C++ compile error formats: // C:\foo\bar baz(339) : error ... // C:\foo\bar baz(339,12) : error ... diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index faae5d685ee..785ad9658ab 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -138,6 +138,10 @@ const supportedFallbackLinkFormats: LinkFormatInfo[] = [ // Python style error: File "", line { urlFormat: 'File "{0}"', linkCellStartOffset: 5 }, { urlFormat: 'File "{0}", line {1}', line: '5', linkCellStartOffset: 5 }, + // Unknown tool #200166: FILE :: + { urlFormat: ' FILE {0}', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}', line: '5', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}:{2}', line: '5', column: '3', linkCellStartOffset: 7 }, // Some C++ compile error formats { urlFormat: '{0}({1}) :', line: '5', linkCellEndOffset: -2 }, { urlFormat: '{0}({1},{2}) :', line: '5', column: '3', linkCellEndOffset: -2 }, From 9f00156541273d204ae6c246b84db9dd01f25294 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 14:01:13 -0800 Subject: [PATCH 145/228] fix #205601 --- src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 43246667cfe..0b5277e586d 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -2417,6 +2417,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private async _computeTasksForSingleConfig(workspaceFolder: IWorkspaceFolder, config: TaskConfig.IExternalTaskRunnerConfiguration | undefined, runSource: TaskRunSource, custom: CustomTask[], customized: IStringDictionary, source: TaskConfig.TaskConfigSource, isRecentTask: boolean = false): Promise { if (!config) { return false; + } else if (!workspaceFolder) { + this._logService.trace('TaskService.computeTasksForSingleConfig: no workspace folder'); + return false; } const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); const problemReporter = new ProblemReporter(this._outputChannel); From 82af505ffd2378e10eb782b5108d2a39bf354562 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 14:05:08 -0800 Subject: [PATCH 146/228] improve log --- 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 0b5277e586d..c32972eee7b 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -2418,7 +2418,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!config) { return false; } else if (!workspaceFolder) { - this._logService.trace('TaskService.computeTasksForSingleConfig: no workspace folder'); + this._logService.trace('TaskService.computeTasksForSingleConfig: no workspace folder for worskspace', this._workspace?.id); return false; } const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); From 1943dda2d976b5ee3b5c976bbcea7fdf373027dd Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Thu, 29 Feb 2024 16:04:15 -0800 Subject: [PATCH 147/228] further delay IW contribution registration (#206574) --- .../interactive/browser/interactive.contribution.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 6d300714340..9f3899d7961 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -253,9 +253,13 @@ class InteractiveWindowWorkingCopyEditorHandler extends Disposable implements IW } } -registerWorkbenchContribution2(InteractiveDocumentContribution.ID, InteractiveDocumentContribution, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(InteractiveInputContentProvider.ID, InteractiveInputContentProvider, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(InteractiveWindowWorkingCopyEditorHandler.ID, InteractiveWindowWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(InteractiveDocumentContribution.ID, InteractiveDocumentContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(InteractiveInputContentProvider.ID, InteractiveInputContentProvider, { + editorTypeId: INTERACTIVE_WINDOW_EDITOR_ID +}); +registerWorkbenchContribution2(InteractiveWindowWorkingCopyEditorHandler.ID, InteractiveWindowWorkingCopyEditorHandler, { + editorTypeId: INTERACTIVE_WINDOW_EDITOR_ID +}); type interactiveEditorInputData = { resource: URI; inputResource: URI; name: string; language: string }; From 31a6c35865768eedec770e03c08dea1f74cd50c2 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:05:48 -0800 Subject: [PATCH 148/228] add label in issue reporter when custom URI added (#206577) add label when there is custom uri --- .../issue/issueReporterService.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 067f09fd0bd..851f5c38798 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -501,6 +501,24 @@ export class IssueReporter extends Disposable { this.previewButton.enabled = false; this.previewButton.label = localize('loadingData', "Loading data..."); } + + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension && selectedExtension.uri) { + const extensionLink = document.createElement('a'); + extensionLink.href = URI.revive(selectedExtension.uri).toString(); + extensionLink.textContent = selectedExtension.id; + const issueReporterElement = this.getElementById('issue-reporter')!; + Object.assign(extensionLink.style, { + alignSelf: 'flex-end', + display: 'block', + fontSize: '13px', + marginBottom: '10px', + padding: '4px 0px', + textDecoration: 'none', + width: 'auto' + }); + issueReporterElement.appendChild(extensionLink); + } } private isPreviewEnabled() { From d52acfa1b4a4e92cf225964f7a944096077cdfab Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Thu, 29 Feb 2024 19:08:15 -0600 Subject: [PATCH 149/228] add onWillHide to quickpick (#206576) --- src/vs/platform/quickinput/browser/quickInput.ts | 6 ++++++ .../quickinput/browser/quickInputController.ts | 1 + src/vs/platform/quickinput/common/quickInput.ts | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 73c3a23a305..2bfd12c91cb 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -162,6 +162,7 @@ class QuickInput extends Disposable implements IQuickInput { private _lastSeverity: Severity | undefined; private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); private readonly onDidHideEmitter = this._register(new Emitter()); + private readonly onWillHideEmitter = this._register(new Emitter()); private readonly onDisposeEmitter = this._register(new Emitter()); protected readonly visibleDisposables = this._register(new DisposableStore()); @@ -352,6 +353,11 @@ class QuickInput extends Disposable implements IQuickInput { readonly onDidHide = this.onDidHideEmitter.event; + willHide(reason = QuickInputHideReason.Other): void { + this.onWillHideEmitter.fire({ reason }); + } + readonly onWillHide = this.onWillHideEmitter.event; + protected update() { if (!this.visible) { return; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index d5a23ae66e8..10529b5b903 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -614,6 +614,7 @@ export class QuickInputController extends Disposable { if (!controller) { return; } + controller.willHide(reason); const container = this.ui?.container; const focusChanged = container && !dom.isAncestorOfActiveElement(container); diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 4ccce2c7120..ef907680e5c 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -209,6 +209,11 @@ export interface IQuickInput extends IDisposable { */ readonly onDidHide: Event; + /** + * An event that is fired when the quick input will be hidden. + */ + readonly onWillHide: Event; + /** * An event that is fired when the quick input is disposed. */ @@ -285,6 +290,12 @@ export interface IQuickInput extends IDisposable { * @param reason The reason why the quick input was hidden. */ didHide(reason?: QuickInputHideReason): void; + + /** + * Notifies that the quick input will be hidden. + * @param reason The reason why the quick input will be hidden. + */ + willHide(reason?: QuickInputHideReason): void; } export interface IQuickWidget extends IQuickInput { From 19ecb4b8337d0871f0a204853003a609d716b04e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 29 Feb 2024 22:23:07 -0300 Subject: [PATCH 150/228] Use input.foreground for chat input (#206564) Fix microsoft/vscode-copilot-release#958 --- build/lib/stylelint/vscode-known-variables.json | 3 ++- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index ffbd6ae9edd..51d8c95ae17 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -47,6 +47,7 @@ "--vscode-chat-requestBorder", "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", + "--vscode-chat-list-background", "--vscode-checkbox-background", "--vscode-checkbox-border", "--vscode-checkbox-foreground", @@ -827,4 +828,4 @@ "--zoom-factor", "--test-bar-width" ] -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 20f6f5d621f..793c8f1032a 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -359,6 +359,10 @@ border-color: var(--vscode-focusBorder); } +.interactive-session .interactive-input-and-execute-toolbar .monaco-editor .mtk1 { + color: var(--vscode-input-foreground); +} + .interactive-session .interactive-input-and-execute-toolbar .monaco-editor, .interactive-session .interactive-input-and-execute-toolbar .monaco-editor .monaco-editor-background { background-color: var(--vscode-input-background) !important; From 8f58a72088e36c8aa052c27dd7322f87359b71d7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 1 Mar 2024 11:03:20 +0100 Subject: [PATCH 151/228] . --- src/vs/workbench/contrib/speech/browser/speechService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index a4e23053798..fb511647f02 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -81,6 +81,7 @@ export class SpeechService extends Disposable implements ISpeechService { } else if (this.providers.size > 1) { this.logService.warn(`Multiple speech providers registered. Picking first one: ${provider.metadata.displayName}`); } + const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); const session = this._activeSpeechToTextSession = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); From be59ec51ee54d0e305fd73c092449a5d839ab362 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 1 Mar 2024 05:02:41 -0800 Subject: [PATCH 152/228] Add learnMore proposed API to forceNewSession (#206588) ref https://github.com/microsoft/vscode/issues/206587 --- .../api/browser/mainThreadAuthentication.ts | 58 +++++++++++++++---- .../workbench/api/common/extHost.api.impl.ts | 3 + .../common/extensionsApiProposals.ts | 1 + .../vscode.proposed.authLearnMore.d.ts | 16 +++++ 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.authLearnMore.d.ts diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 3bb8ef8fbe4..b3ebdd940c3 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -8,16 +8,30 @@ 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 { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import type { AuthenticationGetSessionOptions } from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +interface AuthenticationForceNewSessionOptions { + detail?: string; + learnMore?: UriComponents; + sessionToRecreate?: AuthenticationSession; +} + +interface AuthenticationGetSessionOptions { + clearSessionPreference?: boolean; + createIfNone?: boolean; + forceNewSession?: boolean | AuthenticationForceNewSessionOptions; + silent?: boolean; +} export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -64,7 +78,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @IDialogService private readonly dialogService: IDialogService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -102,18 +117,38 @@ 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, detail?: string): Promise { + 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); - const { confirmed } = await this.dialogService.confirm({ + + const buttons: IPromptButton[] = [ + { + label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), + run() { + return true; + }, + } + ]; + if (options?.learnMore) { + buttons.push({ + label: nls.localize('learnMore', "Learn more"), + run: async () => { + const result = this.loginPrompt(providerName, extensionName, recreatingSession, options); + await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true }); + return await result; + } + }); + } + const { result } = await this.dialogService.prompt({ type: Severity.Info, message, - detail, - primaryButton: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow") + buttons, + detail: options?.detail, + cancelButton: true, }); - return confirmed; + return result ?? false; } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { @@ -156,12 +191,15 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We may need to prompt because we don't have a valid session // modal flows if (options.createIfNone || options.forceNewSession) { - const detail = (typeof options.forceNewSession === 'object') ? options.forceNewSession.detail : undefined; + let uiOptions: AuthenticationForceNewSessionOptions | undefined; + if (typeof options.forceNewSession === 'object') { + uiOptions = options.forceNewSession; + } // 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, detail); + const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, uiOptions); if (!isAllowed) { throw new Error('User did not consent to login.'); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index da678cb6efb..05d95911654 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -286,6 +286,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const authentication: typeof vscode.authentication = { getSession(providerId: string, scopes: readonly string[], options?: vscode.AuthenticationGetSessionOptions) { + if (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) { + checkProposedApiEnabled(extension, 'authLearnMore'); + } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, getSessions(providerId: string, scopes: readonly string[]) { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 071545be93b..8571e00fc4d 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -10,6 +10,7 @@ export const allApiProposals = Object.freeze({ 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', 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', 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', diff --git a/src/vscode-dts/vscode.proposed.authLearnMore.d.ts b/src/vscode-dts/vscode.proposed.authLearnMore.d.ts new file mode 100644 index 00000000000..c324e24f1a6 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.authLearnMore.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * 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/206587 + + export interface AuthenticationForceNewSessionOptions { + /** + * An optional Uri to open in the browser to learn more about this authentication request. + */ + learnMore?: Uri; + } +} From bd79cb3a4671c6981533e308d18d4935ae847037 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 1 Mar 2024 11:32:52 -0300 Subject: [PATCH 153/228] Change FollowupProvider to take a ChatContext. (#206611) * Change FollowupProvider to take a ChatContext. Also fix #205761 * Update test --- .../src/singlefolder-tests/chat.test.ts | 2 +- .../workbench/api/browser/mainThreadChatAgents2.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostChatAgents2.ts | 11 +++++++---- src/vs/workbench/contrib/chat/common/chatAgents.ts | 12 ++++++------ .../workbench/contrib/chat/common/chatServiceImpl.ts | 2 +- .../contrib/chat/test/common/voiceChat.test.ts | 2 +- src/vscode-dts/vscode.proposed.chatParticipant.d.ts | 4 ++-- 8 files changed, 21 insertions(+), 18 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 9877eb1dd92..2ed59099b34 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -98,7 +98,7 @@ suite('chat', () => { }); participant.isDefault = true; participant.followupProvider = { - provideFollowups(result, _token) { + provideFollowups(result, _context, _token) { deferred.complete(result); return []; }, diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index cf3d1e134dd..445e29220e7 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -91,12 +91,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._pendingProgress.delete(request.requestId); } }, - provideFollowups: async (request, result, token): Promise => { + provideFollowups: async (request, result, history, token): Promise => { if (!this._agents.get(handle)?.hasFollowups) { return []; } - return this._proxy.$provideFollowups(request, handle, result, token); + return this._proxy.$provideFollowups(request, handle, result, { history }, token); }, provideWelcomeMessage: (token: CancellationToken) => { return this._proxy.$provideWelcomeMessage(handle, token); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d64dbc0af3f..485c62e477d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1227,7 +1227,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, 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; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 13bedf2b1e8..07a808d31b9 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -245,14 +245,16 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._sessionDisposables.deleteAndDispose(sessionId); } - async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise { + async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { return Promise.resolve([]); } + const convertedHistory = await this.prepareHistoryTurns(agent.id, context); + const ehResult = typeConvert.ChatAgentResult.to(result); - return (await agent.provideFollowups(ehResult, token)) + return (await agent.provideFollowups(ehResult, { history: convertedHistory }, token)) .filter(f => { // The followup must refer to a participant that exists from the same extension const isValid = !f.participant || Iterable.some( @@ -376,11 +378,12 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } - async provideFollowups(result: vscode.ChatResult, token: CancellationToken): Promise { + async provideFollowups(result: vscode.ChatResult, context: vscode.ChatContext, token: CancellationToken): Promise { if (!this._followupProvider) { return []; } - const followups = await this._followupProvider.provideFollowups(result, token); + + const followups = await this._followupProvider.provideFollowups(result, context, token); if (!followups) { return []; } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index b9c3a5d4508..2d9318102b8 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -37,7 +37,7 @@ export interface IChatAgentData { export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; + provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; provideSampleQuestions?(token: CancellationToken): ProviderResult; } @@ -118,7 +118,7 @@ export interface IChatAgentService { registerAgent(name: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getRegisteredAgents(): Array; getActivatedAgents(): Array; getRegisteredAgent(id: string): IChatAgentData | undefined; @@ -236,7 +236,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return await data.impl.invoke(request, progress, history, token); } - async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { + async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); if (!data?.impl) { throw new Error(`No activated agent with id ${id}`); @@ -246,7 +246,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return []; } - return data.impl.provideFollowups(request, result, token); + return data.impl.provideFollowups(request, result, history, token); } } @@ -266,9 +266,9 @@ export class MergedChatAgent implements IChatAgent { return this.impl.invoke(request, progress, history, token); } - async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { if (this.impl.provideFollowups) { - return this.impl.provideFollowups(request, result, token); + return this.impl.provideFollowups(request, result, history, token); } return []; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index a4a77bb0ba2..c5835557350 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -551,7 +551,7 @@ export class ChatService extends Disposable implements IChatService { const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); rawResult = agentResult; - agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, followupsCancelToken); + agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { request = model.addRequest(parsedRequest, { variables: [] }); // contributed slash commands 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 864ba72ac0d..dba9fb7a613 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -50,7 +50,7 @@ suite('VoiceChat', () => { registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { throw new Error(); } + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } getRegisteredAgents(): Array { return agents; } getActivatedAgents(): IChatAgent[] { return agents; } getRegisteredAgent(id: string): IChatAgent | undefined { throw new Error(); } diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index b6794145df2..f3b59ac9e84 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -178,7 +178,7 @@ declare module 'vscode' { * @param result This instance 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, token: CancellationToken): ProviderResult; + provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; } /** @@ -273,7 +273,7 @@ declare module 'vscode' { /** * The prompt as entered by the user. * - * Information about variables used in this request are is stored in {@link ChatRequest.variables}. + * Information about variables used in this request is stored in {@link ChatRequest.variables}. * * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} * are not part of the prompt. From 2752932c0798c092be0d18209114c0289de8d492 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 1 Mar 2024 12:41:22 -0300 Subject: [PATCH 154/228] Remove `sendInteractiveRequestToProvider` from interactive API (#206623) * Remove `sendInteractiveRequestToProvider` from interactive API * Fixes --- .../src/singlefolder-tests/chat.test.ts | 10 +++++----- src/vs/workbench/api/browser/mainThreadChat.ts | 9 +-------- src/vs/workbench/api/common/extHost.api.impl.ts | 4 ---- src/vs/workbench/api/common/extHost.protocol.ts | 3 +-- src/vs/workbench/api/common/extHostChat.ts | 4 ---- src/vs/workbench/contrib/chat/common/chatService.ts | 1 - .../workbench/contrib/chat/common/chatServiceImpl.ts | 7 +------ .../contrib/chat/test/common/chatService.test.ts | 12 ------------ .../contrib/chat/test/common/mockChatService.ts | 9 +++------ src/vscode-dts/vscode.proposed.interactive.d.ts | 4 ---- 10 files changed, 11 insertions(+), 52 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 2ed59099b34..1a07b475074 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; +import { commands, CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; import { DeferredPromise, assertNoRpc, closeAllEditors, disposeAll } from '../utils'; suite('chat', () => { @@ -51,7 +51,7 @@ suite('chat', () => { test('participant and slash command', async () => { const onRequest = setupParticipant(); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); let i = 0; onRequest(request => { @@ -59,7 +59,7 @@ suite('chat', () => { assert.deepStrictEqual(request.request.command, 'hello'); assert.strictEqual(request.request.prompt, 'friend'); i++; - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); } else { assert.strictEqual(request.context.history.length, 1); assert.strictEqual(request.context.history[0].participant.name, 'participant'); @@ -76,7 +76,7 @@ suite('chat', () => { })); const deferred = getDeferredForRequest(); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant hi #myVar' }); + 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].values[0].value, 'myValue'); @@ -105,7 +105,7 @@ suite('chat', () => { }; disposables.push(participant); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); const result = await deferred.p; assert.deepStrictEqual(result.metadata, { key: 'value' }); }); diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index c7155e716a9..e70553d866f 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -9,7 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostChatShape, ExtHostContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadChat) @@ -83,13 +83,6 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { this._stateEmitters.get(sessionId)?.fire(state); } - async $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(providerId); - if (widget && widget.viewModel) { - this._chatService.sendRequestToProvider(widget.viewModel.sessionId, message); - } - } - async $unregisterChatProvider(handle: number): Promise { this._providerRegistrations.deleteAndDispose(handle); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 05d95911654..a719b6131d3 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1388,10 +1388,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'interactive'); return extHostChat.registerChatProvider(extension, id, provider); }, - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest) { - checkProposedApiEnabled(extension, 'interactive'); - return extHostChat.sendInteractiveRequestToProvider(providerId, message); - }, transferChatSession(session: vscode.InteractiveSession, toWorkspace: vscode.Uri) { checkProposedApiEnabled(extension, 'interactive'); return extHostChat.transferChatSession(session, toWorkspace); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 485c62e477d..b67fd890e78 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 { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatDynamicRequest, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +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 { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; @@ -1313,7 +1313,6 @@ export type IChatProgressDto = export interface MainThreadChatShape extends IDisposable { $registerChatProvider(handle: number, id: string): Promise; $acceptChatState(sessionId: number, state: any): Promise; - $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): void; $unregisterChatProvider(handle: number): Promise; $transferChatSession(sessionId: number, toWorkspace: UriComponents): void; } diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index d125a8b8f79..9b806f07947 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -58,10 +58,6 @@ export class ExtHostChat implements ExtHostChatShape { this._proxy.$transferChatSession(sessionId, newWorkspace); } - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest): void { - this._proxy.$sendRequestToProvider(providerId, message); - } - async $prepareChat(handle: number, token: CancellationToken): Promise { const entry = this._chatProvider.get(handle); if (!entry) { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 5da8d9b747b..360d8ea0e34 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -293,7 +293,6 @@ export interface IChatService { cancelCurrentRequestForSession(sessionId: string): void; clearSession(sessionId: string): void; addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void; - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void; getHistory(): IChatDetail[]; clearAllHistoryEntries(): void; removeHistoryEntry(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index c5835557350..e254395407f 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -25,7 +25,7 @@ import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatCo import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; @@ -634,11 +634,6 @@ export class ChatService extends Disposable implements IChatService { model.removeRequest(requestId); } - async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise<{ responseCompletePromise: Promise } | undefined> { - this.trace('sendRequestToProvider', `sessionId: ${sessionId}`); - return await this.sendRequest(sessionId, message.message); - } - getProviders(): string[] { return Array.from(this._providers.keys()); } 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 0cd1c90c3ce..ab9ef913673 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -195,18 +195,6 @@ suite('Chat', () => { }, 'Expected to throw for dupe provider'); }); - test('sendRequestToProvider', async () => { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); - - const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None)); - assert.strictEqual(model.getRequests().length, 0); - - const response = await testService.sendRequestToProvider(model.sessionId, { message: 'test request' }); - await response?.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 1); - }); - test('addCompleteRequest', async () => { const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 35c17753392..d961bbb74c5 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatCompleteResponse, IChatDetail, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; export class MockChatService implements IChatService { _serviceBrand: undefined; @@ -60,9 +60,6 @@ export class MockChatService implements IChatService { addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void { throw new Error('Method not implemented.'); } - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void { - throw new Error('Method not implemented.'); - } getHistory(): IChatDetail[] { throw new Error('Method not implemented.'); } diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 5644d00ce83..852f8830de2 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -125,8 +125,6 @@ declare module 'vscode' { inputPlaceholder?: string; } - export type InteractiveWelcomeMessageContent = string | MarkdownString | ChatFollowup[]; - export interface InteractiveSessionProvider { prepareSession(token: CancellationToken): ProviderResult; } @@ -144,8 +142,6 @@ declare module 'vscode' { export function registerInteractiveSessionProvider(id: string, provider: InteractiveSessionProvider): Disposable; - export function sendInteractiveRequestToProvider(providerId: string, message: InteractiveSessionDynamicRequest): void; - export function registerInteractiveEditorSessionProvider(provider: InteractiveEditorSessionProvider, metadata?: InteractiveEditorSessionProviderMetadata): Disposable; export function transferChatSession(session: InteractiveSession, toWorkspace: Uri): void; From 45fb8ce098c88a060b2a776a7af6a4a889c88d3a Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 1 Mar 2024 07:53:24 -0800 Subject: [PATCH 155/228] Remove check for featured extensions (#206625) --- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 99d6a260886..2f4054f1e28 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -829,7 +829,7 @@ export class GettingStartedPage extends EditorPane { }; const layoutRecentList = () => { - if (this.container.classList.contains('noWalkthroughs') && this.container.classList.contains('noExtensions')) { + if (this.container.classList.contains('noWalkthroughs')) { recentList.setLimit(10); reset(leftColumn, startList.getDomElement()); reset(rightColumn, recentList.getDomElement()); From 0b208e353d259b25ac2921d408d45823562c9bb2 Mon Sep 17 00:00:00 2001 From: Yusuke Yamada Date: Sat, 2 Mar 2024 01:43:52 +0900 Subject: [PATCH 156/228] Fixed to show files in deepest directory in search results (#206609) Fixed to search for FileMatch in addition to the for loop of FolderMatch to check for the presence of AIResults --- src/vs/workbench/contrib/search/browser/searchModel.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 00f9a268b17..4aa0ad102e4 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -1026,6 +1026,15 @@ export class FolderMatch extends Disposable { } } + // FolderMatch on a leaf node does not always run the for-loop of folderMatch because it does not have a FolderMatch as a child. + // Therefore, FileMatch is also searched to check for the existence of AIResults + const fileIterator = this.fileMatchesIterator(); + for (const elem of fileIterator) { + if (elem.hasDownstreamNonAIResults()) { + return true; + } + } + return false; } From bb171489789c9a49e985a4a2c8694138d70d42c1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 1 Mar 2024 18:05:34 +0100 Subject: [PATCH 157/228] Editor pane: make `window` accessor that always returns a result (fix #206467) (#206451) --- src/vs/workbench/browser/editor.ts | 10 ++--- .../browser/parts/editor/binaryDiffEditor.ts | 5 ++- .../browser/parts/editor/binaryEditor.ts | 4 +- .../browser/parts/editor/editorPane.ts | 17 ++++--- .../browser/parts/editor/editorPanes.ts | 14 +++--- .../browser/parts/editor/editorPlaceholder.ts | 19 ++++---- .../parts/editor/editorWithViewState.ts | 17 +++---- .../browser/parts/editor/sideBySideEditor.ts | 15 ++++--- .../browser/parts/editor/textCodeEditor.ts | 5 +-- .../browser/parts/editor/textDiffEditor.ts | 15 ++++--- .../browser/parts/editor/textEditor.ts | 13 +++--- .../parts/editor/textResourceEditor.ts | 8 ++-- src/vs/workbench/common/editor.ts | 5 +-- .../contrib/chat/browser/chatEditor.ts | 4 +- .../contrib/debug/browser/disassemblyView.ts | 9 ++-- .../abstractRuntimeExtensionsEditor.ts | 4 +- .../extensions/browser/extensionEditor.ts | 4 +- .../runtimeExtensionsEditor.ts | 4 +- .../files/browser/editors/binaryFileEditor.ts | 11 ++--- .../files/browser/editors/textFileEditor.ts | 10 ++--- .../files/test/browser/editorAutoSave.test.ts | 2 +- .../interactive/browser/interactiveEditor.ts | 34 ++++++-------- .../mergeEditor/browser/view/mergeEditor.ts | 9 ++-- .../browser/multiDiffEditor.ts | 4 +- .../browser/diff/notebookDiffEditor.ts | 25 +++++------ .../notebook/browser/notebookEditor.ts | 45 +++++++++---------- .../contrib/output/browser/outputView.ts | 10 +++-- .../preferences/browser/keybindingsEditor.ts | 4 +- .../preferences/browser/settingsEditor2.ts | 21 +++++---- .../searchEditor/browser/searchEditor.ts | 7 +-- .../terminal/browser/terminalEditor.ts | 15 ++++--- .../webviewPanel/browser/webviewEditor.ts | 13 +++--- .../browser/gettingStarted.ts | 13 +++--- .../browser/walkThroughPart.ts | 31 ++++++------- .../workspace/browser/workspaceTrustEditor.ts | 4 +- .../editor/test/browser/editorService.test.ts | 34 +++++++------- .../history/browser/historyService.ts | 6 +-- .../test/browser/historyService.test.ts | 22 ++++----- .../browser/parts/editor/editorPane.test.ts | 34 +++++++------- .../parts/editor/textEditorPane.test.ts | 4 +- .../test/browser/workbenchTestServices.ts | 4 +- 41 files changed, 274 insertions(+), 260 deletions(-) diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 7d0b333bece..d5e1a7f3391 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -59,23 +59,23 @@ export class EditorPaneDescriptor implements IEditorPaneDescriptor { static readonly onWillInstantiateEditorPane = EditorPaneDescriptor._onWillInstantiateEditorPane.event; static create( - ctor: { new(...services: Services): EditorPane }, + ctor: { new(group: IEditorGroup, ...services: Services): EditorPane }, typeId: string, name: string ): EditorPaneDescriptor { - return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); + return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); } private constructor( - private readonly ctor: IConstructorSignature, + private readonly ctor: IConstructorSignature, readonly typeId: string, readonly name: string ) { } - instantiate(instantiationService: IInstantiationService): EditorPane { + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): EditorPane { EditorPaneDescriptor._onWillInstantiateEditorPane.fire({ typeId: this.typeId }); - const pane = instantiationService.createInstance(this.ctor); + const pane = instantiationService.createInstance(this.ctor, group); EditorPaneDescriptor.instantiatedEditorPanes.add(this.typeId); return pane; diff --git a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts index bcd00491191..f4314ae0dbb 100644 --- a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts @@ -13,7 +13,7 @@ import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/bina import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; /** @@ -24,6 +24,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { static override readonly ID = BINARY_DIFF_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -33,7 +34,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); + super(group, telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); } getMetadata(): string | undefined { diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index cba829c7be5..52a01908818 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -13,6 +13,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ByteSize } from 'vs/platform/files/common/files'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorPlaceholder, IEditorPlaceholderContents } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise; @@ -33,12 +34,13 @@ export abstract class BaseBinaryResourceEditor extends EditorPlaceholder { constructor( id: string, + group: IEditorGroup, private readonly callbacks: IOpenCallbacks, telemetryService: ITelemetryService, themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } override getTitle(): string { diff --git a/src/vs/workbench/browser/parts/editor/editorPane.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts index 0f26fa1aa20..7a73fe46fc5 100644 --- a/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -24,6 +24,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; +import { getWindowById } from 'vs/base/browser/dom'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -70,8 +71,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { protected _options: IEditorOptions | undefined; get options(): IEditorOptions | undefined { return this._options; } - private _group: IEditorGroup | undefined; - get group(): IEditorGroup | undefined { return this._group; } + get window() { return getWindowById(this.group.windowId, true).window; } /** * Should be overridden by editors that have their own ScopedContextKeyService @@ -80,6 +80,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { constructor( id: string, + readonly group: IEditorGroup, telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService @@ -145,22 +146,20 @@ export abstract class EditorPane extends Composite implements IEditorPane { this._options = options; } - override setVisible(visible: boolean, group?: IEditorGroup): void { + override setVisible(visible: boolean): void { super.setVisible(visible); // Propagate to Editor - this.setEditorVisible(visible, group); + this.setEditorVisible(visible); } /** - * Indicates that the editor control got visible or hidden in a specific group. A - * editor instance will only ever be visible in one editor group. + * Indicates that the editor control got visible or hidden. * * @param visible the state of visibility of this editor - * @param group the editor group this editor is in. */ - protected setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - this._group = group; + protected setEditorVisible(visible: boolean): void { + // Subclasses can implement } setBoundarySashes(_sashes: IBoundarySashes) { diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 1094f315842..28e059443e7 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -10,7 +10,7 @@ import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, createEditorOpenError, isEditorOpenError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getActiveElement, getWindowById } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -131,7 +131,7 @@ export class EditorPanes extends Disposable { try { // Assert the `EditorInputCapabilities.AuxWindowUnsupported` condition - if (getWindow(this.editorGroupParent) !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) { + if (getWindowById(this.groupView.windowId, true).window !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) { return await this.doShowError(createEditorOpenError(localize('editorUnsupportedInAuxWindow', "This type of editor cannot be opened in other windows yet."), [ toAction({ id: 'workbench.editor.action.closeEditor', label: localize('openFolder', "Close Editor"), run: async () => { @@ -277,7 +277,7 @@ export class EditorPanes extends Disposable { if (focus && this.shouldRestoreFocus(activeElement)) { pane.focus(); } else if (!internalOptions?.preserveWindowOrder) { - this.hostService.moveTop(getWindow(this.editorGroupParent)); + this.hostService.moveTop(getWindowById(this.groupView.windowId, true).window); } } @@ -353,7 +353,7 @@ export class EditorPanes extends Disposable { show(container); // Indicate to editor that it is now visible - editorPane.setVisible(true, this.groupView); + editorPane.setVisible(true); // Layout if (this.pagePosition) { @@ -393,7 +393,7 @@ export class EditorPanes extends Disposable { } // Otherwise instantiate new - const editorPane = this._register(descriptor.instantiate(this.instantiationService)); + const editorPane = this._register(descriptor.instantiate(this.instantiationService, this.groupView)); this.editorPanes.push(editorPane); return editorPane; @@ -472,7 +472,7 @@ export class EditorPanes extends Disposable { // the DOM to give a chance to persist certain state that // might depend on still being the active DOM element. this.safeRun(() => this._activeEditorPane?.clearInput()); - this.safeRun(() => this._activeEditorPane?.setVisible(false, this.groupView)); + this.safeRun(() => this._activeEditorPane?.setVisible(false)); // Remove editor pane from parent const editorPaneContainer = this._activeEditorPane.getContainer(); @@ -492,7 +492,7 @@ export class EditorPanes extends Disposable { } setVisible(visible: boolean): void { - this.safeRun(() => this._activeEditorPane?.setVisible(visible, this.groupView)); + this.safeRun(() => this._activeEditorPane?.setVisible(visible)); } layout(pagePosition: IDomNodePagePosition): void { diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index a42032f3c16..7a7691b3850 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -29,6 +29,7 @@ import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IEditorPlaceholderContents { icon: string; @@ -55,11 +56,12 @@ export abstract class EditorPlaceholder extends EditorPane { constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { @@ -186,13 +188,14 @@ export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IStorageService storageService: IStorageService ) { - super(WorkspaceTrustRequiredPlaceholderEditor.ID, telemetryService, themeService, storageService); + super(WorkspaceTrustRequiredPlaceholderEditor.ID, group, telemetryService, themeService, storageService); } override getTitle(): string { @@ -223,18 +226,18 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService ) { - super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService); + super(ErrorPlaceholderEditor.ID, group, telemetryService, themeService, storageService); } protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions, disposables: DisposableStore): Promise { const resource = input.resource; - const group = this.group; const error = options.error; const isFileNotFound = (error)?.fileOperationResult === FileOperationResult.FILE_NOT_FOUND; @@ -274,20 +277,20 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { } }; }); - } else if (group) { + } else { actions = [ { label: localize('retry', "Try Again"), - run: () => group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) + run: () => this.group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) } ]; } // Auto-reload when file is added - if (group && isFileNotFound && resource && this.fileService.hasProvider(resource)) { + if (isFileNotFound && resource && this.fileService.hasProvider(resource)) { disposables.add(this.fileService.onDidFilesChange(e => { if (e.contains(resource, FileChangeType.ADDED, FileChangeType.UPDATED)) { - group.openEditor(input, options); + this.group.openEditor(input, options); } })); } diff --git a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts index f01bedd8f59..e31756007e9 100644 --- a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts +++ b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts @@ -31,6 +31,7 @@ export abstract class AbstractEditorWithViewState extends Edit constructor( id: string, + group: IEditorGroup, viewStateStorageKey: string, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @@ -40,17 +41,17 @@ export abstract class AbstractEditorWithViewState extends Edit @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); this.viewState = this.getEditorMemento(editorGroupService, textResourceConfigurationService, viewStateStorageKey, 100); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { // Listen to close events to trigger `onWillCloseEditorInGroup` - this.groupListener.value = group?.onWillCloseEditor(e => this.onWillCloseEditor(e)); + this.groupListener.value = this.group.onWillCloseEditor(e => this.onWillCloseEditor(e)); - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } private onWillCloseEditor(e: IEditorCloseEvent): void { @@ -110,7 +111,7 @@ export abstract class AbstractEditorWithViewState extends Edit // - the user configured to not restore view state unless the editor is still opened in the group if ( (input.isDisposed() && !this.tracksDisposedEditorViewState()) || - (!this.shouldRestoreEditorViewState(input) && (!this.group || !this.group.contains(input))) + (!this.shouldRestoreEditorViewState(input) && !this.group.contains(input)) ) { this.clearEditorViewState(resource, this.group); } @@ -147,10 +148,6 @@ export abstract class AbstractEditorWithViewState extends Edit } private saveEditorViewState(resource: URI): void { - if (!this.group) { - return; - } - const editorViewState = this.computeEditorViewState(resource); if (!editorViewState) { return; @@ -160,7 +157,7 @@ export abstract class AbstractEditorWithViewState extends Edit } protected loadEditorViewState(input: EditorInput | undefined, context?: IEditorOpenContext): T | undefined { - if (!input || !this.group) { + if (!input) { return undefined; // we need valid input } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index becfd13cac2..c3d7a0cce9d 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -122,6 +122,7 @@ export class SideBySideEditor extends AbstractEditorWithViewState extends return this.editorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.editorControl?.onVisible(); diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 21429e37334..3c298c26b1e 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -58,6 +58,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -68,7 +69,7 @@ export class TextDiffEditor extends AbstractTextEditor imp @IFileService fileService: IFileService, @IPreferencesService private readonly preferencesService: IPreferencesService ) { - super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); + super(TextDiffEditor.ID, group, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); } override getTitle(): string { @@ -171,7 +172,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "At least one file is not displayed in the text compare editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -222,7 +223,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Replace this editor with the binary one - (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + this.group.replaceEditors([{ editor: input, replacement: binaryDiffInput, options: { @@ -232,8 +233,8 @@ export class TextDiffEditor extends AbstractTextEditor imp // and do not control the initial intent that resulted // in us now opening as binary. activation: EditorActivation.PRESERVE, - pinned: this.group?.isPinned(input), - sticky: this.group?.isSticky(input) + pinned: this.group.isPinned(input), + sticky: this.group.isSticky(input) } }]); } @@ -365,8 +366,8 @@ export class TextDiffEditor extends AbstractTextEditor imp return this.diffEditorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.diffEditorControl?.onVisible(); diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 563f12a1be7..f1b5b3a91a8 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -22,7 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorOptions, ITextEditorOptions, TextEditorSelectionRevealType, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; @@ -62,6 +62,7 @@ export abstract class AbstractTextEditor extends Abs constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -71,7 +72,7 @@ export abstract class AbstractTextEditor extends Abs @IEditorGroupsService editorGroupService: IEditorGroupsService, @IFileService protected readonly fileService: IFileService ) { - super(id, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); + super(id, group, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); // Listen to configuration changes this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(e))); @@ -127,8 +128,8 @@ export abstract class AbstractTextEditor extends Abs return editorConfiguration; } - private computeAriaLabel(): string { - return this._input ? computeEditorAriaLabel(this._input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); + protected computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); } private onDidChangeFileSystemProvider(scheme: string): void { @@ -255,12 +256,12 @@ export abstract class AbstractTextEditor extends Abs super.clearInput(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { if (visible) { this.consumePendingConfigurationChangeEvent(); } - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } protected override toEditorViewStateResource(input: EditorInput): URI | undefined { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 7a16fa667b8..0a3b885e01d 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -18,7 +18,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ScrollType, ICodeEditorViewState } from 'vs/editor/common/editorCommon'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/model'; @@ -37,6 +37,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -46,7 +47,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< @IEditorService editorService: IEditorService, @IFileService fileService: IFileService ) { - super(id, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(id, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override async setInput(input: AbstractTextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -130,6 +131,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { static readonly ID = 'workbench.editors.textResourceEditor'; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -141,7 +143,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { @ILanguageService private readonly languageService: ILanguageService, @IFileService fileService: IFileService ) { - super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(TextResourceEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); } protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index c27b43d382c..6cb1ce9b115 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -74,7 +74,7 @@ export interface IEditorDescriptor { /** * Instantiates the editor pane using the provided services. */ - instantiate(instantiationService: IInstantiationService): T; + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): T; /** * Whether the descriptor is for the provided editor pane. @@ -119,7 +119,7 @@ export interface IEditorPane extends IComposite { /** * The assigned group this editor is showing in. */ - readonly group: IEditorGroup | undefined; + readonly group: IEditorGroup; /** * The minimum width of this editor. @@ -327,7 +327,6 @@ export function findViewStateForEditor(input: EditorInput, group: GroupIdentifie */ export interface IVisibleEditorPane extends IEditorPane { readonly input: EditorInput; - readonly group: IEditorGroup; } /** diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 4651778b165..d184cb7525c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -20,6 +20,7 @@ import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInp import { IChatViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { IChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IChatEditorOptions extends IEditorOptions { target: { sessionId: string } | { providerId: string } | { data: ISerializableChatData }; @@ -37,13 +38,14 @@ export class ChatEditor extends EditorPane { private _viewState: IChatViewState | undefined; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { - super(ChatEditorInput.EditorID, telemetryService, themeService, storageService); + super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); } public async clear() { diff --git a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index e092df64537..92f139a3721 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { $, Dimension, addStandardDisposableListener, append, getWindowById } from 'vs/base/browser/dom'; +import { $, Dimension, addStandardDisposableListener, append } from 'vs/base/browser/dom'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { binarySearch2 } from 'vs/base/common/arrays'; @@ -42,6 +42,7 @@ import { InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugMo import { getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { isUri, sourcesEqual } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; interface IDisassembledInstructionEntry { allowBreakpoint: boolean; @@ -92,6 +93,7 @@ export class DisassemblyView extends EditorPane { private readonly _referenceToMemoryAddress = new Map(); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -99,7 +101,7 @@ export class DisassemblyView extends EditorPane { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IDebugService private readonly _debugService: IDebugService, ) { - super(DISASSEMBLY_VIEW_ID, telemetryService, themeService, storageService); + super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService); this._disassembledInstructions = undefined; this._onDidChangeStackFrame = this._register(new Emitter({ leakWarningThreshold: 1000 })); @@ -133,8 +135,7 @@ export class DisassemblyView extends EditorPane { } private createFontInfo() { - const window = getWindowById(this.group?.windowId, true).window; - return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(window).value); + return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value); } get currentInstructionAddresses() { diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 1c278f159d3..7c7fbac3a95 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -38,6 +38,7 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { errorIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; @@ -77,6 +78,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { private _updateSoon: RunOnceScheduler; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -91,7 +93,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { @IClipboardService private readonly _clipboardService: IClipboardService, @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(AbstractRuntimeExtensionsEditor.ID, telemetryService, themeService, storageService); + super(AbstractRuntimeExtensionsEditor.ID, group, telemetryService, themeService, storageService); this._list = null; this._elements = null; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 8f4ca1d9d6a..b2f9fcbd754 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -81,6 +81,7 @@ import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/e import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -228,6 +229,7 @@ export class ExtensionEditor extends EditorPane { private showPreReleaseVersionContextKey: IContextKey | undefined; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -244,7 +246,7 @@ export class ExtensionEditor extends EditorPane { @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { - super(ExtensionEditor.ID, telemetryService, themeService, storageService); + super(ExtensionEditor.ID, group, telemetryService, themeService, storageService); this.extensionReadme = null; this.extensionChangelog = null; this.extensionManifest = null; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index 11dc035fb5b..14cb766b00d 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -30,6 +30,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Schemas } from 'vs/base/common/network'; import { joinPath } from 'vs/base/common/resources'; import { IExtensionFeaturesManagementService } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -65,6 +66,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { private _profileSessionState: IContextKey; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -80,7 +82,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); + super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); this._profileInfo = this._extensionHostProfileService.lastProfile; this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService); this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index ad722bacc27..1b6b279b308 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -15,7 +15,7 @@ import { EditorResolution, IEditorOptions } from 'vs/platform/editor/common/edit import { IEditorResolverService, ResolvedStatus, ResolvedEditor } from 'vs/workbench/services/editor/common/editorResolverService'; import { isEditorInputWithOptions } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; /** * An implementation of editor for binary files that cannot be displayed. @@ -25,14 +25,15 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { static readonly ID = BINARY_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - @IStorageService storageService: IStorageService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService + @IStorageService storageService: IStorageService ) { super( BinaryFileEditor.ID, + group, { openInternal: (input, options) => this.openInternal(input, options) }, @@ -43,7 +44,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } private async openInternal(input: EditorInput, options: IEditorOptions | undefined): Promise { - if (input instanceof FileEditorInput && this.group?.activeEditor) { + if (input instanceof FileEditorInput && this.group.activeEditor) { // We operate on the active editor here to support re-opening // diff editors where `input` may just be one side of the @@ -84,7 +85,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } // Replace the active editor with the picked one - await (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + await this.group.replaceEditors([{ editor: activeEditor, replacement: resolvedEditor?.editor ?? input, options: { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index c0f1c912060..7c01103959b 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -46,6 +46,7 @@ export class TextFileEditor extends AbstractTextCodeEditor static readonly ID = TEXT_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IFileService fileService: IFileService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -65,7 +66,7 @@ export class TextFileEditor extends AbstractTextCodeEditor @IHostService private readonly hostService: IHostService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(TextFileEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); // Clear view state for deleted files this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); @@ -192,7 +193,7 @@ export class TextFileEditor extends AbstractTextCodeEditor } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "The file is not displayed in the text editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -240,7 +241,6 @@ export class TextFileEditor extends AbstractTextCodeEditor private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { const defaultBinaryEditor = this.configurationService.getValue('workbench.editor.defaultBinaryEditor'); - const group = this.group ?? this.editorGroupService.activeGroup; const editorOptions = { ...options, @@ -259,9 +259,9 @@ export class TextFileEditor extends AbstractTextCodeEditor // and avoid enforcing binary or text on the file editor input. if (defaultBinaryEditor && defaultBinaryEditor !== '' && defaultBinaryEditor !== DEFAULT_EDITOR_ASSOCIATION.id) { - this.doOpenAsBinaryInDifferentEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInDifferentEditor(this.group, defaultBinaryEditor, input, editorOptions); } else { - this.doOpenAsBinaryInSameEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInSameEditor(this.group, defaultBinaryEditor, input, editorOptions); } } diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index e1ecd72a649..f104ba762eb 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -104,7 +104,7 @@ suite('EditorAutoSave', () => { assert.strictEqual(model.isDirty(), false); - await editorPane?.group?.closeAllEditors(); + await editorPane?.group.closeAllEditors(); }); function awaitModelSaved(model: ITextFileEditorModel): Promise { diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index e0f91695461..6793200e5e9 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; @@ -63,7 +63,6 @@ import { INTERACTIVE_WINDOW_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import 'vs/css!./interactiveEditor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { deepClone } from 'vs/base/common/objects'; -import { mainWindow } from 'vs/base/browser/window'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -108,7 +107,7 @@ export class InteractiveEditor extends EditorPane { private _editorOptions: IEditorOptions; private _notebookOptions: NotebookOptions; private _editorMemento: IEditorMemento; - private _groupListener = this._register(new DisposableStore()); + private _groupListener = this._register(new MutableDisposable()); private _runbuttonToolbar: ToolBar | undefined; private _onDidFocusWidget = this._register(new Emitter()); @@ -117,6 +116,7 @@ export class InteractiveEditor extends EditorPane { readonly onDidChangeSelection = this._onDidChangeSelection.event; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -137,6 +137,7 @@ export class InteractiveEditor extends EditorPane { ) { super( INTERACTIVE_WINDOW_EDITOR_ID, + group, telemetryService, themeService, storageService @@ -160,7 +161,7 @@ export class InteractiveEditor extends EditorPane { this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(DOM.getWindowById(this.group?.windowId, true).window ?? mainWindow, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); //TODO@bpasero might crash this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); @@ -313,7 +314,7 @@ export class InteractiveEditor extends EditorPane { } private _saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this._notebookWidget.value && input instanceof InteractiveEditorInput) { + if (this._notebookWidget.value && input instanceof InteractiveEditorInput) { if (this._notebookWidget.value.isDisposed) { return; } @@ -328,10 +329,7 @@ export class InteractiveEditor extends EditorPane { } private _loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined { - let result: InteractiveEditorViewState | undefined; - if (this.group) { - result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); - } + const result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); if (result) { return result; } @@ -351,7 +349,6 @@ export class InteractiveEditor extends EditorPane { } override async setInput(input: InteractiveEditorInput, options: InteractiveEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const group = this.group!; const notebookInput = input.notebookEditorInput; // there currently is a widget which we still own so @@ -362,9 +359,7 @@ export class InteractiveEditor extends EditorPane { this._widgetDisposableStore.clear(); - const codeWindow = this.group ? DOM.getWindowById(group.windowId, true).window : mainWindow; - - this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, notebookInput, { + this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, notebookInput, { isEmbedded: true, isReadOnly: true, contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([ @@ -388,8 +383,8 @@ export class InteractiveEditor extends EditorPane { MarkerController.ID ]), options: this._notebookOptions, - codeWindow: codeWindow - }, undefined, this._rootElement ? DOM.getWindow(this._rootElement) : mainWindow); + codeWindow: this.window + }, undefined, this.window); this._codeEditorWidget = this._instantiationService.createInstance(CodeEditorWidget, this._inputEditorContainer, this._editorOptions, { ...{ @@ -681,12 +676,9 @@ export class InteractiveEditor extends EditorPane { this._notebookWidget.value!.focus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - if (group) { - this._groupListener.clear(); - this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); - } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.value = this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)); if (!visible) { this._saveEditorViewState(this.input); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 45af28b94f0..3609b0046fa 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -108,6 +108,7 @@ export class MergeEditor extends AbstractTextEditor { private readonly scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); constructor( + group: IEditorGroup, @IInstantiationService instantiation: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @@ -121,7 +122,7 @@ export class MergeEditor extends AbstractTextEditor { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(MergeEditor.ID, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(MergeEditor.ID, group, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override dispose(): void { @@ -354,7 +355,7 @@ export class MergeEditor extends AbstractTextEditor { // all empty -> replace this editor with a normal editor for result that.editorService.replaceEditors( [{ editor: input, replacement: { resource: input.result, options: { preserveFocus: true } }, forceReplaceDirty: true }], - that.group ?? that.editorGroupService.activeGroup + that.group ); } }); @@ -467,8 +468,8 @@ export class MergeEditor extends AbstractTextEditor { return super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { if (visible) { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index 6f66dbec274..ffe7a8738f5 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -19,7 +19,7 @@ import { ICompositeControl } from 'vs/workbench/common/composite'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { URI } from 'vs/base/common/uri'; import { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; @@ -39,6 +39,7 @@ export class MultiDiffEditor extends AbstractEditorWithViewState('editor'); - return FontMeasurements.readFontInfo(window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(window).value)); + return FontMeasurements.readFontInfo(this.window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(this.window).value)); } private isOverviewRulerEnabled(): boolean { @@ -271,7 +270,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD NotebookTextDiffList, 'NotebookTextDiff', this._listViewContainer, - this.instantiationService.createInstance(NotebookCellTextDiffListDelegate, DOM.getWindow(this._listViewContainer)), + this.instantiationService.createInstance(NotebookCellTextDiffListDelegate, this.window), renderers, this.contextKeyService, { @@ -462,7 +461,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _attachModel() { this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); const updateInsets = () => { - DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + DOM.scheduleAtNextAnimationFrame(this.window, () => { if (this._isDisposed) { return; } @@ -499,7 +498,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); - this._modifiedWebview.createWebview(DOM.getActiveWindow()); + this._modifiedWebview.createWebview(this.window); this._modifiedWebview.element.style.width = `calc(50% - 16px)`; this._modifiedWebview.element.style.left = `calc(50%)`; } @@ -516,7 +515,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); - this._originalWebview.createWebview(DOM.getActiveWindow()); + this._originalWebview.createWebview(this.window); this._originalWebview.element.style.width = `calc(50% - 16px)`; this._originalWebview.element.style.left = `16px`; } @@ -776,7 +775,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const webview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; - DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + DOM.scheduleAtNextAnimationFrame(this.window, () => { webview?.ackHeight([{ cellId: cellInfo.cellId, outputId, height }]); }, 10); } @@ -794,7 +793,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } let r: () => void; - const layoutDisposable = DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + const layoutDisposable = DOM.scheduleAtNextAnimationFrame(this.window, () => { this.pendingLayouts.delete(cell); relayout(cell, height); @@ -978,10 +977,6 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return this; } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - } - override clearInput(): void { super.clearInput(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 0a4cc62f4ed..dacf253ffed 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -76,6 +76,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { readonly onDidChangeSelection = this._onDidChangeSelection.event; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -94,7 +95,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { @INotebookEditorWorkerService private readonly _notebookEditorWorkerService: INotebookEditorWorkerService, @IPreferencesService private readonly _preferencesService: IPreferencesService ) { - super(NotebookEditor.ID, telemetryService, themeService, storageService); + super(NotebookEditor.ID, group, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); this._register(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._onDidChangeFileSystemProvider(e.scheme))); @@ -150,24 +151,22 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { return this._widget.value; } - override setVisible(visible: boolean, group?: IEditorGroup | undefined): void { - super.setVisible(visible, group); + override setVisible(visible: boolean): void { + super.setVisible(visible); if (!visible) { this._widget.value?.onWillHide(); } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - if (group) { - this._groupListener.clear(); - this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); - this._groupListener.add(group.onDidModelChange(() => { - if (this._editorGroupService.activeGroup !== group) { - this._widget?.value?.updateEditorFocus(); - } - })); - } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.clear(); + this._groupListener.add(this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); + this._groupListener.add(this.group.onDidModelChange(() => { + if (this._editorGroupService.activeGroup !== this.group) { + this._widget?.value?.updateEditorFocus(); + } + })); if (!visible) { this._saveEditorViewState(this.input); @@ -203,7 +202,6 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { const perf = new NotebookPerfMarks(); perf.mark('startTime'); - const group = this.group!; this._inputListener.value = input.onDidChangeCapabilities(() => this._onDidChangeInputCapabilities(input)); @@ -213,7 +211,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { // we need to hide it before getting a new widget this._widget.value?.onWillHide(); - this._widget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input, undefined, this._pagePosition?.dimension, DOM.getWindowById(group.windowId, true).window); + this._widget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, input, undefined, this._pagePosition?.dimension, this.window); if (this._rootElement && this._widget.value!.getDomNode()) { this._rootElement.setAttribute('aria-flowto', this._widget.value!.getDomNode().id || ''); @@ -319,7 +317,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._widgetDisposableStore.add(this._widget.value.onDidBlurWidget(() => this._onDidBlurWidget.fire())); this._widgetDisposableStore.add(this._editorGroupService.createEditorDropTarget(this._widget.value.getDomNode(), { - containsGroup: (group) => this.group?.id === group.id + containsGroup: (group) => this.group.id === group.id })); perf.mark('editorLoaded'); @@ -338,7 +336,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } // Handle case where a file is too large to open without confirmation - if ((e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (e instanceof TooLargeFileOperationError) { message = localize('notebookTooLargeForHeapErrorWithSize', "The notebook is not displayed in the notebook editor because it is very large ({0}).", ByteSize.formatSize(e.size)); @@ -512,7 +510,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { private _saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this._widget.value && input instanceof NotebookEditorInput) { + if (this._widget.value && input instanceof NotebookEditorInput) { if (this._widget.value.isDisposed) { return; } @@ -523,10 +521,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } private _loadNotebookEditorViewState(input: NotebookEditorInput): INotebookEditorViewState | undefined { - let result: INotebookEditorViewState | undefined; - if (this.group) { - result = this._editorMemento.loadEditorState(this.group, input.resource); - } + const result = this._editorMemento.loadEditorState(this.group, input.resource); if (result) { return result; } @@ -545,11 +540,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._rootElement.classList.toggle('narrow-width', dimension.width < 600); this._pagePosition = { dimension, position }; - if (!this._widget.value || !(this._input instanceof NotebookEditorInput)) { + if (!this._widget.value || !(this.input instanceof NotebookEditorInput)) { return; } - if (this._input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) { + if (this.input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) { // input and widget mismatch // this happens when // 1. open document A, pin the document diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index b7484919efa..53206dacd2e 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -33,6 +33,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; +import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; export class OutputViewPane extends ViewPane { @@ -159,10 +160,9 @@ class OutputEditor extends AbstractTextResourceEditor { @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, - @IFileService fileService: IFileService, - @IContextKeyService contextKeyService: IContextKeyService, + @IFileService fileService: IFileService ) { - super(OUTPUT_VIEW_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(OUTPUT_VIEW_ID, editorGroupService.activeGroup /* TODO@bpasero this is wrong */, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey)); } @@ -213,6 +213,10 @@ class OutputEditor extends AbstractTextResourceEditor { return this.input ? this.input.getAriaLabel() : nls.localize('outputViewAriaLabel', "Output panel"); } + protected override computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, undefined, this.editorGroupService.count) : this.getAriaLabel(); + } + override async setInput(input: TextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const focus = !(options && options.preserveFocus); if (this.input && input.matches(this.input)) { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index f9b434adf8f..480d9b5ab89 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -60,6 +60,7 @@ import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetN import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; const $ = DOM.$; @@ -108,6 +109,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP readonly overflowWidgetsDomNode: HTMLElement; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IKeybindingService private readonly keybindingsService: IKeybindingService, @@ -121,7 +123,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP @IStorageService storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(KeybindingsEditor.ID, telemetryService, themeService, storageService); + super(KeybindingsEditor.ID, group, telemetryService, themeService, storageService); this.delayedFiltering = new Delayer(300); this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(!!this.keybindingFocusContextKey.get()))); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 33c9e99303b..3a5a03a9718 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -66,6 +66,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { CodeWindow } from 'vs/base/browser/window'; export const enum SettingsFocusContext { @@ -219,6 +220,7 @@ export class SettingsEditor2 extends EditorPane { private installedExtensionIds: string[] = []; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @@ -240,7 +242,7 @@ export class SettingsEditor2 extends EditorPane { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, ) { - super(SettingsEditor2.ID, telemetryService, themeService, storageService); + super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); this.localSearchDelayer = new Delayer(300); this.remoteSearchThrottle = new ThrottledDelayer(200); @@ -398,7 +400,7 @@ export class SettingsEditor2 extends EditorPane { } private restoreCachedState(): ISettingsEditor2State | null { - const cachedState = this.group && this.input && this.editorMemento.loadEditorState(this.group, this.input); + const cachedState = this.input && this.editorMemento.loadEditorState(this.group, this.input); if (cachedState && typeof cachedState.target === 'object') { cachedState.target = URI.revive(cachedState.target); } @@ -499,8 +501,8 @@ export class SettingsEditor2 extends EditorPane { } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (!visible) { // Wait for editor to be removed from DOM #106303 @@ -645,7 +647,7 @@ export class SettingsEditor2 extends EditorPane { })); if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) { - const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer)); + const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => { this.lastSyncedLabel = lastSyncedLabel; this.updateInputAriaLabel(); @@ -1426,7 +1428,7 @@ export class SettingsEditor2 extends EditorPane { // If the context view is focused, delay rendering settings if (this.contextViewFocused()) { - const element = DOM.getWindow(this.settingsTree.getHTMLElement()).document.querySelector('.context-view'); + const element = this.window.document.querySelector('.context-view'); if (element) { this.scheduleRefresh(element as HTMLElement, key); } @@ -1830,10 +1832,10 @@ export class SettingsEditor2 extends EditorPane { if (this.isVisible()) { const searchQuery = this.searchWidget.getValue().trim(); const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget; - if (this.group && this.input) { + if (this.input) { this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target }); } - } else if (this.group && this.input) { + } else if (this.input) { this.editorMemento.clearEditorState(this.input, this.group); } @@ -1849,6 +1851,7 @@ class SyncControls extends Disposable { public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event; constructor( + window: CodeWindow, container: HTMLElement, @ICommandService private readonly commandService: ICommandService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -1881,7 +1884,7 @@ class SyncControls extends Disposable { })); const updateLastSyncedTimer = this._register(new DOM.WindowIntervalTimer()); - updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, DOM.getWindow(container)); + updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, window); this.update(); this._register(this.userDataSyncService.onDidChangeStatus(() => { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index ffc69643037..84bb2199629 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -47,7 +47,7 @@ import { SearchModel, SearchResult } from 'vs/workbench/contrib/search/browser/s import { InSearchEditor, SearchEditorID, SearchEditorInputTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ITextQuery, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -97,6 +97,7 @@ export class SearchEditor extends AbstractTextCodeEditor private updatingModelForSearch: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -116,7 +117,7 @@ export class SearchEditor extends AbstractTextCodeEditor @IFileService fileService: IFileService, @ILogService private readonly logService: ILogService ) { - super(SearchEditor.ID, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); + super(SearchEditor.ID, group, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); this.container = DOM.$('.search-editor'); this.searchOperation = this._register(new LongRunningOperation(progressService)); @@ -668,7 +669,7 @@ export class SearchEditor extends AbstractTextCodeEditor } private getInput(): SearchEditorInput | undefined { - return this._input as SearchEditorInput; + return this.input as SearchEditorInput; } private priorConfig: Partial> | undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 6835b5e9c35..0ea8218fcce 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -47,6 +47,7 @@ export class TerminalEditor extends EditorPane { private _cancelContextMenu: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -61,7 +62,7 @@ export class TerminalEditor extends EditorPane { @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService ) { - super(terminalEditorId, telemetryService, themeService, storageService); + super(terminalEditorId, group, telemetryService, themeService, storageService); this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, contextKeyService)); this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalInstanceContext, contextKeyService)); } @@ -74,7 +75,7 @@ export class TerminalEditor extends EditorPane { if (this._lastDimension) { this.layout(this._lastDimension); } - this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); if (this._editorInput.terminalInstance) { // since the editor does not monitor focus changes, for ex. between the terminal // panel and the editors, this is needed so that the active instance gets set @@ -143,7 +144,7 @@ export class TerminalEditor extends EditorPane { // copyPaste: Shift+right click should open context menu if (rightClickBehavior === 'copyPaste' && event.shiftKey) { - openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); + openContextMenu(this.window, event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); return; } @@ -181,7 +182,7 @@ export class TerminalEditor extends EditorPane { else if (!this._cancelContextMenu && rightClickBehavior !== 'copyPaste' && rightClickBehavior !== 'paste') { if (!this._cancelContextMenu) { - openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); + openContextMenu(this.window, event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); } event.preventDefault(); event.stopImmediatePropagation(); @@ -199,9 +200,9 @@ export class TerminalEditor extends EditorPane { this._lastDimension = dimension; } - override setVisible(visible: boolean, group?: IEditorGroup): void { - super.setVisible(visible, group); - this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + override setVisible(visible: boolean): void { + super.setVisible(visible); + this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); } override getActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index f9b3f0c34f3..33ad2d64e7d 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts @@ -53,6 +53,7 @@ export class WebviewEditor extends EditorPane { private readonly _scopedContextKeyService = this._register(new MutableDisposable()); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -62,7 +63,7 @@ export class WebviewEditor extends EditorPane { @IHostService private readonly _hostService: IHostService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, ) { - super(WebviewEditor.ID, telemetryService, themeService, storageService); + super(WebviewEditor.ID, group, telemetryService, themeService, storageService); this._register(Event.any( _editorGroupsService.activePart.onDidScroll, @@ -122,7 +123,7 @@ export class WebviewEditor extends EditorPane { this.webview?.focus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { this._visible = visible; if (this.input instanceof WebviewInput && this.webview) { if (visible) { @@ -131,7 +132,7 @@ export class WebviewEditor extends EditorPane { this.webview.release(this); } } - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } public override clearInput() { @@ -161,9 +162,7 @@ export class WebviewEditor extends EditorPane { } if (input instanceof WebviewInput) { - if (this.group) { - input.updateGroup(this.group.id); - } + input.updateGroup(this.group.id); if (!alreadyOwnsWebview) { this.claimWebview(input); @@ -186,7 +185,7 @@ export class WebviewEditor extends EditorPane { // Webviews are not part of the normal editor dom, so we have to register our own drag and drop handler on them. this._webviewVisibleDisposables.add(this._editorGroupsService.createEditorDropTarget(input.webview.container, { - containsGroup: (group) => this.group?.id === group.id + containsGroup: (group) => this.group.id === group.id })); this._webviewVisibleDisposables.add(new WebviewWindowDragMonitor(() => this.webview)); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 2f4054f1e28..78a07e62ce2 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, Dimension, addDisposableListener, append, clearNode, getWindow, reset } from 'vs/base/browser/dom'; +import { $, Dimension, addDisposableListener, append, clearNode, reset } from 'vs/base/browser/dom'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -65,7 +65,7 @@ import { GettingStartedInput } from 'vs/workbench/contrib/welcomeGettingStarted/ import { IResolvedWalkthrough, IResolvedWalkthroughStep, IWalkthroughsService, hiddenEntriesConfigurationKey } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService'; import { RestoreWalkthroughsConfigurationValue, restoreWalkthroughsConfigurationKey } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; import { startEntries } from 'vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent'; -import { GroupDirection, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -162,6 +162,7 @@ export class GettingStartedPage extends EditorPane { private categoriesSlideDisposables: DisposableStore; constructor( + group: IEditorGroup, @ICommandService private readonly commandService: ICommandService, @IProductService private readonly productService: IProductService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -186,7 +187,7 @@ export class GettingStartedPage extends EditorPane { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService) { - super(GettingStartedPage.ID, telemetryService, themeService, storageService); + super(GettingStartedPage.ID, group, telemetryService, themeService, storageService); this.container = $('.gettingStartedContainer', { @@ -266,7 +267,7 @@ export class GettingStartedPage extends EditorPane { ourStep.done = step.done; if (category.id === this.currentWalkthrough?.id) { - const badgeelements = assertIsDefined(getWindow(this.container).document.querySelectorAll(`[data-done-step-id="${step.id}"]`)); + const badgeelements = assertIsDefined(this.window.document.querySelectorAll(`[data-done-step-id="${step.id}"]`)); badgeelements.forEach(badgeelement => { if (step.done) { badgeelement.setAttribute('aria-checked', 'true'); @@ -1117,7 +1118,7 @@ export class GettingStartedPage extends EditorPane { } private updateCategoryProgress() { - getWindow(this.container).document.querySelectorAll('.category-progress').forEach(element => { + this.window.document.querySelectorAll('.category-progress').forEach(element => { const categoryID = element.getAttribute('x-data-category-id'); const category = this.gettingStartedCategories.find(category => category.id === categoryID); if (!category) { throw Error('Could not find category with ID ' + categoryID); } @@ -1170,7 +1171,7 @@ export class GettingStartedPage extends EditorPane { } private focusSideEditorGroup() { - const fullSize = this.group ? this.groupsService.getPart(this.group).contentDimension : undefined; + const fullSize = this.groupsService.getPart(this.group).contentDimension; if (!fullSize || fullSize.width <= 700) { return; } if (this.groupsService.count === 1) { const sideGroup = this.groupsService.addGroup(this.groupsService.groups[0], GroupDirection.RIGHT); diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts index 50efd6cfc84..dd11b080f6b 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -32,8 +32,8 @@ import { UILabelProvider } from 'vs/base/common/keybindingLabels'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { deepClone } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { addDisposableListener, Dimension, getWindow, safeInnerHtml, size } from 'vs/base/browser/dom'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { addDisposableListener, Dimension, safeInnerHtml, size } from 'vs/base/browser/dom'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; @@ -66,6 +66,7 @@ export class WalkThroughPart extends EditorPane { private editorMemento: IEditorMemento; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @@ -79,7 +80,7 @@ export class WalkThroughPart extends EditorPane { @IExtensionService private readonly extensionService: IExtensionService, @IEditorGroupsService editorGroupService: IEditorGroupsService, ) { - super(WalkThroughPart.ID, telemetryService, themeService, storageService); + super(WalkThroughPart.ID, group, telemetryService, themeService, storageService); this.editorFocus = WALK_THROUGH_FOCUS.bindTo(this.contextKeyService); this.editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY); } @@ -156,7 +157,7 @@ export class WalkThroughPart extends EditorPane { this.content.addEventListener('click', event => { for (let node = event.target as HTMLElement; node; node = node.parentNode as HTMLElement) { if (node instanceof HTMLAnchorElement && node.href) { - const baseElement = node.ownerDocument.getElementsByTagName('base')[0] || getWindow(node).location; + const baseElement = node.ownerDocument.getElementsByTagName('base')[0] || this.window.location; if (baseElement && node.href.indexOf(baseElement.href) >= 0 && node.hash) { const scrollTarget = this.content.querySelector(node.hash); const innerContent = this.content.firstElementChild; @@ -441,22 +442,18 @@ export class WalkThroughPart extends EditorPane { private saveTextEditorViewState(input: WalkThroughInput): void { const scrollPosition = this.scrollbar.getScrollPosition(); - if (this.group) { - this.editorMemento.saveEditorState(this.group, input, { - viewState: { - scrollTop: scrollPosition.scrollTop, - scrollLeft: scrollPosition.scrollLeft - } - }); - } + this.editorMemento.saveEditorState(this.group, input, { + viewState: { + scrollTop: scrollPosition.scrollTop, + scrollLeft: scrollPosition.scrollLeft + } + }); } private loadTextEditorViewState(input: WalkThroughInput) { - if (this.group) { - const state = this.editorMemento.loadEditorState(this.group, input); - if (state) { - this.scrollbar.setScrollPosition(state.viewState); - } + const state = this.editorMemento.loadEditorState(this.group, input); + if (state) { + this.scrollbar.setScrollPosition(state.viewState); } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index fe7f625a784..51339f65077 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -59,6 +59,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { basename, dirname } from 'vs/base/common/resources'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const shieldIcon = registerIcon('workspace-trust-banner', Codicon.shield, localize('shieldIcon', 'Icon for workspace trust ion the banner.')); @@ -685,6 +686,7 @@ export class WorkspaceTrustEditor extends EditorPane { private workspaceTrustedUrisTable!: WorkspaceTrustedUrisTable; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -697,7 +699,7 @@ export class WorkspaceTrustEditor extends EditorPane { @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IProductService private readonly productService: IProductService, @IKeybindingService private readonly keybindingService: IKeybindingService, - ) { super(WorkspaceTrustEditor.ID, telemetryService, themeService, storageService); } + ) { super(WorkspaceTrustEditor.ID, group, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { this.rootElement = append(parent, $('.workspace-trust-editor', { tabindex: '0' })); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index a940bd1a309..7671e480d79 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -143,7 +143,7 @@ suite('EditorService', () => { assert.strictEqual(willInstantiateEditorPaneListenerCounter, 1); // Close input - await editor?.group?.closeEditor(input); + await editor?.group.closeEditor(input); assert.strictEqual(0, editorService.count); assert.strictEqual(0, editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length); @@ -1399,15 +1399,15 @@ suite('EditorService', () => { const rootPane = await openEditor(untypedEditor1); const sidePane = await openEditor(untypedEditor2, SIDE_GROUP); - assert.strictEqual(rootPane?.group?.count, 1); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 1); + assert.strictEqual(sidePane?.group.count, 1); accessor.editorGroupService.activateGroup(sidePane.group); await openEditor(untypedEditor1); - assert.strictEqual(rootPane?.group?.count, 1); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 1); + assert.strictEqual(sidePane?.group.count, 1); await resetTestState(); } @@ -1419,18 +1419,18 @@ suite('EditorService', () => { const rootPane = await openEditor(untypedEditor1); await openEditor(untypedEditor2); - assert.strictEqual(rootPane?.group?.activeEditor?.resource?.toString(), untypedEditor2.resource.toString()); + assert.strictEqual(rootPane?.group.activeEditor?.resource?.toString(), untypedEditor2.resource.toString()); const sidePane = await openEditor(untypedEditor2, SIDE_GROUP); - assert.strictEqual(rootPane?.group?.count, 2); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 2); + assert.strictEqual(sidePane?.group.count, 1); accessor.editorGroupService.activateGroup(sidePane.group); await openEditor(untypedEditor1); - assert.strictEqual(rootPane?.group?.count, 2); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 2); + assert.strictEqual(sidePane?.group.count, 1); await resetTestState(); } @@ -1458,7 +1458,7 @@ suite('EditorService', () => { assert.strictEqual(pane?.options?.sticky, true); assert.strictEqual(pane?.options?.preserveFocus, true); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); // Untyped editor (without registered editor) pane = await service.openEditor({ resource: URI.file('resource-openEditors') }); @@ -1499,7 +1499,7 @@ suite('EditorService', () => { assert.strictEqual(service.isOpened({ resource: input.resource, typeId: input.typeId, editorId: input.editorId }), true); assert.strictEqual(service.isOpened({ resource: otherInput.resource, typeId: otherInput.typeId, editorId: otherInput.editorId }), true); - await editor2?.group?.closeEditor(input); + await editor2?.group.closeEditor(input); assert.strictEqual(part.activeGroup.count, 1); assert.strictEqual(service.isOpened(input), false); @@ -1507,7 +1507,7 @@ suite('EditorService', () => { assert.strictEqual(service.isOpened({ resource: input.resource, typeId: input.typeId, editorId: input.editorId }), false); assert.strictEqual(service.isOpened({ resource: otherInput.resource, typeId: otherInput.typeId, editorId: otherInput.editorId }), true); - await editor1?.group?.closeEditor(sideBySideInput); + await editor1?.group.closeEditor(sideBySideInput); assert.strictEqual(service.isOpened(input), false); assert.strictEqual(service.isOpened(otherInput), false); @@ -2343,7 +2343,7 @@ suite('EditorService', () => { assert.strictEqual(accessor.fileService.watches.length, 1); assert.strictEqual(accessor.fileService.watches[0].toString(), input2.resource.toString()); - await editor?.group?.closeAllEditors(); + await editor?.group.closeAllEditors(); assert.strictEqual(accessor.fileService.watches.length, 0); }); @@ -2608,14 +2608,14 @@ suite('EditorService', () => { const found1 = service.findEditors(input.resource); assert.strictEqual(found1.length, 2); assert.strictEqual(found1[0].editor, input); - assert.strictEqual(found1[0].groupId, sideEditor?.group?.id); + assert.strictEqual(found1[0].groupId, sideEditor?.group.id); assert.strictEqual(found1[1].editor, input); assert.strictEqual(found1[1].groupId, rootGroup.id); const found2 = service.findEditors(input); assert.strictEqual(found2.length, 2); assert.strictEqual(found2[0].editor, input); - assert.strictEqual(found2[0].groupId, sideEditor?.group?.id); + assert.strictEqual(found2[0].groupId, sideEditor?.group.id); assert.strictEqual(found2[1].editor, input); assert.strictEqual(found2[1].groupId, rootGroup.id); } @@ -2642,7 +2642,7 @@ suite('EditorService', () => { // Check we don't find editors after closing them await rootGroup.closeAllEditors(); - await sideEditor?.group?.closeAllEditors(); + await sideEditor?.group.closeAllEditors(); { const found1 = service.findEditors(input.resource); assert.strictEqual(found1.length, 0); diff --git a/src/vs/workbench/services/history/browser/historyService.ts b/src/vs/workbench/services/history/browser/historyService.ts index efaf3103c6c..6b67a036cb5 100644 --- a/src/vs/workbench/services/history/browser/historyService.ts +++ b/src/vs/workbench/services/history/browser/historyService.ts @@ -182,7 +182,7 @@ export class HistoryService extends Disposable implements IHistoryService { } // Remember as last active editor (can be undefined if none opened) - this.lastActiveEditor = activeEditorPane?.input && activeEditorPane.group ? { editor: activeEditorPane.input, groupId: activeEditorPane.group.id } : undefined; + this.lastActiveEditor = activeEditorPane?.input ? { editor: activeEditorPane.input, groupId: activeEditorPane.group.id } : undefined; // Dispose old listeners this.activeEditorListeners.clear(); @@ -1522,7 +1522,7 @@ ${entryLabels.join('\n')} this.trace('notifyNavigation()', editorPane?.input, event); const isSelectionAwareEditorPane = isEditorPaneWithSelection(editorPane); - const hasValidEditor = editorPane?.group && editorPane.input && !editorPane.input.isDisposed(); + const hasValidEditor = editorPane?.input && !editorPane.input.isDisposed(); // Treat editor changes that happen as part of stack navigation specially // we do not want to add a new stack entry as a matter of navigating the @@ -1893,7 +1893,7 @@ ${entryLabels.join('\n')} return false; // we need an active editor pane with selection support } - if (pane.group?.id !== this.current.groupId) { + if (pane.group.id !== this.current.groupId) { return false; // we need matching groups } diff --git a/src/vs/workbench/services/history/test/browser/historyService.test.ts b/src/vs/workbench/services/history/test/browser/historyService.test.ts index 28f269d3bee..82dfcd4434d 100644 --- a/src/vs/workbench/services/history/test/browser/historyService.test.ts +++ b/src/vs/workbench/services/history/test/browser/historyService.test.ts @@ -109,28 +109,28 @@ suite('HistoryService', function () { // [index.txt] | [>index.txt<] [other.html] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goBack(); // [>index.txt<] | [index.txt] [other.html] - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goForward(); // [index.txt] | [>index.txt<] [other.html] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goForward(); // [index.txt] | [index.txt] [>other.html<] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), otherResource.toString()); return workbenchTeardown(instantiationService); @@ -313,7 +313,7 @@ suite('HistoryService', function () { // [one.txt] [>two.html<] | const editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); - pane1?.group?.moveEditor(pane1.input!, sideGroup); + pane1?.group.moveEditor(pane1.input!, sideGroup); await editorChangePromise; // [one.txt] | [>two.html<] @@ -322,7 +322,7 @@ suite('HistoryService', function () { // [>one.txt<] | [two.html] - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); return workbenchTeardown(instantiationService); @@ -341,7 +341,7 @@ suite('HistoryService', function () { assert.notStrictEqual(pane1, pane2); - await pane1?.group?.closeAllEditors(); + await pane1?.group.closeAllEditors(); // [>two.html<] @@ -349,7 +349,7 @@ suite('HistoryService', function () { // [>two.html<] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource2.toString()); return workbenchTeardown(instantiationService); @@ -545,7 +545,7 @@ suite('HistoryService', function () { await historyService.goBack(); await historyService.goBack(); - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); // [one.txt] [two.html] [>three.html<] | [>one.txt<] [two.html] [three.html] @@ -556,7 +556,7 @@ suite('HistoryService', function () { await historyService.goBack(); await historyService.goBack(); - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); return workbenchTeardown(instantiationService); @@ -631,7 +631,7 @@ suite('HistoryService', function () { const resource = toResource.call(this, '/path/index.txt'); const pane = await editorService.openEditor({ resource }); - await pane?.group?.closeAllEditors(); + await pane?.group.closeAllEditors(); const onDidActiveEditorChange = new DeferredPromise(); disposables.add(editorService.onDidActiveEditorChange(e => { diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 142d49dadc1..3d817b86fea 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -23,7 +23,7 @@ import { TestStorageService, TestWorkspaceTrustManagementService } from 'vs/work import { extUri } from 'vs/base/common/resources'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -36,9 +36,9 @@ const editorInputRegistry: IEditorFactoryRegistry = Registry.as(EditorExtensions class TestEditor extends EditorPane { - constructor() { + constructor(group: IEditorGroup,) { const disposables = new DisposableStore(); - super('TestEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + super('TestEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); this._register(disposables); } @@ -50,9 +50,9 @@ class TestEditor extends EditorPane { class OtherTestEditor extends EditorPane { - constructor() { + constructor(group: IEditorGroup,) { const disposables = new DisposableStore(); - super('testOtherEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + super('testOtherEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); this._register(disposables); } @@ -118,7 +118,9 @@ suite('EditorPane', () => { }); test('EditorPane API', async () => { - const editor = new TestEditor(); + const group = new TestEditorGroupView(1); + const editor = new TestEditor(group); + assert.ok(editor.group); const input = disposables.add(new OtherTestInput()); const options = {}; @@ -127,13 +129,11 @@ suite('EditorPane', () => { await editor.setInput(input, options, Object.create(null), CancellationToken.None); assert.strictEqual(input, editor.input); - const group = new TestEditorGroupView(1); - editor.setVisible(true, group); + editor.setVisible(true); assert(editor.isVisible()); - assert.strictEqual(editor.group, group); editor.dispose(); editor.clearInput(); - editor.setVisible(false, group); + editor.setVisible(false); assert(!editor.isVisible()); assert(!editor.input); assert(!editor.getControl()); @@ -174,18 +174,22 @@ suite('EditorPane', () => { const inst = workbenchInstantiationService(undefined, disposables); - const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const group = new TestEditorGroupView(1); + + const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual(editor.getId(), 'testEditor'); - const otherEditor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const otherEditor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual(otherEditor.getId(), 'workbench.editors.textResourceEditor'); }); test('Editor Pane Lookup favors specific class over superclass (match on super class)', function () { const inst = workbenchInstantiationService(undefined, disposables); + const group = new TestEditorGroupView(1); + disposables.add(registerTestResourceEditor()); - const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual('workbench.editors.textResourceEditor', editor.getId()); }); @@ -453,8 +457,8 @@ suite('EditorPane', () => { test('WorkspaceTrustRequiredEditor', async function () { class TrustRequiredTestEditor extends EditorPane { - constructor(@ITelemetryService telemetryService: ITelemetryService) { - super('TestEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + constructor(group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService) { + super('TestEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); } override getId(): string { return 'trustRequiredTestEditor'; } diff --git a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts index c8bcb2d24a1..60ab56b329a 100644 --- a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts @@ -74,7 +74,7 @@ suite('TextEditorPane', () => { pane.setSelection(new Selection(1, 1, 1, 1), EditorPaneSelectionChangeReason.USER); const selection = pane.getSelection(); assert.ok(selection); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); const options = selection.restore({}); pane = (await accessor.editorService.openEditor({ resource, options }) as TestTextFileEditor); @@ -85,7 +85,7 @@ suite('TextEditorPane', () => { assert.strictEqual(newSelection.compare(selection), EditorPaneSelectionCompareResult.IDENTICAL); await model.revert(); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); }); test('TextEditorPaneSelection', function () { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 4a05917faec..9cb63bb2b63 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1563,8 +1563,8 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor Date: Fri, 1 Mar 2024 09:32:36 -0800 Subject: [PATCH 158/228] debug: clear prompt if a terminal is reused betwen debug sessions (#206632) Fixes #106743 --- src/vs/workbench/api/node/extHostDebugService.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index f38194be406..995d4f8a17b 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -91,7 +91,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { const terminalName = args.title || nls.localize('debug.terminal.title', "Debug Process"); const termKey = createKeyForShell(shell, shellArgs, args); - let terminal = await this._integratedTerminalInstances.checkout(termKey, terminalName); + let terminal = await this._integratedTerminalInstances.checkout(termKey, terminalName, true); let cwdForPrepareCommand: string | undefined; let giveShellTimeToInitialize = false; @@ -127,6 +127,10 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { // give a new terminal some time to initialize the shell await new Promise(resolve => setTimeout(resolve, 1000)); } else { + if (terminal.state.isInteractedWith) { + terminal.sendText('\u0003'); // Ctrl+C for #106743. Not part of the same command for #107969 + } + if (configProvider.getConfiguration('debug.terminal').get('clearBeforeReusing')) { // clear terminal before reusing it if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0 || shell.indexOf('cmd.exe') >= 0) { @@ -195,7 +199,7 @@ class DebugTerminalCollection { private _terminalInstances = new Map(); - public async checkout(config: string, name: string) { + public async checkout(config: string, name: string, cleanupOthersByName = false) { const entries = [...this._terminalInstances.entries()]; const promises = entries.map(([terminal, termInfo]) => createCancelablePromise(async ct => { @@ -215,6 +219,9 @@ class DebugTerminalCollection { } if (termInfo.config !== config) { + if (cleanupOthersByName) { + terminal.dispose(); + } return null; } From 0e959d149fdc275e1f118280008343b857d2426f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 1 Mar 2024 10:56:03 -0800 Subject: [PATCH 159/228] debug: fix selected session not expanded if call stack not visible (#206635) Fixes #202683 --- src/vs/workbench/contrib/debug/browser/callStackView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index b9d319bad85..db948476aef 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -348,6 +348,7 @@ export class CallStackView extends ViewPane { } if (!this.isBodyVisible()) { this.needsRefresh = true; + this.selectionNeedsUpdate = true; return; } if (this.onCallStackChangeScheduler.isScheduled()) { From d75907639453ab82e49a2c40a464fcfe628d4971 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 1 Mar 2024 10:59:39 -0800 Subject: [PATCH 160/228] pipe through the rest of the properties (#206636) --- src/vs/workbench/api/common/extHost.protocol.ts | 2 ++ src/vs/workbench/api/common/extHostNotebookKernels.ts | 2 ++ .../browser/contrib/notebookVariables/notebookVariablesView.ts | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b67fd890e78..d7e5580443b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1126,6 +1126,8 @@ export interface VariablesResult { name: string; value: string; type?: string; + language?: string; + expression?: string; hasNamedChildren: boolean; indexedChildrenCount: number; extensionId: string; diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index f2a201e9e06..998e624a195 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -475,6 +475,8 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { name: result.variable.name, value: result.variable.value, type: result.variable.type, + language: result.variable.language, + expression: result.variable.expression, hasNamedChildren: result.hasNamedChildren, indexedChildrenCount: result.indexedChildrenCount, extensionId: obj.extensionId.value, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts index 1c3c61d25ea..bab9b8086c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts @@ -34,7 +34,7 @@ import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebook import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -export type contextMenuArg = { source?: string; type?: string; value?: string; expression?: string; language?: string; extensionId?: string }; +export type contextMenuArg = { source: string; name: string; type?: string; value?: string; expression?: string; language?: string; extensionId?: string }; export class NotebookVariablesView extends ViewPane { @@ -109,6 +109,7 @@ export class NotebookVariablesView extends ViewPane { const arg: contextMenuArg = { source: element.notebook.uri.toString(), + name: element.name, value: element.value, type: element.type, expression: element.expression, From 313c78eeadbce621c3a5e890b4d1760bb21d7a46 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:26:12 -0800 Subject: [PATCH 161/228] fix: added repo name in issue reporter (#206638) fix repo name in issue reporter --- .../issue/issueReporterService.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 851f5c38798..56623913d7d 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -78,6 +78,10 @@ export class IssueReporter extends Disposable { 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(); } @@ -502,13 +506,16 @@ export class IssueReporter extends Disposable { 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 extensionLink = document.createElement('a'); - extensionLink.href = URI.revive(selectedExtension.uri).toString(); - extensionLink.textContent = selectedExtension.id; - const issueReporterElement = this.getElementById('issue-reporter')!; - Object.assign(extensionLink.style, { + 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', @@ -517,7 +524,11 @@ export class IssueReporter extends Disposable { textDecoration: 'none', width: 'auto' }); - issueReporterElement.appendChild(extensionLink); + show(issueRepoName); + } else { + // clear styles + issueRepoName.removeAttribute('style'); + hide(issueRepoName); } } From eb4ebc2247669a72ec9b5ffaf4847283b2283901 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 1 Mar 2024 13:39:48 -0800 Subject: [PATCH 162/228] prevent outer scroll if inner region recently scrolled (#206642) * prevent outer scroll if inner region recently scrolled * remove NodeJS reference --- .../browser/view/renderers/webviewPreloads.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index c08eb70c296..59beaaf2ef9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -455,15 +455,30 @@ async function webviewPreloads(ctx: PreloadContext) { } }; - function scrollWillGoToParent(event: WheelEvent) { + let scrollTimeout: any /* NodeJS.Timeout */ | undefined; + let scrolledElement: Element | undefined; + function flagRecentlyScrolled(node: Element) { + scrolledElement = node; + node.setAttribute('recentlyScrolled', 'true'); + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 300); + } + + function eventTargetShouldHandleScroll(event: WheelEvent) { for (let node = event.target as Node | null; node; node = node.parentNode) { if (!(node instanceof Element) || node.id === 'container' || node.classList.contains('cell_container') || node.classList.contains('markup') || node.classList.contains('output_container')) { return false; } + if (node.hasAttribute('recentlyScrolled') && scrolledElement === node) { + flagRecentlyScrolled(node); + return true; + } + // scroll up if (event.deltaY < 0 && node.scrollTop > 0) { // there is still some content to scroll + flagRecentlyScrolled(node); return true; } @@ -481,6 +496,7 @@ async function webviewPreloads(ctx: PreloadContext) { continue; } + flagRecentlyScrolled(node); return true; } } @@ -489,7 +505,7 @@ async function webviewPreloads(ctx: PreloadContext) { } const handleWheel = (event: WheelEvent & { wheelDeltaX?: number; wheelDeltaY?: number; wheelDelta?: number }) => { - if (event.defaultPrevented || scrollWillGoToParent(event)) { + if (event.defaultPrevented || eventTargetShouldHandleScroll(event)) { return; } postNotebookMessage('did-scroll-wheel', { From df809f53dd50e003a0dfdab295ea6091a0235795 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 1 Mar 2024 14:40:46 -0800 Subject: [PATCH 163/228] cli: use a better permission for keychain fallback (#206650) Fixes https://github.com/microsoft/vscode-remote-release/issues/9619 --- cli/src/auth.rs | 5 ++++- cli/src/state.rs | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/cli/src/auth.rs b/cli/src/auth.rs index ee7117330be..2ee4f73c919 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -404,7 +404,10 @@ impl Auth { let mut keyring_storage = KeyringStorage::default(); #[cfg(target_os = "linux")] let mut keyring_storage = ThreadKeyringStorage::default(); - let mut file_storage = FileStorage(PersistedState::new(self.file_storage_path.clone())); + let mut file_storage = FileStorage(PersistedState::new_with_mode( + self.file_storage_path.clone(), + 0o600, + )); let native_storage_result = if std::env::var("VSCODE_CLI_USE_FILE_KEYCHAIN").is_ok() || self.file_storage_path.exists() diff --git a/cli/src/state.rs b/cli/src/state.rs index 8815e2df40c..534c1556763 100644 --- a/cli/src/state.rs +++ b/cli/src/state.rs @@ -6,7 +6,8 @@ extern crate dirs; use std::{ - fs::{create_dir_all, read_to_string, remove_dir_all, write}, + fs::{self, create_dir_all, read_to_string, remove_dir_all}, + io::Write, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; @@ -34,6 +35,8 @@ where { path: PathBuf, state: Option, + #[allow(dead_code)] + mode: u32, } impl PersistedStateContainer @@ -58,13 +61,28 @@ where fn save(&mut self, state: T) -> Result<(), WrappedError> { let s = serde_json::to_string(&state).unwrap(); self.state = Some(state); - write(&self.path, s).map_err(|e| { + self.write_state(s).map_err(|e| { wrap( e, format!("error saving launcher state into {}", self.path.display()), ) }) } + + fn write_state(&mut self, s: String) -> std::io::Result<()> { + #[cfg(not(windows))] + use std::os::unix::fs::OpenOptionsExt; + + let mut f = fs::OpenOptions::new(); + f.create(true); + f.write(true); + f.truncate(true); + #[cfg(not(windows))] + f.mode(self.mode); + + let mut f = f.open(&self.path)?; + f.write_all(s.as_bytes()) + } } /// Container that holds some state value that is persisted to disk. @@ -82,8 +100,17 @@ where { /// Creates a new state container that persists to the given path. pub fn new(path: PathBuf) -> PersistedState { + Self::new_with_mode(path, 0o644) + } + + /// Creates a new state container that persists to the given path. + pub fn new_with_mode(path: PathBuf, mode: u32) -> PersistedState { PersistedState { - container: Arc::new(Mutex::new(PersistedStateContainer { path, state: None })), + container: Arc::new(Mutex::new(PersistedStateContainer { + path, + state: None, + mode, + })), } } @@ -217,5 +244,4 @@ impl LauncherPaths { pub fn web_server_storage(&self) -> PathBuf { self.root.join("serve-web") } - } From 2ea3428bf70a60bbd5e1b49e98888a53143092e3 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 1 Mar 2024 16:18:29 -0800 Subject: [PATCH 164/228] Bump distro (#206652) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03245f8347a..04e74dfabb8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "a5b6daf94540aab9d17335c2c2533e629d750123", + "distro": "c628288b553c076290c08f74482e6b71c337e4a8", "author": { "name": "Microsoft Corporation" }, From 1f94e5cd54ce0a7bc503a3f95a3742ddc5980151 Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Fri, 1 Mar 2024 18:23:57 -0600 Subject: [PATCH 165/228] replace CancellationTokenSource().token (#206651) * replace CancellationTokenSource().token * remove unused import --- .../services/search/test/node/textSearchManager.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts index f4203933951..693c4e9f0c0 100644 --- a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts +++ b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Progress } from 'vs/platform/progress/common/progress'; @@ -35,7 +35,7 @@ suite('NativeTextSearchManager', () => { }; const m = new NativeTextSearchManager(query, provider); - await m.search(() => { }, new CancellationTokenSource().token); + await m.search(() => { }, CancellationToken.None); assert.ok(correctEncoding); }); From f838112f066283bcce562b56766b3816112ee77c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:49:38 +0100 Subject: [PATCH 166/228] Relayout on tab bar change (#206669) relayout on tab bar change --- .../parts/editor/multiRowEditorTabsControl.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 1a6b5ca985e..24d85415eb8 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -36,19 +36,23 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, stickyModel)); this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, unstickyModel)); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } - private handlePinnedTabsSeparateRowToolbars(): void { + private handlePinnedTabsLayoutChange(): void { if (this.groupView.count === 0) { // Do nothing as no tab bar is visible return; } + + const hadTwoTabBars = this.parent.classList.contains('two-tab-bars'); + const hasTwoTabBars = this.groupView.count !== this.groupView.stickyCount && this.groupView.stickyCount > 0; + // Ensure action toolbar is only visible once - if (this.groupView.count === this.groupView.stickyCount) { - this.parent.classList.toggle('two-tab-bars', false); - } else { - this.parent.classList.toggle('two-tab-bars', true); + this.parent.classList.toggle('two-tab-bars', hasTwoTabBars); + + if (hadTwoTabBars !== hasTwoTabBars) { + this.groupView.relayout(); } } @@ -85,7 +89,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleOpenedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } beforeCloseEditor(editor: EditorInput): void { @@ -111,7 +115,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleClosedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number, stickyStateChange: boolean): void { @@ -125,7 +129,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.openEditor(editor); } - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } else { if (this.model.isSticky(editor)) { @@ -144,14 +148,14 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.closeEditor(editor); this.stickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } unstickEditor(editor: EditorInput): void { this.stickyEditorTabsControl.closeEditor(editor); this.unstickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } setActive(isActive: boolean): void { From b56aee50fe2dd709e68161559e9af97c7d1e8fa3 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Sat, 2 Mar 2024 13:21:43 -0800 Subject: [PATCH 167/228] Support buttons in separators in Quick Access (#206701) * WIP buttons on separators working * revert changes to textSearchQuickAccess * better check --- .../quickinput/browser/pickerQuickAccess.ts | 91 ++++++++++++------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index acde1e461d1..86160c9e26a 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -6,7 +6,7 @@ import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { isFunction } from 'vs/base/common/types'; @@ -59,6 +59,22 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; } +export interface IPickerQuickAccessSeparator extends IQuickPickSeparator { + /** + * A method that will be executed when a button of the pick item was + * clicked on. + * + * @param buttonIndex index of the button of the item that + * was clicked. + * + * @param the state of modifier keys when the button was triggered. + * + * @returns a value that indicates what should happen after the trigger + * which can be a `Promise` for long running operations. + */ + trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; +} + export interface IPickerQuickAccessProviderOptions { /** @@ -320,47 +336,52 @@ export abstract class PickerQuickAccessProvider { - if (typeof item.trigger === 'function') { - const buttonIndex = item.buttons?.indexOf(button) ?? -1; - if (buttonIndex >= 0) { - const result = item.trigger(buttonIndex, picker.keyMods); - const action = (typeof result === 'number') ? result : await result; + const buttonTrigger = async (button: IQuickInputButton, item: T | IPickerQuickAccessSeparator) => { + if (typeof item.trigger !== 'function') { + return; + } - if (token.isCancellationRequested) { - return; - } + const buttonIndex = item.buttons?.indexOf(button) ?? -1; + if (buttonIndex >= 0) { + const result = item.trigger(buttonIndex, picker.keyMods); + const action = (typeof result === 'number') ? result : await result; - switch (action) { - case TriggerAction.NO_ACTION: - break; - case TriggerAction.CLOSE_PICKER: - picker.hide(); - break; - case TriggerAction.REFRESH_PICKER: - updatePickerItems(); - break; - case TriggerAction.REMOVE_ITEM: { - const index = picker.items.indexOf(item); - if (index !== -1) { - const items = picker.items.slice(); - const removed = items.splice(index, 1); - const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); - const keepScrollPositionBefore = picker.keepScrollPosition; - picker.keepScrollPosition = true; - picker.items = items; - if (activeItems) { - picker.activeItems = activeItems; - } - picker.keepScrollPosition = keepScrollPositionBefore; + if (token.isCancellationRequested) { + return; + } + + switch (action) { + case TriggerAction.NO_ACTION: + break; + case TriggerAction.CLOSE_PICKER: + picker.hide(); + break; + case TriggerAction.REFRESH_PICKER: + updatePickerItems(); + break; + case TriggerAction.REMOVE_ITEM: { + const index = picker.items.indexOf(item); + if (index !== -1) { + const items = picker.items.slice(); + const removed = items.splice(index, 1); + const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); + const keepScrollPositionBefore = picker.keepScrollPosition; + picker.keepScrollPosition = true; + picker.items = items; + if (activeItems) { + picker.activeItems = activeItems; } - break; + picker.keepScrollPosition = keepScrollPositionBefore; } + break; } } } - })); + }; + + // Trigger the pick with button index if button triggered + disposables.add(picker.onDidTriggerItemButton(({ button, item }) => buttonTrigger(button, item))); + disposables.add(picker.onDidTriggerSeparatorButton(({ button, separator }) => buttonTrigger(button, separator))); return disposables; } From bfefd56dbe16b2880ab8d42803687deb40f9c262 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 3 Mar 2024 10:04:21 +0100 Subject: [PATCH 168/228] Revealing a view causes VS Code to steal focus (fix #205766) (#206712) --- src/vs/base/browser/dom.ts | 17 +++++++++++++++-- src/vs/workbench/browser/window.ts | 14 +++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 82459f97178..62b509ccda2 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,11 +921,24 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -export function focusWindow(element: Node): void { +export function focusWindow(element: Node, options?: { force: boolean }): void { const window = getWindow(element); - if (!window.document.hasFocus()) { + + // Force: always focus the element window + if (options?.force) { window.focus(); } + + // Not forced: only focus the element window if another + // window in the same workspace group has focus (when auxiliary + // windows are opened). + // This prevents stealing focus from another workspace window. + else { + const activeWindow = getActiveWindow(); + if (activeWindow.document.hasFocus() && activeWindow !== window) { + window.focus(); + } + } } const globalStylesheets = new Map>(); diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 17354e5f403..69a5fe4dee2 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; +import { addDisposableListener, EventHelper, EventType, focusWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; @@ -56,16 +56,8 @@ export abstract class BaseWindow extends Disposable { targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { - // If the active focused window is not the same as the - // window of the element to focus, make sure to focus - // that window first before focusing the element. - const activeWindow = getActiveWindow(); - if (activeWindow.document.hasFocus()) { - const elementWindow = getWindow(this); - if (activeWindow !== elementWindow) { - elementWindow.focus(); - } - } + // Ensure elements window is focused + focusWindow(this); // Pass to original focus() method originalFocus.apply(this, [options]); From 161d5010003b829d0d54b1c2a4c502421cc09461 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 3 Mar 2024 16:26:34 +0100 Subject: [PATCH 169/228] check for extension updates while checking for VS Code update (#206704) * check for extension updates while checking for VS Code update * fix tests --- .../extensions/browser/extensionsWorkbenchService.ts | 7 +++++++ .../extensionRecommendationsService.test.ts | 2 ++ .../test/electron-sandbox/extensionsActions.test.ts | 2 ++ .../test/electron-sandbox/extensionsViews.test.ts | 2 ++ .../electron-sandbox/extensionsWorkbenchService.test.ts | 2 ++ 5 files changed, 15 insertions(+) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index c92d36d3ecf..12460b0406d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -53,6 +53,7 @@ import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecyc import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { mainWindow } from 'vs/base/browser/window'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; interface IExtensionStateProvider { (extension: Extension): T; @@ -783,6 +784,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IStorageService private readonly storageService: IStorageService, @IDialogService private readonly dialogService: IDialogService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, + @IUpdateService private readonly updateService: IUpdateService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -858,6 +860,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); + this._register(this.updateService.onStateChange(e => { + if ((e.type === StateType.AvailableForDownload || e.type === StateType.Downloading) && this.isAutoUpdateEnabled()) { + this.checkForUpdates(); + } + })); // Update AutoUpdate Contexts this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 1e9e11baa19..0db4f047c60 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -63,6 +63,7 @@ import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; +import { IUpdateService } from 'vs/platform/update/common/update'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -274,6 +275,7 @@ suite('ExtensionRecommendationsService Test', () => { }, }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 0762678249d..ea44eb82a58 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -56,6 +56,7 @@ import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/envi import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IUpdateService } from 'vs/platform/update/common/update'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -136,6 +137,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); + instantiationService.stub(IUpdateService, { onStateChange: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 4a06d92c0e8..a349dbbf69c 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -48,6 +48,7 @@ import { arch } from 'vs/base/common/process'; import { IProductService } from 'vs/platform/product/common/productService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IUpdateService } from 'vs/platform/update/common/update'; suite('ExtensionsViews Tests', () => { @@ -187,6 +188,7 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledTheme], EnablementState.DisabledGlobally); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); + instantiationService.stub(IUpdateService, { onStateChange: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 39d5869cd3c..7d56ee6d115 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -51,6 +51,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Mutable } from 'vs/base/common/types'; +import { IUpdateService } from 'vs/platform/update/common/update'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -131,6 +132,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', []); instantiationService.stubPromise(INotificationService, 'prompt', 0); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); + instantiationService.stub(IUpdateService, { onStateChange: Event.None }); }); test('test gallery extension', async () => { From c0fddb67b9b84dca476e6fd685bdfd4a85a13639 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 3 Mar 2024 23:35:38 +0100 Subject: [PATCH 170/228] window - try to improve focus handling (#206721) --- src/vs/base/browser/dom.ts | 20 -------- src/vs/workbench/browser/composite.ts | 10 +--- src/vs/workbench/browser/layout.ts | 5 +- .../browser/parts/editor/editorGroupView.ts | 5 +- .../workbench/browser/parts/views/viewPane.ts | 4 +- src/vs/workbench/browser/window.ts | 51 +++++++++++++++---- src/vs/workbench/electron-sandbox/window.ts | 41 ++++----------- .../browser/auxiliaryWindowService.ts | 29 +++++------ .../auxiliaryWindowService.ts | 36 +++---------- src/vs/workbench/test/browser/window.test.ts | 5 +- 10 files changed, 81 insertions(+), 125 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 62b509ccda2..ff113c9baa9 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,26 +921,6 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -export function focusWindow(element: Node, options?: { force: boolean }): void { - const window = getWindow(element); - - // Force: always focus the element window - if (options?.force) { - window.focus(); - } - - // Not forced: only focus the element window if another - // window in the same workspace group has focus (when auxiliary - // windows are opened). - // This prevents stealing focus from another workspace window. - else { - const activeWindow = getActiveWindow(); - if (activeWindow.document.hasFocus() && activeWindow !== window) { - window.focus(); - } - } -} - const globalStylesheets = new Map>(); export function isGlobalStylesheet(node: Node): boolean { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 59eba8e11ff..424fcfa19e5 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -10,7 +10,7 @@ import { IComposite, ICompositeControl } from 'vs/workbench/common/composite'; import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConstructorSignature, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { trackFocus, Dimension, IDomPosition, focusWindow } from 'vs/base/browser/dom'; +import { trackFocus, Dimension, IDomPosition } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Disposable } from 'vs/base/common/lifecycle'; import { assertIsDefined } from 'vs/base/common/types'; @@ -149,13 +149,7 @@ export abstract class Composite extends Component implements IComposite { * Called when this composite should receive keyboard focus. */ focus(): void { - const container = this.getContainer(); - if (container) { - // Make sure to focus the window of the container - // because it is possible that the composite is - // opened in a auxiliary window that is not focused. - focusWindow(container); - } + // Subclasses can implement } /** diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 797a1713dd8..b71d9dd9477 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; @@ -1124,9 +1124,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi focusPart(part: SINGLE_WINDOW_PARTS): void; focusPart(part: Parts, targetWindow: Window = mainWindow): void { const container = this.getContainer(targetWindow, part) ?? this.mainContainer; - if (container) { - focusWindow(container); - } switch (part) { case Parts.EDITOR_PART: diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index faddd4e7b06..69bfb90f5d4 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -11,7 +11,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, focusWindow, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, getWindow, getActiveElement } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -976,9 +976,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { focus(): void { - // Ensure window focus - focusWindow(this.element); - // Pass focus to editor panes if (this.activeEditorPane) { this.activeEditorPane.focus(); diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 84e11baf381..6ef0b18cdb1 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { asCssVariable, foreground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault, focusWindow } from 'vs/base/browser/dom'; +import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault } from 'vs/base/browser/dom'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -630,8 +630,6 @@ export abstract class ViewPane extends Pane implements IView { } focus(): void { - focusWindow(this.element); - if (this.viewWelcomeController.enabled) { this.viewWelcomeController.focus(); } else if (this.element) { diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 69a5fe4dee2..9f98ac1c680 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, EventHelper, EventType, focusWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; +import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { matchesScheme, Schemas } from 'vs/base/common/network'; -import { isIOS, isMacintosh } from 'vs/base/common/platform'; +import { isIOS, isMacintosh, isNative } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -30,6 +30,7 @@ import { registerWindowDriver } from 'vs/workbench/services/driver/browser/drive import { CodeWindow, isAuxiliaryWindow, mainWindow } from 'vs/base/browser/window'; import { createSingleCallFunction } from 'vs/base/common/functional'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export abstract class BaseWindow extends Disposable { @@ -39,11 +40,16 @@ export abstract class BaseWindow extends Disposable { constructor( targetWindow: CodeWindow, dom = { getWindowsCount, getWindows }, /* for testing */ - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService ) { super(); + if (isNative) { + this.enableNativeWindowFocus(targetWindow); + } this.enableWindowFocusOnElementFocus(targetWindow); + this.enableMultiWindowAwareTimeout(targetWindow, dom); this.registerFullScreenListeners(targetWindow.vscodeWindowId); @@ -51,13 +57,40 @@ export abstract class BaseWindow extends Disposable { //#region focus handling in multi-window applications + protected enableNativeWindowFocus(targetWindow: CodeWindow): void { + const originalWindowFocus = targetWindow.focus.bind(targetWindow); + + const that = this; + targetWindow.focus = function () { + originalWindowFocus(); + + if ( + !that.environmentService.extensionTestsLocationURI && // never steal focus when running tests + !targetWindow.document.hasFocus() // skip when already having focus + ) { + // Enable `window.focus()` to work in Electron by + // asking the main process to focus the window. + // https://github.com/electron/electron/issues/25578 + that.hostService.focus(targetWindow); + } + }; + } + protected enableWindowFocusOnElementFocus(targetWindow: CodeWindow): void { const originalFocus = HTMLElement.prototype.focus; targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { - // Ensure elements window is focused - focusWindow(this); + // If the active focused window is not the same as the + // window of the element to focus, make sure to focus + // that window first before focusing the element. + const activeWindow = getActiveWindow(); + if (activeWindow.document.hasFocus()) { + const elementWindow = getWindow(this); + if (activeWindow !== elementWindow) { + elementWindow.focus(); + } + } // Pass to original focus() method originalFocus.apply(this, [options]); @@ -178,12 +211,12 @@ export class BrowserWindow extends BaseWindow { @IDialogService private readonly dialogService: IDialogService, @ILabelService private readonly labelService: ILabelService, @IProductService private readonly productService: IProductService, - @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, + @IBrowserWorkbenchEnvironmentService private readonly browserEnvironmentService: IBrowserWorkbenchEnvironmentService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IHostService hostService: IHostService ) { - super(mainWindow, undefined, hostService); + super(mainWindow, undefined, hostService, browserEnvironmentService); this.registerListeners(); this.create(); @@ -280,8 +313,8 @@ export class BrowserWindow extends BaseWindow { this.openerService.setDefaultExternalOpener({ openExternal: async (href: string) => { let isAllowedOpener = false; - if (this.environmentService.options?.openerAllowedExternalUrlPrefixes) { - for (const trustedPopupPrefix of this.environmentService.options.openerAllowedExternalUrlPrefixes) { + if (this.browserEnvironmentService.options?.openerAllowedExternalUrlPrefixes) { + for (const trustedPopupPrefix of this.browserEnvironmentService.options.openerAllowedExternalUrlPrefixes) { if (href.startsWith(trustedPopupPrefix)) { isAllowedOpener = true; break; diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index f207e15834f..eb2159272be 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { equals } from 'vs/base/common/objects'; -import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindow, getWindowById, getWindowId, getWindows } from 'vs/base/browser/dom'; +import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindow, getWindowById, getWindows } from 'vs/base/browser/dom'; import { Action, Separator, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput, IEditorPane, isResourceEditorInput, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; @@ -107,7 +107,7 @@ export class NativeWindow extends BaseWindow { @IMenuService private readonly menuService: IMenuService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IIntegrityService private readonly integrityService: IIntegrityService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @INativeWorkbenchEnvironmentService private readonly nativeEnvironmentService: INativeWorkbenchEnvironmentService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IOpenerService private readonly openerService: IOpenerService, @@ -131,7 +131,7 @@ export class NativeWindow extends BaseWindow { @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService, @IHostService hostService: IHostService ) { - super(mainWindow, undefined, hostService); + super(mainWindow, undefined, hostService, nativeEnvironmentService); this.mainPartEditorService = editorService.createScoped('main', this._store); @@ -353,7 +353,7 @@ export class NativeWindow extends BaseWindow { this._register(Event.debounce(this.editorService.onDidVisibleEditorsChange, () => undefined, 0, undefined, undefined, undefined, this._store)(() => this.maybeCloseWindow())); // Listen to editor closing (if we run with --wait) - const filesToWait = this.environmentService.filesToWait; + const filesToWait = this.nativeEnvironmentService.filesToWait; if (filesToWait) { this.trackClosedWaitFiles(filesToWait.waitMarkerFileUri, coalesce(filesToWait.paths.map(path => path.fileUri))); } @@ -412,7 +412,7 @@ export class NativeWindow extends BaseWindow { Event.map(Event.filter(this.nativeHostService.onDidMaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: true, windowId })), Event.map(Event.filter(this.nativeHostService.onDidUnmaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: false, windowId })) )(e => this.layoutService.updateWindowMaximizedState(getWindowById(e.windowId)!.window, e.maximized))); - this.layoutService.updateWindowMaximizedState(mainWindow, this.environmentService.window.maximized ?? false); + this.layoutService.updateWindowMaximizedState(mainWindow, this.nativeEnvironmentService.window.maximized ?? false); // Detect panel position to determine minimum width this._register(this.layoutService.onDidChangePanelPosition(pos => this.onDidChangePanelPosition(positionFromString(pos)))); @@ -582,7 +582,7 @@ export class NativeWindow extends BaseWindow { } private maybeCloseWindow(): void { - const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.environmentService.args.wait; + const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.nativeEnvironmentService.args.wait; if (!closeWhenEmpty) { return; // return early if configured to not close when empty } @@ -671,29 +671,6 @@ export class NativeWindow extends BaseWindow { if (this.environmentService.enableSmokeTestDriver) { this.setupDriver(); } - - // Patch methods that we need to work properly - this.patchMethods(); - } - - private patchMethods(): void { - - // Enable `window.focus()` to work in Electron by - // asking the main process to focus the window. - // https://github.com/electron/electron/issues/25578 - const that = this; - const originalWindowFocus = mainWindow.focus.bind(mainWindow); - mainWindow.focus = function () { - if (that.environmentService.extensionTestsLocationURI) { - return; // no focus when we are running tests from CLI - } - - originalWindowFocus(); - - if (!mainWindow.document.hasFocus()) { - that.nativeHostService.focusWindow({ targetWindowId: getWindowId(mainWindow) }); - } - }; } private async handleWarnings(): Promise { @@ -731,11 +708,11 @@ export class NativeWindow extends BaseWindow { let installLocationUri: URI; if (isMacintosh) { // appRoot = /Applications/Visual Studio Code - Insiders.app/Contents/Resources/app - installLocationUri = dirname(dirname(dirname(URI.file(this.environmentService.appRoot)))); + installLocationUri = dirname(dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot)))); } else { // appRoot = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app // appRoot = /usr/share/code-insiders/resources/app - installLocationUri = dirname(dirname(URI.file(this.environmentService.appRoot))); + installLocationUri = dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot))); } for (const folder of this.contextService.getWorkspace().folders) { @@ -753,7 +730,7 @@ export class NativeWindow extends BaseWindow { // macOS 10.13 and 10.14 warning if (isMacintosh) { - const majorVersion = this.environmentService.os.release.split('.')[0]; + const majorVersion = this.nativeEnvironmentService.os.release.split('.')[0]; const eolReleases = new Map([ ['17', 'macOS High Sierra'], ['18', 'macOS Mojave'], diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index ce6f32b0914..6864585e014 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -22,6 +22,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur 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 { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export const IAuxiliaryWindowService = createDecorator('auxiliaryWindowService'); @@ -84,9 +85,10 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { readonly container: HTMLElement, stylesHaveLoaded: Barrier, @IConfigurationService private readonly configurationService: IConfigurationService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(window, undefined, hostService); + super(window, undefined, hostService, environmentService); this.whenStylesHaveLoaded = stylesHaveLoaded.wait().then(() => { }); this.registerListeners(); @@ -182,7 +184,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili @IDialogService private readonly dialogService: IDialogService, @IConfigurationService protected readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService ) { super(); } @@ -237,7 +240,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } protected createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesLoaded: Barrier): AuxiliaryWindow { - return new AuxiliaryWindow(targetWindow, container, stylesLoaded, this.configurationService, this.hostService); + return new AuxiliaryWindow(targetWindow, container, stylesLoaded, this.configurationService, this.hostService, this.environmentService); } private async openWindow(options?: IAuxiliaryWindowOpenOptions): Promise { @@ -293,24 +296,20 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } protected createContainer(auxiliaryWindow: CodeWindow, disposables: DisposableStore, options?: IAuxiliaryWindowOpenOptions): { stylesLoaded: Barrier; container: HTMLElement } { - this.patchMethods(auxiliaryWindow); + auxiliaryWindow.document.createElement = function () { + // Disallow `createElement` because it would create + // HTML Elements in the "wrong" context and break + // code that does "instanceof HTMLElement" etc. + throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.'); + }; this.applyMeta(auxiliaryWindow); const { stylesLoaded } = this.applyCSS(auxiliaryWindow, disposables); const container = this.applyHTML(auxiliaryWindow, disposables); + return { stylesLoaded, container }; } - protected patchMethods(auxiliaryWindow: CodeWindow): void { - - // Disallow `createElement` because it would create - // HTML Elements in the "wrong" context and break - // code that does "instanceof HTMLElement" etc. - auxiliaryWindow.document.createElement = function () { - throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.'); - }; - } - private applyMeta(auxiliaryWindow: CodeWindow): void { for (const metaTag of ['meta[charset="utf-8"]', 'meta[http-equiv="Content-Security-Policy"]', 'meta[name="viewport"]', 'meta[name="theme-color"]']) { const metaElement = mainWindow.document.querySelector(metaTag); diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index d07c6d8addf..4a687027faf 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -17,11 +17,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Barrier } from 'vs/base/common/async'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { applyZoom } from 'vs/platform/window/electron-sandbox/window'; import { getZoomLevel } from 'vs/base/browser/browser'; import { getActiveWindow } from 'vs/base/browser/dom'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; type NativeCodeWindow = CodeWindow & { readonly vscode: ISandboxGlobals; @@ -38,9 +38,10 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { @IConfigurationService configurationService: IConfigurationService, @INativeHostService private readonly nativeHostService: INativeHostService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(window, container, stylesHaveLoaded, configurationService, hostService); + super(window, container, stylesHaveLoaded, configurationService, hostService, environmentService); } protected override async confirmBeforeClose(e: BeforeUnloadEvent): Promise { @@ -68,10 +69,10 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService @IDialogService dialogService: IDialogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITelemetryService telemetryService: ITelemetryService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(layoutService, dialogService, configurationService, telemetryService, hostService); + super(layoutService, dialogService, configurationService, telemetryService, hostService, environmentService); } protected override async resolveWindowId(auxiliaryWindow: NativeCodeWindow): Promise { @@ -97,29 +98,8 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return super.createContainer(auxiliaryWindow, disposables); } - protected override patchMethods(auxiliaryWindow: NativeCodeWindow): void { - super.patchMethods(auxiliaryWindow); - - // Enable `window.focus()` to work in Electron by - // asking the main process to focus the window. - // https://github.com/electron/electron/issues/25578 - const that = this; - const originalWindowFocus = auxiliaryWindow.focus.bind(auxiliaryWindow); - auxiliaryWindow.focus = function () { - if (that.environmentService.extensionTestsLocationURI) { - return; // no focus when we are running tests from CLI - } - - originalWindowFocus(); - - if (!auxiliaryWindow.document.hasFocus()) { - that.nativeHostService.focusWindow({ targetWindowId: auxiliaryWindow.vscodeWindowId }); - } - }; - } - protected override createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesHaveLoaded: Barrier,): AuxiliaryWindow { - return new NativeAuxiliaryWindow(targetWindow, container, stylesHaveLoaded, this.configurationService, this.nativeHostService, this.instantiationService, this.hostService); + return new NativeAuxiliaryWindow(targetWindow, container, stylesHaveLoaded, this.configurationService, this.nativeHostService, this.instantiationService, this.hostService, this.environmentService); } } diff --git a/src/vs/workbench/test/browser/window.test.ts b/src/vs/workbench/test/browser/window.test.ts index 6d9b702cea6..4a395ee47b3 100644 --- a/src/vs/workbench/test/browser/window.test.ts +++ b/src/vs/workbench/test/browser/window.test.ts @@ -10,7 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { BaseWindow } from 'vs/workbench/browser/window'; -import { TestHostService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestHostService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('Window', () => { @@ -19,9 +19,10 @@ suite('Window', () => { class TestWindow extends BaseWindow { constructor(window: CodeWindow, dom: { getWindowsCount: () => number; getWindows: () => Iterable }) { - super(window, dom, new TestHostService()); + super(window, dom, new TestHostService(), TestEnvironmentService); } + protected override enableNativeWindowFocus(): void { } protected override enableWindowFocusOnElementFocus(): void { } } From d374d10eea0154ce74b5cafe64d3e0096dd58717 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 08:41:30 +0100 Subject: [PATCH 171/228] windows - some better handling of focus also for `webview` (#206757) * :lipstick: * fix focus issues with webview and aux window --- src/vs/base/browser/dom.ts | 29 +++++++++++++++++++ src/vs/workbench/browser/composite.ts | 16 ++-------- src/vs/workbench/browser/part.ts | 4 --- src/vs/workbench/browser/window.ts | 15 +++------- src/vs/workbench/common/component.ts | 1 - .../extensions/browser/extensionEditor.ts | 2 +- .../interactive/browser/interactiveEditor.ts | 2 +- .../browser/diff/notebookDiffEditor.ts | 2 +- .../contrib/webview/browser/webviewElement.ts | 6 +++- 9 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index ff113c9baa9..907f89d911e 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,6 +921,35 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } +/** + * Given an element, will attempt to pass focus() to the window it belongs + * to, depending on the options passed in: + * - force: always focus the element's window + * - otherwise: only focus the element's window if another window in the same + * workspace group has focus (when auxiliary windows are opened). + * + * @param element used to figure out the window the element belongs to + */ +export function focusWindow(element: Node, options?: { force: boolean }): void { + const window = getWindow(element); + + // Force: always focus the element window + if (options?.force) { + window.focus(); + } + + // Not forced: only focus the element window if another + // window in the same workspace group has focus (when auxiliary + // windows are opened). + // This prevents stealing focus from another workspace window. + else { + const activeWindow = getActiveWindow(); + if (activeWindow !== window && activeWindow.document.hasFocus()) { + window.focus(); + } + } +} + const globalStylesheets = new Map>(); export function isGlobalStylesheet(node: Node): boolean { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 424fcfa19e5..d56a31212ed 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -36,7 +36,7 @@ export abstract class Composite extends Component implements IComposite { private readonly _onTitleAreaUpdate = this._register(new Emitter()); readonly onTitleAreaUpdate = this._onTitleAreaUpdate.event; - private _onDidFocus: Emitter | undefined; + protected _onDidFocus: Emitter | undefined; get onDidFocus(): Event { if (!this._onDidFocus) { this._onDidFocus = this.registerFocusTrackEvents().onDidFocus; @@ -45,10 +45,6 @@ export abstract class Composite extends Component implements IComposite { return this._onDidFocus.event; } - protected fireOnDidFocus(): void { - this._onDidFocus?.fire(); - } - private _onDidBlur: Emitter | undefined; get onDidBlur(): Event { if (!this._onDidBlur) { @@ -86,22 +82,16 @@ export abstract class Composite extends Component implements IComposite { protected actionRunner: IActionRunner | undefined; - private _telemetryService: ITelemetryService; - protected get telemetryService(): ITelemetryService { return this._telemetryService; } - - private visible: boolean; + private visible = false; private parent: HTMLElement | undefined; constructor( id: string, - telemetryService: ITelemetryService, + protected readonly telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService ) { super(id, themeService, storageService); - - this._telemetryService = telemetryService; - this.visible = false; } getTitle(): string | undefined { diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 7ae271c65cc..e78f59839be 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -61,10 +61,6 @@ export abstract class Part extends Component implements ISerializableView { } } - override updateStyles(): void { - super.updateStyles(); - } - /** * Note: Clients should not call this method, the workbench calls this * method. Calling it otherwise may result in unexpected behavior. diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 9f98ac1c680..203275338eb 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; +import { addDisposableListener, EventHelper, EventType, focusWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; @@ -81,16 +81,9 @@ export abstract class BaseWindow extends Disposable { targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { - // If the active focused window is not the same as the - // window of the element to focus, make sure to focus - // that window first before focusing the element. - const activeWindow = getActiveWindow(); - if (activeWindow.document.hasFocus()) { - const elementWindow = getWindow(this); - if (activeWindow !== elementWindow) { - elementWindow.focus(); - } - } + // Ensure the window the element belongs to is focused + // in scenarios where auxiliary windows are present + focusWindow(this); // Pass to original focus() method originalFocus.apply(this, [options]); diff --git a/src/vs/workbench/common/component.ts b/src/vs/workbench/common/component.ts index 6c25dc9d977..f8dd0115412 100644 --- a/src/vs/workbench/common/component.ts +++ b/src/vs/workbench/common/component.ts @@ -20,7 +20,6 @@ export class Component extends Themable { ) { super(themeService); - this.id = id; this.memento = new Memento(this.id, storageService); this._register(storageService.onWillSaveState(() => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index b2f9fcbd754..34c163c0b47 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -683,7 +683,7 @@ export class ExtensionEditor extends EditorPane { webview.setHtml(body); webview.claim(this, undefined); - this.contentDisposables.add(webview.onDidFocus(() => this.fireOnDidFocus())); + this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire())); this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress))); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 6793200e5e9..d6192e7c946 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -161,7 +161,7 @@ export class InteractiveEditor extends EditorPane { this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); //TODO@bpasero might crash + this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 6be73e321c5..39617722b2b 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -154,7 +154,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @ICodeEditorService codeEditorService: ICodeEditorService ) { super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); - this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false);//TODO@bpasero will crash + this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false); this._register(this._notebookOptions); this._revealFirst = true; } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 6514979da3d..f9baf25d202 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener, EventType, getActiveWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, focusWindow, getActiveWindow } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { promiseWithResolvers, ThrottledDelayer } from 'vs/base/common/async'; import { streamToBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -803,6 +803,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD return; } + // Ensure the window the element belongs to is focused + // in scenarios where auxiliary windows are present + focusWindow(this.element); + try { this.element.contentWindow?.focus(); } catch { From 355fa9491c359a8c0e2288ff5aec4538e7e69b10 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 11:55:15 +0100 Subject: [PATCH 172/228] Comments filter wording feedback (#206776) Fixes #206756 --- .../workbench/contrib/comments/browser/commentsViewActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index e6fd43f4b91..7a0f4d21531 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -123,7 +123,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleUnResolvedComments`, - title: localize('toggle unresolved', "Toggle Unresolved Comments"), + title: localize('toggle unresolved', "Show Unresolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_UNRESOLVED, @@ -148,7 +148,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleResolvedComments`, - title: localize('toggle resolved', "Toggle Resolved Comments"), + title: localize('toggle resolved', "Show Resolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_RESOLVED, From 5f354675ad908a6cb49820a8bba2dfda9781ef70 Mon Sep 17 00:00:00 2001 From: Anthony Stewart <150152+a-stewart@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:17:47 +0100 Subject: [PATCH 173/228] Export ILocalizedString in nls.mock.ts (#206449) --- src/vs/nls.mock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/nls.mock.ts b/src/vs/nls.mock.ts index d9ee1ecd2c6..5323c6c6340 100644 --- a/src/vs/nls.mock.ts +++ b/src/vs/nls.mock.ts @@ -8,7 +8,7 @@ export interface ILocalizeInfo { comment: string[]; } -interface ILocalizedString { +export interface ILocalizedString { original: string; value: string; } From 595efea6480d30fff1bb503df5bf80c950d3661f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 4 Mar 2024 12:28:36 +0100 Subject: [PATCH 174/228] use document object identity and not uri equality when checking if a document belongs to a notebook (#206778) https://github.com/microsoft/vscode/issues/206487 --- src/vs/workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHostNotebookDocument.ts | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a719b6131d3..7344da8ee5e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -541,7 +541,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const interalSelector = typeConverters.LanguageSelector.from(selector); let notebook: vscode.NotebookDocument | undefined; if (targetsNotebooks(interalSelector)) { - notebook = extHostNotebook.notebookDocuments.find(value => Boolean(value.getCell(document.uri)))?.apiNotebook; + notebook = extHostNotebook.notebookDocuments.find(value => value.apiNotebook.getCells().find(c => c.document === document))?.apiNotebook; } return score(interalSelector, document.uri, document.languageId, true, notebook?.uri, notebook?.notebookType); }, diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 2cc7a200edc..8f74a0a4b69 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -442,17 +442,7 @@ export class ExtHostNotebookDocument { return this._cells[index]; } - getCell(cellHandle: number | URI): ExtHostCell | undefined { - if (URI.isUri(cellHandle)) { - const data = notebookCommon.CellUri.parse(cellHandle); - if (!data) { - return undefined; - } - if (data.notebook.toString() !== this.uri.toString()) { - return undefined; - } - cellHandle = data.handle; - } + getCell(cellHandle: number): ExtHostCell | undefined { return this._cells.find(cell => cell.handle === cellHandle); } From 467e1971fc471b5f6c82cf826ab1bcfb12c59cd5 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 12:28:56 +0100 Subject: [PATCH 175/228] Maximum call stack size exceeded (#206781) Fixes #205093 --- src/vs/workbench/contrib/remote/browser/remoteExplorer.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 37948b2bf60..492516a79f7 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -269,8 +269,10 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon this.outputForwarder?.dispose(); this.outputForwarder = undefined; if (environment?.os !== OperatingSystem.Linux) { - Registry.as(ConfigurationExtensions.Configuration) - .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); + if (this.configurationService.inspect(PORT_AUTO_SOURCE_SETTING).default?.value !== PORT_AUTO_SOURCE_SETTING_OUTPUT) { + Registry.as(ConfigurationExtensions.Configuration) + .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); + } this.outputForwarder = this._register(new OutputAutomaticPortForwarding(this.terminalService, this.notificationService, this.openerService, this.externalOpenerService, this.remoteExplorerService, this.configurationService, this.debugService, this.tunnelService, this.hostService, this.logService, this.contextKeyService, () => false)); } else { From 9bbdceef42f1235bf4c64b4eaac0162cce42f482 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 13:12:03 +0100 Subject: [PATCH 176/228] Comments filter looks unbalanced (#206785) * Comments filter looks unbalanced Fixes #206755 * PR feedback * More PR feedback --- src/vs/workbench/browser/parts/views/media/views.css | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 15c4a1aae00..130fd60fe85 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -268,7 +268,7 @@ } .viewpane-filter-container > .viewpane-filter > .viewpane-filter-controls > .viewpane-filter-badge { - margin: 4px 0px; + margin: 4px 2px 4px 0px; padding: 0px 8px; border-radius: 2px; } @@ -278,10 +278,6 @@ display: none; } -.viewpane-filter > .viewpane-filter-controls > .monaco-action-bar .action-item .action-label.codicon.filter { - padding: 2px; -} - .panel > .title .monaco-action-bar .action-item.viewpane-filter-container { max-width: 400px; min-width: 150px; From 25f30add73696a20fa9b2266748159e711ad7985 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 4 Mar 2024 13:26:58 +0100 Subject: [PATCH 177/228] lm-access - update docs (#206788) --- .../vscode.proposed.languageModels.d.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 8732b0e67e0..9cd0f8c1ccb 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -15,7 +15,7 @@ declare module 'vscode' { /** * An async iterable that is a stream of text chunks forming the overall response. * - * *Note* that this stream will error when during receiving an error occurrs. + * *Note* that this stream will error when during data receiving an error occurrs. */ stream: AsyncIterable; } @@ -87,9 +87,11 @@ declare module 'vscode' { constructor(content: string); } + /** + * Different types of language model messages. + */ export type LanguageModelChatMessage = LanguageModelChatSystemMessage | LanguageModelChatUserMessage | LanguageModelChatAssistantMessage; - /** * An event describing the change in the set of available language models. */ @@ -109,7 +111,8 @@ declare module 'vscode' { * * 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 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 { @@ -167,27 +170,25 @@ declare module 'vscode' { /** * Make a chat request using a language model. * - * *Note* that language model use may be subject to access restrictions and user consent. This function will return a rejected promise - * if access to the language model is not possible. Reasons for this can be: + * - *Note 1:* language model use may be subject to access restrictions and user consent. * - * - user consent not given - * - quote limits exceeded - * - model does not exist + * - *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 aviailable values and handle missing language models gracefully. * - * @param languageModel A language model identifier. See {@link languageModels} for aviailable values. + * 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.cause `LanguageModelError.cause`} + * + * @param languageModel A language model identifier. * @param messages An array of message instances. - * @param options Objects that control the request. + * @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. */ - // TODO@API refine doc - // TODO@API ✅ ExtensionContext#permission#languageModels: { languageModel: string: LanguageModelAccessInformation} - // TODO@API ✅ define specific error types? - // TODO@API ✅ NAME: sendChatRequest, fetchChatResponse, makeChatRequest, chat, chatRequest sendChatRequest - // TODO@API ✅ NAME: LanguageModelChatXYZMessage - // TODO@API ✅ errors on everything that prevents us to make the actual request - // TODO@API ✅ double auth - // TODO@API ✅ NAME: LanguageModelChatResponse, ChatResponse, ChatRequestResponse export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options: LanguageModelChatRequestOptions, token: CancellationToken): Thenable; /** From eaf5342bb323c0d64b6013b2a272dab2f0a8a664 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 13:29:14 +0100 Subject: [PATCH 178/228] AutoSave is triggering editor.formatOnSave even when files.autoSave is afterDelay (fix #206475) (#206789) --- .../browser/parts/editor/editorAutoSave.ts | 15 ++++++++------- .../common/filesConfigurationService.ts | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index ddcce4134c0..66940e29b77 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -80,7 +80,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution if (workingCopyResult?.condition === condition) { if ( workingCopyResult.workingCopy.isDirty() && - this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource, workingCopyResult.reason).mode !== AutoSaveMode.OFF ) { this.discardAutoSave(workingCopyResult.workingCopy); @@ -96,7 +96,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution editorResult?.condition === condition && !editorResult.editor.editor.isDisposed() && editorResult.editor.editor.isDirty() && - this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor, editorResult.reason).mode !== AutoSaveMode.OFF ) { this.waitingOnConditionAutoSaveEditors.delete(resource); @@ -151,7 +151,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution return; // no auto save for non-dirty, readonly or untitled editors } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { // Determine if we need to save all. In case of a window focus change we also save if // auto save mode is configured to be ON_FOCUS_CHANGE (editor focus change) @@ -198,7 +198,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution continue; // we never auto save untitled working copies } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { @@ -257,12 +257,13 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Save if dirty and unless prevented by other conditions such as error markers if (workingCopy.isDirty()) { - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const reason = SaveReason.AUTO; + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(), workingCopy.typeId); - workingCopy.save({ reason: SaveReason.AUTO }); + workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { - this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason: SaveReason.AUTO, condition: autoSaveMode.reason }); + this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason, condition: autoSaveMode.reason }); } } }, autoSaveAfterDelay); diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index a2c2076994b..d4c2c6f025e 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -22,7 +22,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { LRUCache, ResourceMap } from 'vs/base/common/map'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -88,7 +88,7 @@ export interface IFilesConfigurationService { hasShortAutoSaveDelay(resourceOrEditor: EditorInput | URI | undefined): boolean; - getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined): IAutoSaveMode; + getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined, saveReason?: SaveReason): IAutoSaveMode; toggleAutoSave(): Promise; @@ -384,7 +384,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return false; } - getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined): IAutoSaveMode { + getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined, saveReason?: SaveReason): IAutoSaveMode { const resource = this.toResource(resourceOrEditor); if (resource && this.autoSaveDisabledOverrides.has(resource)) { return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.DISABLED }; @@ -395,6 +395,16 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.SETTINGS }; } + if (typeof saveReason === 'number') { + if ( + (autoSaveConfiguration.autoSave === 'afterDelay' && saveReason !== SaveReason.AUTO) || + (autoSaveConfiguration.autoSave === 'onFocusChange' && saveReason !== SaveReason.FOCUS_CHANGE && saveReason !== SaveReason.WINDOW_CHANGE) || + (autoSaveConfiguration.autoSave === 'onWindowChange' && saveReason !== SaveReason.WINDOW_CHANGE) + ) { + return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.SETTINGS }; + } + } + if (resource) { if (autoSaveConfiguration.autoSaveWorkspaceFilesOnly && autoSaveConfiguration.isOutOfWorkspace) { return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.OUT_OF_WORKSPACE }; From ad1373ca5a576a0ce3374779266393db655f3caa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 4 Mar 2024 06:26:29 -0800 Subject: [PATCH 179/228] Update xterm --- package.json | 16 +++++------ remote/package.json | 16 +++++------ remote/web/package.json | 14 ++++----- remote/web/yarn.lock | 56 ++++++++++++++++++------------------ remote/yarn.lock | 64 ++++++++++++++++++++--------------------- yarn.lock | 64 ++++++++++++++++++++--------------------- 6 files changed, 115 insertions(+), 115 deletions(-) diff --git a/package.json b/package.json index 04e74dfabb8..3c44945826b 100644 --- a/package.json +++ b/package.json @@ -80,14 +80,14 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.33", - "@xterm/addon-image": "0.7.0-beta.31", - "@xterm/addon-search": "0.14.0-beta.33", - "@xterm/addon-serialize": "0.12.0-beta.33", - "@xterm/addon-unicode11": "0.7.0-beta.33", - "@xterm/addon-webgl": "0.17.0-beta.33", - "@xterm/headless": "5.4.0-beta.33", - "@xterm/xterm": "5.4.0-beta.33", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/headless": "5.5.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/package.json b/remote/package.json index 1070eac356f..2dcfab99b56 100644 --- a/remote/package.json +++ b/remote/package.json @@ -13,14 +13,14 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.33", - "@xterm/addon-image": "0.7.0-beta.31", - "@xterm/addon-search": "0.14.0-beta.33", - "@xterm/addon-serialize": "0.12.0-beta.33", - "@xterm/addon-unicode11": "0.7.0-beta.33", - "@xterm/addon-webgl": "0.17.0-beta.33", - "@xterm/headless": "5.4.0-beta.33", - "@xterm/xterm": "5.4.0-beta.33", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/headless": "5.5.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "cookie": "^0.4.0", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", diff --git a/remote/web/package.json b/remote/web/package.json index 16b4ef21ba4..377df190971 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,13 +7,13 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-canvas": "0.6.0-beta.33", - "@xterm/addon-image": "0.7.0-beta.31", - "@xterm/addon-search": "0.14.0-beta.33", - "@xterm/addon-serialize": "0.12.0-beta.33", - "@xterm/addon-unicode11": "0.7.0-beta.33", - "@xterm/addon-webgl": "0.17.0-beta.33", - "@xterm/xterm": "5.4.0-beta.33", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "jschardet": "3.0.0", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 7ce1173e61e..b69b9a96e3f 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -48,40 +48,40 @@ resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-canvas@0.6.0-beta.33": - version "0.6.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" - integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== -"@xterm/addon-image@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" - integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== -"@xterm/addon-search@0.14.0-beta.33": - version "0.14.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" - integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== -"@xterm/addon-serialize@0.12.0-beta.33": - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" - integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== -"@xterm/addon-unicode11@0.7.0-beta.33": - version "0.7.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" - integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== -"@xterm/addon-webgl@0.17.0-beta.33": - version "0.17.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" - integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== -"@xterm/xterm@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" - integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== jschardet@3.0.0: version "3.0.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index 63fb04013f6..b068bf655bb 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -114,45 +114,45 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-canvas@0.6.0-beta.33": - version "0.6.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" - integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== -"@xterm/addon-image@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" - integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== -"@xterm/addon-search@0.14.0-beta.33": - version "0.14.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" - integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== -"@xterm/addon-serialize@0.12.0-beta.33": - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" - integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== -"@xterm/addon-unicode11@0.7.0-beta.33": - version "0.7.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" - integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== -"@xterm/addon-webgl@0.17.0-beta.33": - version "0.17.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" - integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== -"@xterm/headless@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.33.tgz#8accf68a0e58ba03c8a4627aa108df2d690d8713" - integrity sha512-DlGM2qPdaDmeznUSk9doOQfoi4x8uTTfBysSb+Llxm/SyfxBqnNZEV6N1j494RoBJlawZh2UugmV1itQD+Wlzg== +"@xterm/headless@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.3.tgz#d58d07b5d5e08987cc0cd5888f28f4750627c286" + integrity sha512-F5AdR4VPBmCQGcc57zGTHTT5JZyAUWpBxxY+vclrH/AVxnf9/5uRcSdCmXc8Y558FtdVynG31k48c6fd9n1vVw== -"@xterm/xterm@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" - integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" diff --git a/yarn.lock b/yarn.lock index 1aa5e794d31..4dac8ed481a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1700,45 +1700,45 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8" integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw== -"@xterm/addon-canvas@0.6.0-beta.33": - version "0.6.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" - integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== -"@xterm/addon-image@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" - integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== -"@xterm/addon-search@0.14.0-beta.33": - version "0.14.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" - integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== -"@xterm/addon-serialize@0.12.0-beta.33": - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" - integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== -"@xterm/addon-unicode11@0.7.0-beta.33": - version "0.7.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" - integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== -"@xterm/addon-webgl@0.17.0-beta.33": - version "0.17.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" - integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== -"@xterm/headless@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.33.tgz#8accf68a0e58ba03c8a4627aa108df2d690d8713" - integrity sha512-DlGM2qPdaDmeznUSk9doOQfoi4x8uTTfBysSb+Llxm/SyfxBqnNZEV6N1j494RoBJlawZh2UugmV1itQD+Wlzg== +"@xterm/headless@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.3.tgz#d58d07b5d5e08987cc0cd5888f28f4750627c286" + integrity sha512-F5AdR4VPBmCQGcc57zGTHTT5JZyAUWpBxxY+vclrH/AVxnf9/5uRcSdCmXc8Y558FtdVynG31k48c6fd9n1vVw== -"@xterm/xterm@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" - integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== "@xtuc/ieee754@^1.2.0": version "1.2.0" From 4b7845ab8c1fb7810ffa3e39c8bf493e4b12ac4d Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 4 Mar 2024 15:44:41 +0100 Subject: [PATCH 180/228] reload file icon theme when language configuration changes (#206794) --- .../workbench/services/themes/browser/workbenchThemeService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index cfb4bd79404..a9cb06c4565 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -116,7 +116,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @ILogService private readonly logService: ILogService, @IHostColorSchemeService private readonly hostColorService: IHostColorSchemeService, @IUserDataInitializationService private readonly userDataInitializationService: IUserDataInitializationService, - @ILanguageService languageService: ILanguageService + @ILanguageService private readonly languageService: ILanguageService ) { this.container = layoutService.mainContainer; this.settings = new ThemeConfiguration(configurationService); @@ -378,6 +378,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { await this.setProductIconTheme(DEFAULT_PRODUCT_ICON_THEME_ID, 'auto'); } }); + this.languageService.onDidChange(() => this.reloadCurrentFileIconTheme()); return Promise.all([this.getColorThemes(), this.getFileIconThemes(), this.getProductIconThemes()]).then(([ct, fit, pit]) => { updateColorThemeConfigurationSchemas(ct); From 9e8295eafa599dc2f9088cba2ba1b70f6f022e7d Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 15:51:36 +0100 Subject: [PATCH 181/228] Properly update comment avatar (#206799) Part of microsoft/vscode-pull-request-github#5762 --- .../contrib/comments/browser/commentNode.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 58e376f9561..95ef19e971e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -9,7 +9,7 @@ import * as languages from 'vs/editor/common/languages'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, IActionRunner, IAction, Separator, ActionRunner } from 'vs/base/common/actions'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -60,6 +60,7 @@ class CommentsActionRunner extends ActionRunner { export class CommentNode extends Disposable { private _domNode: HTMLElement; private _body: HTMLElement; + private _avatar: HTMLElement; private _md: HTMLElement | undefined; private _plainText: HTMLElement | undefined; private _clearTimeout: any; @@ -129,12 +130,9 @@ export class CommentNode extends Disposable { this._commentMenus = this.commentService.getCommentMenus(this.owner); this._domNode.tabIndex = -1; - const avatar = dom.append(this._domNode, dom.$('div.avatar-container')); - if (comment.userIconPath) { - const img = dom.append(avatar, dom.$('img.avatar')); - img.src = FileAccess.uriToBrowserUri(URI.revive(comment.userIconPath)).toString(true); - img.onerror = _ => img.remove(); - } + this._avatar = dom.append(this._domNode, dom.$('div.avatar-container')); + this.updateCommentUserIcon(this.comment.userIconPath); + this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); this.createHeader(this._commentDetailsContainer); @@ -223,6 +221,15 @@ export class CommentNode extends Disposable { } } + private updateCommentUserIcon(userIconPath: UriComponents | undefined) { + this._avatar.textContent = ''; + if (userIconPath) { + const img = dom.append(this._avatar, dom.$('img.avatar')); + img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true); + img.onerror = _ => img.remove(); + } + } + public get onDidClick(): Event> { return this._onDidClick.event; } @@ -701,6 +708,10 @@ export class CommentNode extends Disposable { this.updateCommentBody(newComment.body); } + if (this.comment.userIconPath && newComment.userIconPath && (URI.from(this.comment.userIconPath).toString() !== URI.from(newComment.userIconPath).toString())) { + this.updateCommentUserIcon(newComment.userIconPath); + } + const isChangingMode: boolean = newComment.mode !== undefined && newComment.mode !== this.comment.mode; this.comment = newComment; From 0b60cd45e43a39acde273feb3f7bd591fd4d740c Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 4 Mar 2024 07:57:47 -0800 Subject: [PATCH 182/228] monaco-editor -> monaco-workbench --- .../contrib/inlineChat/browser/inlineChat.css | 169 +++++++++--------- 1 file changed, 85 insertions(+), 84 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 60eebc0598d..e1b35651f1c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .zone-widget.inline-chat-widget { +.monaco-workbench .zone-widget.inline-chat-widget { z-index: 3; } -.monaco-editor .zone-widget-container.inside-selection { +.monaco-workbench .zone-widget-container.inside-selection { background-color: var(--vscode-inlineChat-regionHighlight); } -.inline-chat { +.monaco-workbench .inline-chat { color: inherit; padding: 6px; margin-top: 6px; @@ -23,11 +23,11 @@ /* body */ -.inline-chat .body { +.monaco-workbench .inline-chat .body { display: flex; } -.inline-chat .body .content { +.monaco-workbench .inline-chat .body .content { display: flex; box-sizing: border-box; outline: 1px solid var(--vscode-inlineChatInput-border); @@ -35,11 +35,11 @@ border-radius: 2px; } -.inline-chat .body .content.synthetic-focus { +.monaco-workbench .inline-chat .body .content.synthetic-focus { outline: 1px solid var(--vscode-inlineChatInput-focusBorder); } -.inline-chat .body .content .input { +.monaco-workbench .inline-chat .body .content .input { display: flex; align-items: center; justify-content: space-between; @@ -48,11 +48,11 @@ cursor: text; } -.inline-chat .body .content .input .monaco-editor-background { +.monaco-workbench .inline-chat .body .content .input .monaco-editor-background { background-color: var(--vscode-inlineChatInput-background); } -.inline-chat .body .content .input .editor-placeholder { +.monaco-workbench .inline-chat .body .content .input .editor-placeholder { position: absolute; z-index: 1; color: var(--vscode-inlineChatInput-placeholderForeground); @@ -61,14 +61,15 @@ text-overflow: ellipsis; } -.inline-chat .body .content .input .editor-placeholder.hidden { +.monaco-workbench .inline-chat .body .content .input .editor-placeholder.hidden { display: none; } -.inline-chat .body .content .input .editor-container { +.monaco-workbench .inline-chat .body .content .input .editor-container { vertical-align: middle; } -.inline-chat .body .toolbar { + +.monaco-workbench .inline-chat .body .toolbar { display: flex; flex-direction: column; align-self: stretch; @@ -78,47 +79,47 @@ background: var(--vscode-inlineChatInput-background); } -.inline-chat .body .toolbar .actions-container { +.monaco-workbench .inline-chat .body .toolbar .actions-container { display: flex; flex-direction: row; gap: 4px; } -.inline-chat .body > .widget-toolbar { +.monaco-workbench .inline-chat .body > .widget-toolbar { padding-left: 4px; } /* progress bit */ -.inline-chat .progress { +.monaco-workbench .inline-chat .progress { position: relative; width: calc(100% - 18px); left: 19px; } /* UGLY - fighting against workbench styles */ -.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { +.monaco-workbench .part.editor > .content .monaco-editor .inline-chat .progress .monaco-progress-container { top: 0; } /* status */ -.inline-chat .status { +.monaco-workbench .inline-chat .status { margin-top: 4px; display: flex; justify-content: space-between; align-items: center; } -.inline-chat .status.actions { +.monaco-workbench .inline-chat .status.actions { margin-top: 4px; } -.inline-chat .status .actions.hidden { +.monaco-workbench .inline-chat .status .actions.hidden { display: none; } -.inline-chat .status .label { +.monaco-workbench .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); font-size: 11px; @@ -126,60 +127,60 @@ display: inline-flex; } -.inline-chat .status .label.hidden { +.monaco-workbench .inline-chat .status .label.hidden { display: none; } -.inline-chat .status .label.info { +.monaco-workbench .inline-chat .status .label.info { margin-right: auto; padding-left: 2px; } -.inline-chat .status .label.info > .codicon { +.monaco-workbench .inline-chat .status .label.info > .codicon { padding: 0 5px; font-size: 12px; line-height: 18px; } -.inline-chat .status .label.status { +.monaco-workbench .inline-chat .status .label.status { padding-left: 10px; padding-right: 4px; margin-left: auto; } -.inline-chat .status .label .slash-command-pill CODE { +.monaco-workbench .inline-chat .status .label .slash-command-pill CODE { border-radius: 3px; padding: 0 1px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); } -.inline-chat .detectedIntent { +.monaco-workbench .inline-chat .detectedIntent { color: var(--vscode-descriptionForeground); padding: 5px 0px 5px 5px; } -.inline-chat .detectedIntent.hidden { +.monaco-workbench .inline-chat .detectedIntent.hidden { display: none; } -.inline-chat .detectedIntent .slash-command-pill CODE { +.monaco-workbench .inline-chat .detectedIntent .slash-command-pill CODE { border-radius: 3px; padding: 0 1px; background-color: var(--vscode-chat-slashCommandBackground); color: var(--vscode-chat-slashCommandForeground); } -.inline-chat .detectedIntent .slash-command-pill a { +.monaco-workbench .inline-chat .detectedIntent .slash-command-pill a { color: var(--vscode-textLink-foreground); cursor: pointer; } -/* .inline-chat .markdownMessage .message * { +/* .monaco-editor .inline-chat .markdownMessage .message * { margin: unset; } -.inline-chat .markdownMessage .message code { +.monaco-editor .inline-chat .markdownMessage .message code { font-family: var(--monaco-monospace-font); font-size: 12px; color: var(--vscode-textPreformat-foreground); @@ -189,7 +190,7 @@ } */ -.inline-chat .chatMessage .chatMessageContent .value { +.monaco-workbench .inline-chat .chatMessage .chatMessageContent .value { -webkit-line-clamp: initial; -webkit-box-orient: vertical; overflow: hidden; @@ -198,131 +199,131 @@ user-select: text; } -.inline-chat .chatMessage .chatMessageContent[state="cropped"] .value { +.monaco-workbench .inline-chat .chatMessage .chatMessageContent[state="cropped"] .value { -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); } -.inline-chat .chatMessage .chatMessageContent[state="expanded"] .value { +.monaco-workbench .inline-chat .chatMessage .chatMessageContent[state="expanded"] .value { -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); } -.inline-chat .followUps { +.monaco-workbench .inline-chat .followUps { padding: 5px 5px; } -.inline-chat .followUps .interactive-session-followups .monaco-button { +.monaco-workbench .inline-chat .followUps .interactive-session-followups .monaco-button { display: block; color: var(--vscode-textLink-foreground); font-size: 12px; } -.inline-chat .followUps.hidden { +.monaco-workbench .inline-chat .followUps.hidden { display: none; } -.inline-chat .chatMessage { +.monaco-workbench .inline-chat .chatMessage { padding: 8px 3px; } -.inline-chat .chatMessage .chatMessageContent { +.monaco-workbench .inline-chat .chatMessage .chatMessageContent { padding: 2px 2px; } -.inline-chat .chatMessage.hidden { +.monaco-workbench .inline-chat .chatMessage.hidden { display: none; } -.inline-chat .status .label A { +.monaco-workbench .inline-chat .status .label A { color: var(--vscode-textLink-foreground); cursor: pointer; } -.inline-chat .status .label.error { +.monaco-workbench .inline-chat .status .label.error { color: var(--vscode-errorForeground); } -.inline-chat .status .label.warn { +.monaco-workbench .inline-chat .status .label.warn { color: var(--vscode-editorWarning-foreground); } -.inline-chat .status .actions { +.monaco-workbench .inline-chat .status .actions { display: flex; } -.inline-chat .status .actions > .monaco-button, -.inline-chat .status .actions > .monaco-button-dropdown { +.monaco-workbench .inline-chat .status .actions > .monaco-button, +.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown { margin-right: 6px; } -.inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { +.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { display: flex; align-items: center; padding: 0 4px; } -.inline-chat .status .actions > .monaco-button.codicon { +.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon { display: flex; } -.inline-chat .status .actions > .monaco-button.codicon::before { +.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon::before { align-self: center; } -.inline-chat .status .actions .monaco-text-button { +.monaco-workbench .inline-chat .status .actions .monaco-text-button { padding: 2px 4px; white-space: nowrap; } -.inline-chat .status .monaco-toolbar .action-item { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-item { padding: 0 2px; } /* TODO@jrieken not needed? */ -.inline-chat .status .monaco-toolbar .action-label.checked { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-label.checked { color: var(--vscode-inputOption-activeForeground); background-color: var(--vscode-inputOption-activeBackground); outline: 1px solid var(--vscode-inputOption-activeBorder); } -.inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { +.monaco-workbench .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { background-color: var(--vscode-button-hoverBackground); } /* preview */ -.inline-chat .preview { +.monaco-workbench .inline-chat .preview { display: none; } -.inline-chat .previewDiff, -.inline-chat .previewCreate { +.monaco-workbench .inline-chat .previewDiff, +.monaco-workbench .inline-chat .previewCreate { display: inherit; border: 1px solid var(--vscode-inlineChat-border); border-radius: 2px; margin: 6px 0px; } -.inline-chat .previewCreateTitle { +.monaco-workbench .inline-chat .previewCreateTitle { padding-top: 6px; } -.inline-chat .diff-review.hidden, -.inline-chat .previewDiff.hidden, -.inline-chat .previewCreate.hidden, -.inline-chat .previewCreateTitle.hidden { +.monaco-workbench .inline-chat .diff-review.hidden, +.monaco-workbench .inline-chat .previewDiff.hidden, +.monaco-workbench .inline-chat .previewCreate.hidden, +.monaco-workbench .inline-chat .previewCreateTitle.hidden { display: none; } -.inline-chat-toolbar { +.monaco-workbench .inline-chat-toolbar { display: flex; } -.inline-chat-toolbar > .monaco-button{ +.monaco-workbench .inline-chat-toolbar > .monaco-button { margin-right: 6px; } -.inline-chat-toolbar .action-label.checked { +.monaco-workbench .inline-chat-toolbar .action-label.checked { color: var(--vscode-inputOption-activeForeground); background-color: var(--vscode-inputOption-activeBackground); outline: 1px solid var(--vscode-inputOption-activeBorder); @@ -330,65 +331,65 @@ /* decoration styles */ -.inline-chat-inserted-range { +.monaco-workbench .inline-chat-inserted-range { background-color: var(--vscode-inlineChatDiff-inserted); } -.inline-chat-inserted-range-linehighlight { +.monaco-workbench .inline-chat-inserted-range-linehighlight { background-color: var(--vscode-diffEditor-insertedLineBackground); } -.inline-chat-original-zone2 { +.monaco-workbench .inline-chat-original-zone2 { background-color: var(--vscode-diffEditor-removedLineBackground); opacity: 0.8; } -.inline-chat-lines-inserted-range { +.monaco-workbench .inline-chat-lines-inserted-range { background-color: var(--vscode-diffEditor-insertedTextBackground); } -.inline-chat-block-selection { +.monaco-workbench .inline-chat-block-selection { background-color: var(--vscode-inlineChat-regionHighlight); } -.inline-chat-slash-command { +.monaco-workbench .inline-chat-slash-command { opacity: 0; } -.inline-chat-slash-command-detail { +.monaco-workbench .inline-chat-slash-command-detail { opacity: 0.5; } /* diff zone */ -.inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, -.inline-chat-diff-widget .monaco-diff-editor .monaco-editor .margin-view-overlays { +.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, +.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-workbench .margin-view-overlays { background-color: var(--vscode-inlineChat-regionHighlight); } /* create zone */ -.inline-chat-newfile-widget { +.monaco-workbench .inline-chat-newfile-widget { background-color: var(--vscode-inlineChat-regionHighlight); } -.inline-chat-newfile-widget .title { +.monaco-workbench .inline-chat-newfile-widget .title { display: flex; align-items: center; justify-content: space-between; } -.inline-chat-newfile-widget .title .detail { +.monaco-workbench .inline-chat-newfile-widget .title .detail { margin-left: 4px; } -.inline-chat-newfile-widget .buttonbar-widget { +.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget { display: flex; margin-left: auto; margin-right: 8px; } -.inline-chat-newfile-widget .buttonbar-widget > .monaco-button { +.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { display: inline-flex; white-space: nowrap; margin-left: 4px; @@ -396,22 +397,22 @@ /* gutter decoration */ -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { display: block; cursor: pointer; transition: opacity .2s ease-in-out; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { opacity: 0.5; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { opacity: 0; } -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, -.monaco-editor .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, +.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { opacity: 1; } From 6cd902f14117929dd3df0caf3cd23229a7c3de30 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 4 Mar 2024 08:00:57 -0800 Subject: [PATCH 183/228] get progress style to work --- src/vs/workbench/contrib/inlineChat/browser/inlineChat.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index e1b35651f1c..31fe615709b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -98,7 +98,7 @@ } /* UGLY - fighting against workbench styles */ -.monaco-workbench .part.editor > .content .monaco-editor .inline-chat .progress .monaco-progress-container { +.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { top: 0; } From 0bfc1dbb62d51ec68e1e8d7eae14231d51f59d92 Mon Sep 17 00:00:00 2001 From: Marcus Revaj Date: Mon, 4 Mar 2024 17:05:28 +0100 Subject: [PATCH 184/228] # Add partial accept kind to inline completion handle (#202668) * # Add partial accept kind to inline completion handle --------- Co-authored-by: Henning Dieterichs --- src/vs/editor/common/languages.ts | 18 ++++++++++++++- .../common/standalone/standaloneEnums.ts | 9 ++++++++ .../browser/inlineCompletionsModel.ts | 14 +++++++---- .../standalone/browser/standaloneLanguages.ts | 1 + src/vs/monaco.d.ts | 18 ++++++++++++++- .../api/browser/mainThreadLanguageFeatures.ts | 4 ++-- .../workbench/api/common/extHost.api.impl.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostLanguageFeatures.ts | 9 ++++---- .../api/common/extHostTypeConverters.ts | 23 +++++++++++++++++++ src/vs/workbench/api/common/extHostTypes.ts | 11 +++++++++ ...e.proposed.inlineCompletionsAdditions.d.ts | 18 +++++++++++++++ 12 files changed, 115 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 57f11ed3e4e..4d157bf5788 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -547,6 +547,22 @@ export interface CompletionList { duration?: number; } +/** + * Info provided on partial acceptance. + */ +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +/** + * How a partial acceptance was triggered. + */ +export const enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2, +} + /** * How a suggest provider was triggered. */ @@ -718,7 +734,7 @@ export interface InlineCompletionsProvider { @@ -389,10 +389,10 @@ export class InlineCompletionsModel extends Disposable { return m.index + 1; } return text.length; - }); + }, PartialAcceptTriggerKind.Line); } - private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number): Promise { + private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); } @@ -448,6 +448,9 @@ export class InlineCompletionsModel extends Disposable { completion.source.inlineCompletions, completion.sourceInlineCompletion, text.length, + { + kind, + } ); } } finally { @@ -465,6 +468,9 @@ export class InlineCompletionsModel extends Disposable { inlineCompletion.source.inlineCompletions, inlineCompletion.sourceInlineCompletion, itemEdit.text.length, + { + kind: PartialAcceptTriggerKind.Suggest, + } ); } } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 8466fbf9f99..5ade938e7c2 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -809,6 +809,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { InlineEditTriggerKind: standaloneEnums.InlineEditTriggerKind, CodeActionTriggerType: standaloneEnums.CodeActionTriggerType, NewSymbolNameTag: standaloneEnums.NewSymbolNameTag, + PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c094df337af..3391b58aa2e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6956,6 +6956,22 @@ declare namespace monaco.languages { dispose?(): void; } + /** + * Info provided on partial acceptance. + */ + export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; + } + + /** + * How a partial acceptance was triggered. + */ + export enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2 + } + /** * How a suggest provider was triggered. */ @@ -7102,7 +7118,7 @@ declare namespace monaco.languages { /** * Will be called when an item is partially accepted. */ - handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number): void; + handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number, info: PartialAcceptInfo): void; /** * Will be called when a completions list is no longer in use and can be garbage-collected. */ diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 80d13f11362..b7ecbfdb9a2 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -601,9 +601,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText); } }, - handlePartialAccept: async (completions, item, acceptedCharacters): Promise => { + handlePartialAccept: async (completions, item, acceptedCharacters, info: languages.PartialAcceptInfo): Promise => { if (supportsHandleEvents) { - await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters); + await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters, info); } }, freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 7344da8ee5e..9ac80463738 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1677,6 +1677,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ThreadFocus: extHostTypes.ThreadFocus, RelatedInformationType: extHostTypes.RelatedInformationType, SpeechToTextStatus: extHostTypes.SpeechToTextStatus, + PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d7e5580443b..575c58a4d24 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2128,7 +2128,7 @@ export interface ExtHostLanguageFeaturesShape { $releaseCompletionItems(handle: number, id: number): void; $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; - $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number): void; + $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; $freeInlineCompletionsList(handle: number, pid: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: languages.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index d6bc4f793aa..1c532bcc8ee 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1230,7 +1230,7 @@ class InlineCompletionAdapterBase { handleDidShowCompletionItem(pid: number, idx: number, updatedInsertText: string): void { } - handlePartialAccept(pid: number, idx: number, acceptedCharacters: number): void { } + handlePartialAccept(pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { } } class InlineCompletionAdapter extends InlineCompletionAdapterBase { @@ -1345,11 +1345,12 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } } - override handlePartialAccept(pid: number, idx: number, acceptedCharacters: number): void { + override handlePartialAccept(pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { if (this._provider.handleDidPartiallyAcceptCompletionItem && this._isAdditionsProposedApiEnabled) { this._provider.handleDidPartiallyAcceptCompletionItem(completionItem, acceptedCharacters); + this._provider.handleDidPartiallyAcceptCompletionItem(completionItem, typeConvert.PartialAcceptInfo.to(info)); } } } @@ -2489,9 +2490,9 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF }, undefined, undefined); } - $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number): void { + $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { - adapter.handlePartialAccept(pid, idx, acceptedCharacters); + adapter.handlePartialAccept(pid, idx, acceptedCharacters, info); }, undefined, undefined); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 68cbe330492..13077072ff3 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2672,6 +2672,29 @@ export namespace TerminalQuickFix { } } +export namespace PartialAcceptInfo { + export function to(info: languages.PartialAcceptInfo): types.PartialAcceptInfo { + return { + kind: PartialAcceptTriggerKind.to(info.kind), + }; + } +} + +export namespace PartialAcceptTriggerKind { + export function to(kind: languages.PartialAcceptTriggerKind): types.PartialAcceptTriggerKind { + switch (kind) { + case languages.PartialAcceptTriggerKind.Word: + return types.PartialAcceptTriggerKind.Word; + case languages.PartialAcceptTriggerKind.Line: + return types.PartialAcceptTriggerKind.Line; + case languages.PartialAcceptTriggerKind.Suggest: + return types.PartialAcceptTriggerKind.Suggest; + default: + return types.PartialAcceptTriggerKind.Unknown; + } + } +} + export namespace DebugTreeItem { export function from(item: vscode.DebugTreeItem, id: number): IDebugVisualizationTreeItem { return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 2e1ff171d1f..8660835e2d6 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1795,6 +1795,17 @@ export class InlineSuggestionList implements vscode.InlineCompletionList { } } +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +export enum PartialAcceptTriggerKind { + Unknown = 0, + Word = 1, + Line = 2, + Suggest = 3, +} + export enum ViewColumn { Active = -1, Beside = -2, diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index 9e3ead9be40..2715014a0a8 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -53,6 +53,24 @@ declare module 'vscode' { */ // eslint-disable-next-line local/vscode-dts-provider-naming handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, acceptedLength: number): void; + + /** + * Is called when an inline completion item was accepted partially. + * @param info Additional info for the partial accepted trigger. + */ + // eslint-disable-next-line local/vscode-dts-provider-naming + handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + } + + export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; + } + + export enum PartialAcceptTriggerKind { + Unknown = 0, + Word = 1, + Line = 2, + Suggest = 3, } // When finalizing `commands`, make sure to add a corresponding constructor parameter. From 5124bd41330d1dd6bd1a9452048fcddacbd20175 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 4 Mar 2024 17:23:57 +0100 Subject: [PATCH 185/228] adding telemetry for when the bulk edit pane has been opened --- .../contrib/bulkEdit/browser/preview/bulkEditPane.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index ba85dee93b2..6f4e6cce624 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -100,6 +100,12 @@ export class BulkEditPane extends ViewPane { this._ctxHasCategories = BulkEditPane.ctxHasCategories.bindTo(contextKeyService); this._ctxGroupByFile = BulkEditPane.ctxGroupByFile.bindTo(contextKeyService); this._ctxHasCheckedChanges = BulkEditPane.ctxHasCheckedChanges.bindTo(contextKeyService); + // telemetry + type BulkEditPaneOpened = { + owner: 'aiday-mar'; + comment: 'Report when the bulk edit pane has been opened'; + }; + this.telemetryService.publicLog2<{}, BulkEditPaneOpened>('views.bulkEditPane'); } override dispose(): void { From eb6000d5249366a60d7547be2f3fc6f2276ae0c4 Mon Sep 17 00:00:00 2001 From: Hylke Bons Date: Wed, 28 Feb 2024 16:49:43 +0100 Subject: [PATCH 186/228] breakpoints: Tweak tooltip string --- .../contrib/debug/browser/breakpointEditorContribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 1c36e83c55c..710d0d1d783 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -54,7 +54,7 @@ const breakpointHelperDecoration: IModelDecorationOptions = { description: 'breakpoint-helper-decoration', glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), glyphMargin: { position: GlyphMarginLane.Right }, - glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint.")), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint")), stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; From 0f5be7c29fc276f70cfd4cf4140079aaef649823 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:35:10 +0100 Subject: [PATCH 187/228] Decorations - use the color of the first decoration that has the color set (#206813) --- .../services/decorations/browser/decorationsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 31f1057999d..edf7fb27300 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -87,7 +87,7 @@ class DecorationRule { private _appendForMany(data: IDecorationData[], element: HTMLStyleElement): void { // label - const { color } = data[0]; + const { color } = data.find(d => !!d.color) ?? data[0]; createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(color)};`, element); // badge or icon From 3e670468cacc479c19c81a72f6c88e14a67aa7a9 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 17:36:13 +0100 Subject: [PATCH 188/228] Bump distro (#206812) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c44945826b..44e075ebd81 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "c628288b553c076290c08f74482e6b71c337e4a8", + "distro": "aa80bd2351f35faf630bcb132a08b10281943951", "author": { "name": "Microsoft Corporation" }, From 052fbcb851edf39fb74b7d8bea5d818ba81068cb Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:11:32 +0100 Subject: [PATCH 189/228] Git - re-enable incoming files decoration (#206815) --- extensions/git/src/decorationProvider.ts | 165 +++++++++++------------ 1 file changed, 80 insertions(+), 85 deletions(-) diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 5167b1eb95e..3aae16f6baf 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode'; +import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n } from 'vscode'; import * as path from 'path'; import { Repository, GitResourceGroup } from './repository'; import { Model } from './model'; import { debounce } from './decorators'; import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable } from './util'; -import { GitErrorCodes, Status } from './api/git'; +import { Change, GitErrorCodes, Status } from './api/git'; class GitIgnoreDecorationProvider implements FileDecorationProvider { @@ -153,100 +153,95 @@ class GitDecorationProvider implements FileDecorationProvider { } } -// class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { +class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { -// private readonly _onDidChangeDecorations = new EventEmitter(); -// readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; + private readonly _onDidChangeDecorations = new EventEmitter(); + readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; -// private decorations = new Map(); -// private readonly disposables: Disposable[] = []; + private decorations = new Map(); + private readonly disposables: Disposable[] = []; -// constructor(private readonly repository: Repository) { -// this.disposables.push(window.registerFileDecorationProvider(this)); -// repository.historyProvider.onDidChangeCurrentHistoryItemGroup(this.onDidChangeCurrentHistoryItemGroup, this, this.disposables); -// } + constructor(private readonly repository: Repository) { + this.disposables.push(window.registerFileDecorationProvider(this)); + repository.historyProvider.onDidChangeCurrentHistoryItemGroup(this.onDidChangeCurrentHistoryItemGroup, this, this.disposables); + } -// private async onDidChangeCurrentHistoryItemGroup(): Promise { -// const newDecorations = new Map(); -// await this.collectIncomingChangesFileDecorations(newDecorations); -// const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); + private async onDidChangeCurrentHistoryItemGroup(): Promise { + const newDecorations = new Map(); + await this.collectIncomingChangesFileDecorations(newDecorations); + const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); -// this.decorations = newDecorations; -// this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); -// } + this.decorations = newDecorations; + this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); + } -// private async collectIncomingChangesFileDecorations(bucket: Map): Promise { -// for (const change of await this.getIncomingChanges()) { -// switch (change.status) { -// case Status.INDEX_ADDED: -// bucket.set(change.uri.toString(), { -// badge: '↓A', -// color: new ThemeColor('gitDecoration.incomingAddedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (added)'), -// }); -// break; -// case Status.DELETED: -// bucket.set(change.uri.toString(), { -// badge: '↓D', -// color: new ThemeColor('gitDecoration.incomingDeletedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (deleted)'), -// }); -// break; -// case Status.INDEX_RENAMED: -// bucket.set(change.originalUri.toString(), { -// badge: '↓R', -// color: new ThemeColor('gitDecoration.incomingRenamedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (renamed)'), -// }); -// break; -// case Status.MODIFIED: -// bucket.set(change.uri.toString(), { -// badge: '↓M', -// color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (modified)'), -// }); -// break; -// default: { -// bucket.set(change.uri.toString(), { -// badge: '↓~', -// color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'), -// tooltip: l10n.t('Incoming Changes'), -// }); -// break; -// } -// } -// } -// } + private async collectIncomingChangesFileDecorations(bucket: Map): Promise { + for (const change of await this.getIncomingChanges()) { + switch (change.status) { + case Status.INDEX_ADDED: + bucket.set(change.uri.toString(), { + badge: '↓A', + tooltip: l10n.t('Incoming Changes (added)'), + }); + break; + case Status.DELETED: + bucket.set(change.uri.toString(), { + badge: '↓D', + tooltip: l10n.t('Incoming Changes (deleted)'), + }); + break; + case Status.INDEX_RENAMED: + bucket.set(change.originalUri.toString(), { + badge: '↓R', + tooltip: l10n.t('Incoming Changes (renamed)'), + }); + break; + case Status.MODIFIED: + bucket.set(change.uri.toString(), { + badge: '↓M', + tooltip: l10n.t('Incoming Changes (modified)'), + }); + break; + default: { + bucket.set(change.uri.toString(), { + badge: '↓~', + tooltip: l10n.t('Incoming Changes'), + }); + break; + } + } + } + } -// private async getIncomingChanges(): Promise { -// try { -// const historyProvider = this.repository.historyProvider; -// const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; + private async getIncomingChanges(): Promise { + try { + const historyProvider = this.repository.historyProvider; + const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; -// if (!currentHistoryItemGroup?.base) { -// return []; -// } + if (!currentHistoryItemGroup?.base) { + return []; + } -// const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); -// if (!ancestor) { -// return []; -// } + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); + if (!ancestor) { + return []; + } -// const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); -// return changes; -// } catch (err) { -// return []; -// } -// } + const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); + return changes; + } catch (err) { + return []; + } + } -// provideFileDecoration(uri: Uri): FileDecoration | undefined { -// return this.decorations.get(uri.toString()); -// } + provideFileDecoration(uri: Uri): FileDecoration | undefined { + return this.decorations.get(uri.toString()); + } -// dispose(): void { -// dispose(this.disposables); -// } -// } + dispose(): void { + dispose(this.disposables); + } +} export class GitDecorations { @@ -287,7 +282,7 @@ export class GitDecorations { private onDidOpenRepository(repository: Repository): void { const providers = combinedDisposable([ new GitDecorationProvider(repository), - // new GitIncomingChangesFileDecorationProvider(repository) + new GitIncomingChangesFileDecorationProvider(repository) ]); this.providers.set(repository, providers); From c0b99c07feb342b3584694afa09faccad84f03d5 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 4 Mar 2024 09:40:00 -0800 Subject: [PATCH 190/228] Update src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts --- .../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 620a12b50af..36663230871 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -53,7 +53,7 @@ export const enum AccessibilityVerbositySettingId { Hover = 'accessibility.verbosity.hover', Notification = 'accessibility.verbosity.notification', EmptyEditorHint = 'accessibility.verbosity.emptyEditorHint', - Comments = 'accessibility.verbosity.comments', + Comments = 'accessibility.verbosity.comments' } export const enum AccessibleViewProviderId { From 7a7b5ac3550ba3d2ae5df97fcf9ece3b436eef9e Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 4 Mar 2024 09:45:46 -0800 Subject: [PATCH 191/228] rm extra provider id --- .../contrib/accessibility/browser/accessibilityConfiguration.ts | 1 - .../terminalContrib/chat/browser/terminalChatAccessibleView.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 36663230871..e3b3bb1ef7a 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -64,7 +64,6 @@ export const enum AccessibleViewProviderId { Chat = 'panelChat', InlineChat = 'inlineChat', InlineCompletions = 'inlineCompletions', - TerminalInlineChat = 'terminalInlineChat', KeybindingsEditor = 'keybindingsEditor', Notebook = 'notebook', Editor = 'editor', diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index 798c0bf9774..c8b11add5be 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -26,7 +26,7 @@ export class TerminalInlineChatAccessibleViewContribution extends Disposable { } const responseContent = controller.lastResponseContent; accessibleViewService.show({ - id: AccessibleViewProviderId.TerminalInlineChat, + id: AccessibleViewProviderId.TerminalChat, verbositySettingKey: AccessibilityVerbositySettingId.InlineChat, provideContent(): string { return responseContent; }, onClose() { From 317d126ddf226bfcab3729ea6e01a3c21943af74 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 4 Mar 2024 09:47:10 -0800 Subject: [PATCH 192/228] use new function call, fix error --- .../terminalContrib/chat/browser/terminalChatController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index 9ddc1e54349..c6f624dd865 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -99,9 +99,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService); this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService); - if (!this._chatAgentService.hasAgent(this._terminalAgentId)) { + if (!this._chatAgentService.getRegisteredAgent(this._terminalAgentId)) { this._register(this._chatAgentService.onDidChangeAgents(() => { - if (this._chatAgentService.getAgent(this._terminalAgentId)) { + if (this._chatAgentService.getRegisteredAgent(this._terminalAgentId)) { this._terminalAgentRegisteredContextKey.set(true); } })); From c19255de9261c3d203c9e707f828eb590c41d2b3 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 4 Mar 2024 15:55:39 -0300 Subject: [PATCH 193/228] Also keep focus in editor for "go to top/bottom of callstack" (#206817) Fix #205922 --- src/vs/workbench/contrib/debug/browser/debugCommands.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 3bb555a1a53..f2f839ff912 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -237,7 +237,7 @@ async function goToBottomOfCallStack(debugService: IDebugService) { if (callStack.length > 0) { const nextVisibleFrame = findNextVisibleFrame(false, callStack, 0); // must consider the next frame up first, which will be the last frame if (nextVisibleFrame) { - debugService.focusStackFrame(nextVisibleFrame); + debugService.focusStackFrame(nextVisibleFrame, undefined, undefined, { preserveFocus: false }); } } } @@ -247,7 +247,7 @@ function goToTopOfCallStack(debugService: IDebugService) { const thread = debugService.getViewModel().focusedThread; if (thread) { - debugService.focusStackFrame(thread.getTopStackFrame()); + debugService.focusStackFrame(thread.getTopStackFrame(), undefined, undefined, { preserveFocus: false }); } } From 069f4d91f377c207f4857e93c0965358985e9ca3 Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 5 Mar 2024 03:56:04 +0900 Subject: [PATCH 194/228] chore: bump electron@28.2.5 (#206822) * chore: bump electron@28.2.5 * chore: bump distro --- .yarnrc | 4 +- build/checksums/electron.txt | 150 +++++++++--------- cgmanifest.json | 8 +- package.json | 4 +- .../windows/electron-main/windowImpl.ts | 7 + yarn.lock | 8 +- 6 files changed, 94 insertions(+), 87 deletions(-) diff --git a/.yarnrc b/.yarnrc index 8675f7ab83c..dc10cd8fae6 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "28.2.2" -ms_build_id "26836304" +target "28.2.5" +ms_build_id "27336930" runtime "electron" build_from_source "true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 35feee9b876..72d5349d425 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -b1478a79a80e453e9ee39ad4a0b0e0d871f3e369ec519e3ac5a1da20bb7bdbf3 *chromedriver-v28.2.2-darwin-arm64.zip -4e7c4651d610c70de883b9ceef633f1a2bf90e0f3a732eae7a6d7bcad11bb2df *chromedriver-v28.2.2-darwin-x64.zip -7cee31da7d90c2a24338a10046386517bb93c69c79bd44cfcc9372a551fc7d01 *chromedriver-v28.2.2-linux-arm64.zip -2056e41f713d1a6c83d1f0260c0f2b8addc3c49887ae85ca7e92267eb53951e8 *chromedriver-v28.2.2-linux-armv7l.zip -19503257092605e21bd3798f5ffd0049d8420a504ececef7b1e95d3733846874 *chromedriver-v28.2.2-linux-x64.zip -ec09eeb8a7040c7402a8a5f54491b33e5dc95ea0535b55381a3ec405014f08db *chromedriver-v28.2.2-mas-arm64.zip -1dd5cb2a113c74ae84f2ac98f6f40da2c367014381a547788fea9ae220e6fc9f *chromedriver-v28.2.2-mas-x64.zip -fd505e1f1c2f72266c48914690a48918fc7920877215a508ea5325cf0353f72c *chromedriver-v28.2.2-win32-arm64.zip -c0226c0fb260d6812185eeea718c8c0054d0fcac995bb1ccb333f852206372c8 *chromedriver-v28.2.2-win32-ia32.zip -b6daccad5bcd3046d0678c927f6b97ed91f2242f716deb0de95a0ee2303af818 *chromedriver-v28.2.2-win32-x64.zip -76a88da92b950c882d90c3dcb26e0c2ca5e5a52ad7a066ec0b3cbf9cc4d04563 *electron-api.json -6ad08d733c95de3c30560e8289d0e657ed5ee03bc8ba9d1f11d528851e5b7fba *electron-v28.2.2-darwin-arm64-dsym-snapshot.zip -8f0d450f3d2392cbe7a6cb274ec0f3bf63da66c98fa0baaa2355e69f1c93b151 *electron-v28.2.2-darwin-arm64-dsym.zip -262036eb86b767db0d199df022b8b432aa3714e451b9ac656af7ef031581b44a *electron-v28.2.2-darwin-arm64-symbols.zip -23119b333c47a5ea9e36e04cdc3b8c5955cfccfeb90994f1fecea4722bfb8dcc *electron-v28.2.2-darwin-arm64.zip -384015a3e49a6846ebefc78f9f01ce6d47c2ec109e6223907298aa6382b0d072 *electron-v28.2.2-darwin-x64-dsym-snapshot.zip -434838821de746ff71baafdf9e0df07cb3766dd73eb7fcd253aee0571bd0cd59 *electron-v28.2.2-darwin-x64-dsym.zip -470087b5d631dc0032611048d5fc23faed9a71ec2c36a528c5a50c2e357d1716 *electron-v28.2.2-darwin-x64-symbols.zip -48f3424b3cbdf602a13f451361ade2f7f2896a354a51f78da4239dbdf2d1218b *electron-v28.2.2-darwin-x64.zip -d5bf835ba4b2eaa4435946f97ad7ac3e7243564037423cfaadaf5cb03af4ddbc *electron-v28.2.2-linux-arm64-debug.zip -90550f29b1f032ebcf467dc81f4915c322f93855a4658cf74261f68a3ccdc21e *electron-v28.2.2-linux-arm64-symbols.zip -746284eb1d8029b0f6b02281543ab2ecf45f071da21407f45b2b32d1ff268310 *electron-v28.2.2-linux-arm64.zip -d5bf835ba4b2eaa4435946f97ad7ac3e7243564037423cfaadaf5cb03af4ddbc *electron-v28.2.2-linux-armv7l-debug.zip -80cc8d9333156caaee59c7ddf3bd77712be8379b51f4218063e6c176a4ec2c26 *electron-v28.2.2-linux-armv7l-symbols.zip -f4580e8877481c0526110feaa78372ed3045bfbf5a6ba4b14e8cd155b9965f5e *electron-v28.2.2-linux-armv7l.zip -da33d92871768e4cf95b143c6022830d97b0ec2d4120463ab71b48597f940f07 *electron-v28.2.2-linux-x64-debug.zip -18dce5283513abd94b79a1636d25e3453f5c33d335425a234b9967dd4e5ce942 *electron-v28.2.2-linux-x64-symbols.zip -1eeb6ebc3b0699cae1fb171bbf7c9105e716db833f6e73a90f4ca161f17ffb15 *electron-v28.2.2-linux-x64.zip -e74da15d90e52cddf0f0f14663f6313df585b486b002966f6016c1b148cdd70d *electron-v28.2.2-mas-arm64-dsym-snapshot.zip -498357eb2e784bff54c5ac59fd3eada814d130f12a5e77d47c468f2305377717 *electron-v28.2.2-mas-arm64-dsym.zip -849fa891d072d06b1e929eb1acfbe7ac83f0238483039f8e1102e01e5223c3f5 *electron-v28.2.2-mas-arm64-symbols.zip -621fd91d70cb33ec58543fc57762e692dfa0e272a53f3316fd215ffa88bd075b *electron-v28.2.2-mas-arm64.zip -0f9e2ab79bca99f44c1e9a140929fad6d2cd37def60303974f5a82ca95dd9a69 *electron-v28.2.2-mas-x64-dsym-snapshot.zip -51c29e047ba7d8669030cc9615f70ecaa5c9519cd04ab5e62822c0d4f21f5fbb *electron-v28.2.2-mas-x64-dsym.zip -25da93f45b095a3669475416832647a01f2a02a95dcc064dfabdf9c621045106 *electron-v28.2.2-mas-x64-symbols.zip -3b6931362f1b7f377624ea7c6ccf069f291e4e675a28f12a56e3e75355c13fbd *electron-v28.2.2-mas-x64.zip -cece93c232c65bf4e1b918b9645f5a2e247bd3f8bb2dd9e6e889a402060a103b *electron-v28.2.2-win32-arm64-pdb.zip -4a46e1ead0de7b6f757c1194add6467b3375a8dcfb02d903e481c0d8db5c7e5d *electron-v28.2.2-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-arm64-toolchain-profile.zip -083f95abbce97cab70e77b86e39cff01ff1df121f36b9da581ead960ae329f69 *electron-v28.2.2-win32-arm64.zip -f9b4633bc03fe7c77db4b335121e7e3e05f6788c6752ccb3f68267e664d4323a *electron-v28.2.2-win32-ia32-pdb.zip -d20f70ea8dc86477f723d104d42fe78e2508577ef3b1eb6ec812366f18ad80d8 *electron-v28.2.2-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-ia32-toolchain-profile.zip -c57691b73592632829cef136be6dd356a82331450920fd024ac3589654d23550 *electron-v28.2.2-win32-ia32.zip -b8a14fc75b9205a4b82aa008e23a020e9fac694399b47390a163c3100ac7946d *electron-v28.2.2-win32-x64-pdb.zip -780089dde95ce1ab5da176ad53d9c7cd122085809622852a3132b80b93faac9b *electron-v28.2.2-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-x64-toolchain-profile.zip -5fbc76585891b0d7b09c938f7be25b7ab36b3768491021b053dc99bc70a8aa29 *electron-v28.2.2-win32-x64.zip -225268475350fa71d9fdea966160fc91379ced2f2353a902addf65d5f9b0dbf1 *electron.d.ts -59a8d6b81d93bc99ecf099fac6492eb67ba601386cce07261a009a5b99e75479 *ffmpeg-v28.2.2-darwin-arm64.zip -15386f238dce9ba40714336265422cc41a1ef0608041f562a8fd42e3813ddc64 *ffmpeg-v28.2.2-darwin-x64.zip -8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.2-linux-arm64.zip -51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.2-linux-armv7l.zip -acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.2-linux-x64.zip -e71aac5c02f67bd5ba5d650160ff4edb122f697ab6bd8e686eae78426c439733 *ffmpeg-v28.2.2-mas-arm64.zip -3d0bb26cc9b751dad883750972fddec72aa936ecaa0d9bd198ba9b47203410e8 *ffmpeg-v28.2.2-mas-x64.zip -035b24a44f09587092e7db4e28400139901cec6378b3c828ce9f90a60f4f3a9a *ffmpeg-v28.2.2-win32-arm64.zip -38b25e225fd028f1f3f2c551f3b42d62d9e5c4ef388e0b0e019e9c8d93a85b07 *ffmpeg-v28.2.2-win32-ia32.zip -41849e779371dc0c35899341ae658b883ef0124296787ad96b7d5e4d9b69f1b9 *ffmpeg-v28.2.2-win32-x64.zip -8830364f8050164b1736246c30e96ae7ac876bcec5af1bf6344edbd66ed45353 *hunspell_dictionaries.zip -fc417873289fa9c947598ed73a27a28c4b5a07ce90ef998bb56550c4e10a034b *libcxx-objects-v28.2.2-linux-arm64.zip -06e9cdb2e8785a0835f66d34e9518c47ef220e32646e5b43e599339836e9e7b1 *libcxx-objects-v28.2.2-linux-armv7l.zip -ac098a006a8f84d0bb19088b2dec3ee3068b19208c5611194e831b1e5878fb2d *libcxx-objects-v28.2.2-linux-x64.zip -56414a1e809874949c1a1111b8e68b8d4f40d55cb481ad4869e920e47fe1b71b *libcxx_headers.zip -36e46cbed397cc1fe34d8dc477d3a87613acb9936f811535c1300e138e1a7008 *libcxxabi_headers.zip -94b01f4dd6bd56dec39a0be9ac14bb8c9a73db22cb579d6093f4f4c95a4a8896 *mksnapshot-v28.2.2-darwin-arm64.zip -ea768087b4fedf09c38eb093beb744c1a3b5b2a54025a83f1e2301ea03539500 *mksnapshot-v28.2.2-darwin-x64.zip -b9a01ba90abb69877838515d8273532e4aeea6d66c49b8aac3267e26546fc8b3 *mksnapshot-v28.2.2-linux-arm64-x64.zip -60005160b5e9db4a3847c63893f44e18ca86657a3ec97b6c13a90e43291bdb65 *mksnapshot-v28.2.2-linux-armv7l-x64.zip -81bf5ec59e7c33c642b79582fc5b775ec635ce0c52f3f5c30315cb45fdbffd12 *mksnapshot-v28.2.2-linux-x64.zip -7bfbe3cf02713b1a09aa19b75b876e158ed167b0d4345ec3b429061b53fc4b8f *mksnapshot-v28.2.2-mas-arm64.zip -91f7d34a05fa9c7cda4f36a44309f51e7defea2134d5bcc818a3eb4537979870 *mksnapshot-v28.2.2-mas-x64.zip -3f7163a34aae864cd44ebec086d4fab30132924680f20136cf19348811bace50 *mksnapshot-v28.2.2-win32-arm64-x64.zip -ac64fbfb78a1f6f389dac96ad7c655e2ea6fb2289e38a8fd516dbbda6bea42a3 *mksnapshot-v28.2.2-win32-ia32.zip -1bcd03747ce3eee6dd94b0608a0812268dacf77bac5541c581c22b92f700b303 *mksnapshot-v28.2.2-win32-x64.zip +23d9bca1abd1c64d0bd47b9528b8db1b1f28c31e81188ecbed4e9cd18eab3545 *chromedriver-v28.2.5-darwin-arm64.zip +9215cf2196988c5f0e0a01fe1bdd827ab25f3a0895b6e9ff96185fed45be24d9 *chromedriver-v28.2.5-darwin-x64.zip +a27c39a8a9f02a630f4ea1218954e768791e44319ce34e99bb524d45aa956376 *chromedriver-v28.2.5-linux-arm64.zip +658bef49300d3183a34609391f64f3df6c9b07eb55886fa1378249e1170ac68e *chromedriver-v28.2.5-linux-armv7l.zip +14a285843587f251455a3ac69be5bebca7e7c3e934151a69dc8c10c943aaac49 *chromedriver-v28.2.5-linux-x64.zip +173112b71f363f1c434eb4bfe8356a5a4592a0580d8c434c2141f3a04de7695b *chromedriver-v28.2.5-mas-arm64.zip +b72902d8f4d886fef3f945e4a9dd707e18d52201a57e421a555cc166689955a7 *chromedriver-v28.2.5-mas-x64.zip +723cc0db4299d23c6be611b723187c857102749de2f2294bae09047b0d99cfd2 *chromedriver-v28.2.5-win32-arm64.zip +1c549de92e2d784cc2a2618d129e368d74e8da6497df7f5bcabfd2f834981f5d *chromedriver-v28.2.5-win32-ia32.zip +2df3c811c3ed8f22f28e740ffe0abf7c6d0c29d1874efa5290b75575a23d292b *chromedriver-v28.2.5-win32-x64.zip +a6e536d48e399f0961cb5de1e9cb0d3e534c4686fdf6fc79080e66516fdd5b6e *electron-api.json +746c5867227538235cff139e174a7b85fa49230a69350414bed7d1e6ae664cba *electron-v28.2.5-darwin-arm64-dsym-snapshot.zip +b5d00927dead894355c26cc581443735c252a71a53a363f3909f02b39ba1a38f *electron-v28.2.5-darwin-arm64-dsym.zip +6bb1356b72b5d3f8c3d25ef3f42a9ab8574498ab79299af056d8ac93972de72d *electron-v28.2.5-darwin-arm64-symbols.zip +87b17c403d355ba2eee43ee3a955c02069571617ef081b951272c1337ed5a2bb *electron-v28.2.5-darwin-arm64.zip +ff8b7d3073bbc1f26d83a224a79def7cfa652318b98d513603ac3d6e3ea56905 *electron-v28.2.5-darwin-x64-dsym-snapshot.zip +26057699098b6a4173c3f2550ec9a08d3edf4ce3aab5b351ca41c056f8f5ea5f *electron-v28.2.5-darwin-x64-dsym.zip +a467b38526c2c6c677dfe71898eaa8f3c8fccd7675bddfbfce6b095ebd66ea62 *electron-v28.2.5-darwin-x64-symbols.zip +a1ed37b654c48afe0fa8411d09b8644e121fa448d9cafa2ee6a3d2f5d8d36a4d *electron-v28.2.5-darwin-x64.zip +e28c071288258dd55ce0ad6c8582ad1db894a7e981dc2f84534d942289ba0a8b *electron-v28.2.5-linux-arm64-debug.zip +cebd2961e8af1600ca307f530c8b89dafa934cfed356830f4b5c04c048ffc204 *electron-v28.2.5-linux-arm64-symbols.zip +2a24355c27b5d43a424caedfcfe3fc42aeea80e13977fd708537519d6850c372 *electron-v28.2.5-linux-arm64.zip +e28c071288258dd55ce0ad6c8582ad1db894a7e981dc2f84534d942289ba0a8b *electron-v28.2.5-linux-armv7l-debug.zip +de46cbbe2c3eb4cd7d761ec57e85cd2077592b88586e1d9f45606df69a1e5dab *electron-v28.2.5-linux-armv7l-symbols.zip +2707f9fb7b7c6ea8038f8c67054cdfee4fb0bbaec36c20f3ffcca05cd5bbcd6c *electron-v28.2.5-linux-armv7l.zip +2567949ac356f53a5f145e6efaef1e1bc07dabfafa549765ebca54b33b0c298b *electron-v28.2.5-linux-x64-debug.zip +b067d8dfd0345129172628575684c7bc3d739843662aecc33ce4221a96fb7d48 *electron-v28.2.5-linux-x64-symbols.zip +1c1e972fa7daa54e1e54b642b2828495020367df5dcf1e3a4d4fc170980a8d6e *electron-v28.2.5-linux-x64.zip +23068f0cad14769f715147d1778da27a1850cdeea20c1ff7c7b1eb729b4d87b0 *electron-v28.2.5-mas-arm64-dsym-snapshot.zip +04640b64a0132f4606d31b89f8fa063b12be8936f0a2c546e05a992f23d00260 *electron-v28.2.5-mas-arm64-dsym.zip +cf1787c50932ef5c5309a8d80d7647495ddac4c71b6e0feb8ea547299c114ed4 *electron-v28.2.5-mas-arm64-symbols.zip +7f2339a92defc1808714bcdd0561430898e6a9f0cb8788a91bf178c10228fb2a *electron-v28.2.5-mas-arm64.zip +26eca4afd370e422c911c68268cf668ca43c10617c4e9cd53eaa564e698efd50 *electron-v28.2.5-mas-x64-dsym-snapshot.zip +ab928fcd3851651d9ef62fe4b62b48471a6673c603b70ca7f4049e72ad3bc2c2 *electron-v28.2.5-mas-x64-dsym.zip +c64232513b56b5b82f76b637a04f68fcfb7231ea8076d6dc1375aac8dac4a02c *electron-v28.2.5-mas-x64-symbols.zip +8d3a0988e699a42482079676ffba2ac50fbd74f1097d40cee0029ff5bc189144 *electron-v28.2.5-mas-x64.zip +7fcc54dc77dedbf4cb9eeaba1678ebf265649479c478e344c6dff27b1ec63b0b *electron-v28.2.5-win32-arm64-pdb.zip +31a69a6ed4e71b4fb047d19522dc32f9ff0f4b617536dcb7e8b562c9c583adbf *electron-v28.2.5-win32-arm64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-arm64-toolchain-profile.zip +68a6fc3411daaf604da0009df506a655587cb6e5cc19d6a1c47ce0b62fdb4ac6 *electron-v28.2.5-win32-arm64.zip +e74519411a678a9885bfb07acb5df85632f3de67d2fc54ccbd5ebd548edf84c8 *electron-v28.2.5-win32-ia32-pdb.zip +798261cfed077ec4afc965ab1a8e3fe4c976533ba86fa8f17cd69107bd1be3be *electron-v28.2.5-win32-ia32-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-ia32-toolchain-profile.zip +41f23f86bf7aa19f67025af7db221b727a38ffc0b1c5661b305be7250ecb7abc *electron-v28.2.5-win32-ia32.zip +ee882c550a2889dd18f58bf0f5c5ee9a1dd0eb6ed29c4f9a359b0db5314d7965 *electron-v28.2.5-win32-x64-pdb.zip +197b7ab2ba961ded3260ae91cf57502c43c2cdaca3fc18b90ec6f4d4e08ba9ac *electron-v28.2.5-win32-x64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-x64-toolchain-profile.zip +9235e039183fd62a6d37f70ae39a0f3a3ddb4e00e1474e6258343d1ad955c995 *electron-v28.2.5-win32-x64.zip +981a6c4d1030af6949405c3818b7332a16a959bd30970f5660e4975ccdf31789 *electron.d.ts +90b6c39e1ba7bbf0bccc3e009bcdbd4d8a57821f669229ab7847fd3d56cc8a66 *ffmpeg-v28.2.5-darwin-arm64.zip +3a736fed82b232624aeba8a33c02e1ce589214db0faf5984e216f8a72cbf713a *ffmpeg-v28.2.5-darwin-x64.zip +1674cdc15b72fb421ae4fd5afb217ef8968fb879db391343519764e2e77edb41 *ffmpeg-v28.2.5-linux-arm64.zip +6db75c7fe794f2a2edf3ff9b0d8ad6157d132c89e36e42580594a26f56658ae5 *ffmpeg-v28.2.5-linux-armv7l.zip +7fed2646cf8cce5c6c1afe4214b2d1ea12c89ed379c4b2cdb06cdadb14ceb4f6 *ffmpeg-v28.2.5-linux-x64.zip +fb3649690c496f4c6f884ddf94a7ff518278f6140c2487dd652256f53ed2e3fa *ffmpeg-v28.2.5-mas-arm64.zip +c1fee5ef3a550a5e9a652e251e6ec3677d156610670b54489b5da0c6b4007179 *ffmpeg-v28.2.5-mas-x64.zip +6745a8816159bc980f1ccb4d28c2f02f70cb5e2faf6423e0924d890ca6353dd8 *ffmpeg-v28.2.5-win32-arm64.zip +e1046c0280a7833227963b43d468973d646bd38ae100a298bb28d6a229e723b2 *ffmpeg-v28.2.5-win32-ia32.zip +a189c9c2317f011735e6d1cb743a78536f45a41f18a16c81d5294ca933d9519a *ffmpeg-v28.2.5-win32-x64.zip +29a594a16cdf3585299e7c585bae2ea007e108a72a96b9e2a95d149e7dc381d6 *hunspell_dictionaries.zip +7c1263edb062c07c2fb589812788f70772629c1798abd1481205cf8cdb999120 *libcxx-objects-v28.2.5-linux-arm64.zip +fc75baa4308a58048fe846b9fe78bba362a34becc5bf32a776f15933f4beaedf *libcxx-objects-v28.2.5-linux-armv7l.zip +1bedd5f8f3c897b0ca3cb263620cc4a8fff7001fcd6318a12c2c4cd9e922b35f *libcxx-objects-v28.2.5-linux-x64.zip +f6f37ee5bf297959c4fdec9bb77637310e6c8a85c7defadcbd660507a9e63728 *libcxx_headers.zip +8849467a7e670355b9cab854d66a09d57e9d91e8881034b07eb71f9d9928eb18 *libcxxabi_headers.zip +ff875bb59ecc8bf01b618d61d4a8378e133ea2c2571653828c9ea08773f5d776 *mksnapshot-v28.2.5-darwin-arm64.zip +30c1f135220d783b08a70bc7992877431e320543837ce0d90102039a945023aa *mksnapshot-v28.2.5-darwin-x64.zip +289e6b5feabe9ea22c10fc0fd0afcde59670506df1af6f1e87dc4dab5cbada29 *mksnapshot-v28.2.5-linux-arm64-x64.zip +e857fe518df1063308514224f82d13ffc24bb661b22d9a8a10a915a69830037b *mksnapshot-v28.2.5-linux-armv7l-x64.zip +a14af21de32fbfdf5d40402b52e7ff4858682cf3958fd6898ea30df331164004 *mksnapshot-v28.2.5-linux-x64.zip +8531b3ed3b47ed0a34a317be2fd03a573ee38e719aeddeeb0a6b3d5c36268ffa *mksnapshot-v28.2.5-mas-arm64.zip +71c978fffa8cb4a3f13842a8eadcb29e0782a648492204ebba08edb23b1fa297 *mksnapshot-v28.2.5-mas-x64.zip +b1ce8db39866860a6853c9a8874224c757a2b3086a451b7b1c30511615457166 *mksnapshot-v28.2.5-win32-arm64-x64.zip +c9a7f82fcd320c52f111d18b7fe6aa9ec739f94a13a7e8e04d22e6706a889c4b *mksnapshot-v28.2.5-win32-ia32.zip +493d0eabacf33c9d51305cf40bf7590901eb4e38d53308a76ae05db5af0a8468 *mksnapshot-v28.2.5-win32-x64.zip diff --git a/cgmanifest.json b/cgmanifest.json index 4b4d49573e4..e0a02fcc97d 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "01303e423c41f9fefe7ff777744a4c549c0c6d8c" + "commitHash": "14d11e5bb9b5b1cd51f7b19546e74a73cab42084" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "120.0.6099.276" + "version": "120.0.6099.291" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "16adf2a26358e3fc2297832e867c942b6df35844" + "commitHash": "6544cec6864be60f577c1fcd41fa646c4d0192aa" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "28.2.2" + "version": "28.2.5" }, { "component": { diff --git a/package.json b/package.json index 44e075ebd81..2afb762ff12 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "aa80bd2351f35faf630bcb132a08b10281943951", + "distro": "4623345215aabf2cde23e144a9d4d3ef7803360e", "author": { "name": "Microsoft Corporation" }, @@ -149,7 +149,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "28.2.2", + "electron": "28.2.5", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 807bb601136..e46474de06c 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -975,6 +975,13 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + type appWithProxySupport = Electron.App & { + setProxy(config: Electron.Config): Promise; + resolveProxy(url: string): Promise; + }; + if (typeof (app as appWithProxySupport).setProxy === 'function') { + (app as appWithProxySupport).setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + } } } } diff --git a/yarn.lock b/yarn.lock index 4dac8ed481a..c96688f58da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3563,10 +3563,10 @@ electron-to-chromium@^1.4.648: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== -electron@28.2.2: - version "28.2.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.2.tgz#d5aa4a33c00927d83ca893f8726f7c62aad98c41" - integrity sha512-8UcvIGFcjplHdjPFNAHVFg5bS0atDyT3Zx21WwuE4iLfxcAMsyMEOgrQX3im5LibA8srwsUZs7Cx0JAUfcQRpw== +electron@28.2.5: + version "28.2.5" + resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.5.tgz#d8e85306e8c51456042223a51f560f6ada565dc8" + integrity sha512-qlvQkDNVAzN647NpiJJw7GYJqE0NwK4+1evkhrQ0Xv6Qgab1EtN50G4oDr4/x/+O5pGUG2P5d3isXu+37O3RDw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18" From f022b12d428258e06c5f0fa5b3fad5f9559dea73 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:48:33 -0800 Subject: [PATCH 195/228] Move export from terminalContrib lint suppress into terminal/ --- .../chat/electron-sandbox/actions/voiceChatActions.ts | 5 +---- .../contrib/terminal/browser/terminalContribExports.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/contrib/terminal/browser/terminalContribExports.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 c64b66f6574..4f52090cc6e 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -52,11 +52,8 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ThemeIcon } from 'vs/base/common/themables'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; - -// This is a one-off/safe import, changing the eslint rules would require duplicating/complicating the rules -// eslint-disable-next-line local/code-import-patterns -import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; import { ProgressLocation } from 'vs/platform/progress/common/progress'; +import { TerminalChatController } from 'vs/workbench/contrib/terminal/browser/terminalContribExports'; 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.") }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts new file mode 100644 index 00000000000..86677966a9b --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalContribExports.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This is a one-off/safe import, to expose to outside contfibs as in general we don't want them +// to touch terminalContrib either. +// eslint-disable-next-line local/code-import-patterns +export { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController'; From bd603172efb3d8bd03d7c9233dff2dc17248dc06 Mon Sep 17 00:00:00 2001 From: Sam Denty Date: Mon, 4 Mar 2024 20:17:04 +0000 Subject: [PATCH 196/228] feat(web/lifecycleService): correct startupKind (#206563) * fixes #206345 * fixes #206296 * fix: don't hard check isWeb * chore: review * cleanup * cleanup --------- Co-authored-by: Benjamin Pasero --- src/vs/workbench/browser/layout.ts | 4 ++-- .../lifecycle/browser/lifecycleService.ts | 16 +++++++++++++++- .../lifecycle/common/lifecycleService.ts | 13 ++++++++----- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index b71d9dd9477..6cb0a546797 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -671,7 +671,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Only restore last viewlet if window was reloaded or we are in development mode let viewContainerToRestore: string | undefined; - if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow || isWeb) { + if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow) { viewContainerToRestore = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); } else { viewContainerToRestore = this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id; @@ -2608,7 +2608,7 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === 'bottom' ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === Position.BOTTOM ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index c257d6ccc42..351cad96bc7 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService, StartupKind } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycleService'; import { localize } from 'vs/nls'; @@ -13,6 +13,7 @@ import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IStorageService, WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { CancellationToken } from 'vs/base/common/cancellation'; import { mainWindow } from 'vs/base/browser/window'; +import { firstOrDefault } from 'vs/base/common/arrays'; export class BrowserLifecycleService extends AbstractLifecycleService { @@ -200,6 +201,19 @@ export class BrowserLifecycleService extends AbstractLifecycleService { // Refs: https://github.com/microsoft/vscode/issues/136035 this.withExpectedShutdown({ disableShutdownHandling: true }, () => mainWindow.location.reload()); } + + protected override doResolveStartupKind(): StartupKind | undefined { + let startupKind = super.doResolveStartupKind(); + if (typeof startupKind !== 'number') { + const timing = firstOrDefault(performance.getEntriesByType('navigation')) as PerformanceNavigationTiming | undefined; + if (timing?.type === 'reload') { + // MDN: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type#value + startupKind = StartupKind.ReloadedWindow; + } + } + + return startupKind; + } } registerSingleton(ILifecycleService, BrowserLifecycleService, InstantiationType.Eager); diff --git a/src/vs/workbench/services/lifecycle/common/lifecycleService.ts b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts index 6aa7358deda..62aa7db5201 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts @@ -60,13 +60,20 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi } private resolveStartupKind(): StartupKind { + const startupKind = this.doResolveStartupKind() ?? StartupKind.NewWindow; + this.logService.trace(`[lifecycle] starting up (startup kind: ${startupKind})`); + + return startupKind; + } + + protected doResolveStartupKind(): StartupKind | undefined { // Retrieve and reset last shutdown reason const lastShutdownReason = this.storageService.getNumber(AbstractLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE); this.storageService.remove(AbstractLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE); // Convert into startup kind - let startupKind: StartupKind; + let startupKind: StartupKind | undefined = undefined; switch (lastShutdownReason) { case ShutdownReason.RELOAD: startupKind = StartupKind.ReloadedWindow; @@ -74,12 +81,8 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi case ShutdownReason.LOAD: startupKind = StartupKind.ReopenedWindow; break; - default: - startupKind = StartupKind.NewWindow; } - this.logService.trace(`[lifecycle] starting up (startup kind: ${startupKind})`); - return startupKind; } From 08185051c341a02e3eb09ad559f573752ff255cf Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 4 Mar 2024 12:37:57 -0800 Subject: [PATCH 197/228] rm merge marker, fix css bug --- .../contrib/chat/electron-sandbox/actions/voiceChatActions.ts | 1 - 1 file changed, 1 deletion(-) 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 4f52090cc6e..82b91bb2073 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -826,7 +826,6 @@ registerThemingParticipant((theme, collector) => { // 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), -<<<<<<< HEAD .monaco-workbench:not(.reduce-motion) .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled), .monaco-workbench:not(.reduce-motion) .terminal-inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled) { color: ${activeRecordingColor}; From 62b22bbe6103f6a35de6cf5141ae26450ae4d507 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 21:59:51 +0100 Subject: [PATCH 198/228] `code --install-extension` not working with roaming profiles (fix #205924) (#206834) --- src/vs/code/electron-main/main.ts | 16 ++++++++++++++-- src/vs/code/node/cliProcessMain.ts | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index c5466e20b45..8548fa7ce4d 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -71,6 +71,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; import { SaveStrategy, StateService } from 'vs/platform/state/node/stateService'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; /** * The main VS Code entry point. @@ -249,8 +250,8 @@ class CodeMain { // Environment service (paths) Promise.all([ - environmentMainService.extensionsPath, - environmentMainService.codeCachePath, + this.allowWindowsUNCPath(environmentMainService.extensionsPath), // enable extension paths on UNC drives... + environmentMainService.codeCachePath, // ...other user-data-derived paths should already be enlisted from `main.js` environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath, userDataProfilesMainService.defaultProfile.globalStorageHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, @@ -269,6 +270,17 @@ class CodeMain { userDataProfilesMainService.init(); } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private async claimInstance(logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, productService: IProductService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index b91367f1fc2..aea83578ee0 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -63,6 +63,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { localize } from 'vs/nls'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; class CliMain extends Disposable { @@ -121,8 +122,8 @@ class CliMain extends Disposable { // Init folders await Promise.all([ - environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath, - environmentService.extensionsPath + this.allowWindowsUNCPath(environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath), + this.allowWindowsUNCPath(environmentService.extensionsPath) ].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); // Logger @@ -233,6 +234,17 @@ class CliMain extends Disposable { return [new InstantiationService(services), appenders]; } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private registerErrorHandler(logService: ILogService): void { // Install handler for unexpected errors From e2f64dd1ab1abac713ce996b3ceedf805a7e7005 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 22:16:13 +0100 Subject: [PATCH 199/228] Aux window: investigate possibly memory leaks (#195889) (#206791) --- src/vs/workbench/browser/layout.ts | 7 +++++-- src/vs/workbench/browser/part.ts | 2 +- .../workbench/browser/parts/banner/bannerPart.ts | 2 +- .../browser/parts/statusbar/statusbarPart.ts | 2 +- .../browser/parts/titlebar/titlebarPart.ts | 4 ++-- .../browser/parts/titlebar/windowTitle.ts | 14 ++++++++++---- .../contrib/webviewPanel/browser/webviewEditor.ts | 2 +- .../parts/titlebar/titlebarPart.ts | 4 ++-- .../services/layout/browser/layoutService.ts | 3 ++- .../test/browser/workbenchTestServices.ts | 2 +- 10 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 6cb0a546797..9e397b31a21 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1089,8 +1089,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }); } - registerPart(part: Part): void { - this.parts.set(part.getId(), part); + registerPart(part: Part): IDisposable { + const id = part.getId(); + this.parts.set(id, part); + + return toDisposable(() => this.parts.delete(id)); } protected getPart(key: Parts): Part { diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index e78f59839be..f015fdbdf53 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -50,7 +50,7 @@ export abstract class Part extends Component implements ISerializableView { ) { super(id, themeService, storageService); - layoutService.registerPart(this); + this._register(layoutService.registerPart(this)); } protected override onThemeChange(theme: IColorTheme): void { diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 3725e594c97..91c8e9902bb 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -86,7 +86,7 @@ export class BannerPart extends Part implements IBannerService { })); // Track focus - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); BannerFocused.bindTo(scopedContextKeyService).set(true); return this.element; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 664a333bb16..06548d13508 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -338,7 +338,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.element = parent; // Track focus within container - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); StatusBarFocused.bindTo(scopedContextKeyService).set(true); // Left items container diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 548f4d36caf..110d04fba11 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -49,7 +49,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { EditorCommandsContextActionRunner } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { IEditorCommandsContext, IEditorPartOptionsChangeEvent, IToolbarActions } from 'vs/workbench/common/editor'; -import { mainWindow } from 'vs/base/browser/window'; +import { CodeWindow, mainWindow } from 'vs/base/browser/window'; import { ACCOUNTS_ACTIVITY_TILE_ACTION, GLOBAL_ACTIVITY_TITLE_ACTION } from 'vs/workbench/browser/parts/titlebar/titlebarActions'; import { IView } from 'vs/base/browser/ui/grid/grid'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; @@ -258,7 +258,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { constructor( id: string, - targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConfigurationService protected readonly configurationService: IConfigurationService, diff --git a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 3ec39eeafc2..e025f5c4d6c 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -27,6 +27,8 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { getWindowById } from 'vs/base/browser/dom'; +import { CodeWindow } from 'vs/base/browser/window'; const enum WindowSettingNames { titleSeparator = 'window.titleSeparator', @@ -79,8 +81,10 @@ export class WindowTitle extends Disposable { private readonly editorService: IEditorService; + private readonly windowId: number; + constructor( - private readonly targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IConfigurationService protected readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -95,6 +99,7 @@ export class WindowTitle extends Disposable { super(); this.editorService = editorService.createScoped(editorGroupsContainer, this._store); + this.windowId = targetWindow.vscodeWindowId; this.updateTitleIncludesFocusedView(); this.registerListeners(); @@ -177,7 +182,8 @@ export class WindowTitle extends Disposable { nativeTitle = this.productService.nameLong; } - if (!this.targetWindow.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { + const window = getWindowById(this.windowId, true).window; + if (!window.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { // TODO@electron macOS: if we set a window title for // the first time and it matches the one we set in // `windowImpl.ts` somehow the window does not appear @@ -185,10 +191,10 @@ export class WindowTitle extends Disposable { // briefly to something different to ensure macOS // recognizes we have a window. // See: https://github.com/microsoft/vscode/issues/191288 - this.targetWindow.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; + window.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; } - this.targetWindow.document.title = nativeTitle; + window.document.title = nativeTitle; this.title = title; this.onDidChangeEmitter.fire(); diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index 33ad2d64e7d..67fbfb9e4dc 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts @@ -91,7 +91,7 @@ export class WebviewEditor extends EditorPane { this._element.id = `webview-editor-element-${generateUuid()}`; parent.appendChild(element); - this._scopedContextKeyService.value = this._contextKeyService.createScoped(element); + this._scopedContextKeyService.value = this._register(this._contextKeyService.createScoped(element)); } public override dispose(): void { diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 5d989fc5e1b..4e4e8f5b7d0 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -26,7 +26,7 @@ import { NativeMenubarControl } from 'vs/workbench/electron-sandbox/parts/titleb import { IEditorGroupsContainer, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { mainWindow } from 'vs/base/browser/window'; +import { CodeWindow, mainWindow } from 'vs/base/browser/window'; export class NativeTitlebarPart extends BrowserTitlebarPart { @@ -59,7 +59,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { constructor( id: string, - targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 0a397839220..68dcd16ab14 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -14,6 +14,7 @@ import { isAuxiliaryWindow } from 'vs/base/browser/window'; import { CustomTitleBarVisibility, TitleBarSetting, getMenuBarVisibility, hasCustomTitlebar, hasNativeTitlebar } from 'vs/platform/window/common/window'; import { isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const IWorkbenchLayoutService = refineServiceDecorator(ILayoutService); @@ -291,7 +292,7 @@ export interface IWorkbenchLayoutService extends ILayoutService { /** * Register a part to participate in the layout. */ - registerPart(part: Part): void; + registerPart(part: Part): IDisposable; /** * Returns whether the target window is maximized. diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 9cb63bb2b63..6fcf99d03f3 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -638,7 +638,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { isMainEditorLayoutCentered(): boolean { return false; } centerMainEditorLayout(_active: boolean): void { } resizePart(_part: Parts, _sizeChangeWidth: number, _sizeChangeHeight: number): void { } - registerPart(part: Part): void { } + registerPart(part: Part): IDisposable { return Disposable.None; } isWindowMaximized(targetWindow: Window) { return false; } updateWindowMaximizedState(targetWindow: Window, maximized: boolean): void { } getVisibleNeighborPart(part: Parts, direction: Direction): Parts | undefined { return undefined; } From 8f062388605e48a3b8d5cbe5eb30c7cc7f2e8558 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Mon, 4 Mar 2024 13:33:43 -0800 Subject: [PATCH 200/228] fix context key issue for accessible view --- .../chat/browser/terminalChatAccessibleView.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index c8b11add5be..f5bbe82de03 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -4,11 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; 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 { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; 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'; @@ -35,6 +33,6 @@ export class TerminalInlineChatAccessibleViewContribution extends Disposable { options: { type: AccessibleViewType.View } }); return true; - }, ContextKeyExpr.and(TerminalChatContextKeys.focused, CTX_INLINE_CHAT_RESPONSE_FOCUSED))); + }, TerminalChatContextKeys.focused)); } } From a5abe07cea11b188843780685b47b0de874b48bf Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 4 Mar 2024 13:40:39 -0800 Subject: [PATCH 201/228] Remove buffers that shouldn't be validated from `geterr` request (#206843) Fixes #206644 --- .../src/tsServer/bufferSyncSupport.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index dc5948314d9..9f5d76f5ac3 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -725,6 +725,13 @@ export default class BufferSyncSupport extends Disposable { orderedFileSet.set(buffer.resource, undefined); } + for (const { resource } of orderedFileSet.entries()) { + const buffer = this.syncedBuffers.get(resource); + if (buffer && !this.shouldValidate(buffer)) { + orderedFileSet.delete(resource); + } + } + if (orderedFileSet.size) { const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, orderedFileSet, () => { if (this.pendingGetErr === getErr) { From 544094579bac7296ac14ab42c03d5790507f33c3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 22:46:40 +0100 Subject: [PATCH 202/228] shared process logging (for #206522) (#206844) --- .../node/sharedProcess/sharedProcessMain.ts | 19 ++++++++++---- .../electron-main/sharedProcess.ts | 25 +++++++++++-------- .../electron-main/utilityProcess.ts | 11 +++++--- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 3c0de38db43..81faf87e87b 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -519,15 +519,24 @@ export async function main(configuration: ISharedProcessConfiguration): Promise< // create shared process and signal back to main that we are // ready to accept message ports as client connections - const sharedProcess = new SharedProcessMain(configuration); - process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); + try { + const sharedProcess = new SharedProcessMain(configuration); + process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); - // await initialization and signal this back to electron-main - await sharedProcess.init(); + // await initialization and signal this back to electron-main + await sharedProcess.init(); - process.parentPort.postMessage(SharedProcessLifecycle.initDone); + process.parentPort.postMessage(SharedProcessLifecycle.initDone); + } catch (error) { + process.parentPort.postMessage({ error: error.toString() }); + } } +const handle = setTimeout(() => { + process.parentPort.postMessage({ warning: '[SharedProcess] did not receive configuration within 30s...' }); +}, 30000); + process.parentPort.once('message', (e: Electron.MessageEvent) => { + clearTimeout(handle); main(e.data as ISharedProcessConfiguration); }); diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 66defd005d1..087f5858b48 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -6,7 +6,7 @@ import { IpcMainEvent, MessagePortMain } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier, DeferredPromise } from 'vs/base/common/async'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -25,6 +25,7 @@ export class SharedProcess extends Disposable { private readonly firstWindowConnectionBarrier = new Barrier(); private utilityProcess: UtilityProcess | undefined = undefined; + private utilityProcessLogListener: IDisposable | undefined = undefined; constructor( private readonly machineId: string, @@ -104,13 +105,10 @@ export class SharedProcess extends Disposable { // all services within have been created. const whenReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); await whenReady.p; + this.utilityProcessLogListener?.dispose(); this.logService.trace('[SharedProcess] Overall ready'); })(); } @@ -131,11 +129,7 @@ export class SharedProcess extends Disposable { // Wait for shared process indicating that IPC connections are accepted const sharedProcessIpcReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); await sharedProcessIpcReady.p; this.logService.trace('[SharedProcess] IPC ready'); @@ -148,6 +142,15 @@ export class SharedProcess extends Disposable { private createUtilityProcess(): void { this.utilityProcess = this._register(new UtilityProcess(this.logService, NullTelemetryService, this.lifecycleMainService)); + // Install a log listener for very early shared process warnings and errors + this.utilityProcessLogListener = this.utilityProcess.onMessage((e: any) => { + if (typeof e.warning === 'string') { + this.logService.warn(e.warning); + } else if (typeof e.error === 'string') { + this.logService.error(e.error); + } + }); + const inspectParams = parseSharedProcessDebugPort(this.environmentMainService.args, this.environmentMainService.isBuilt); let execArgv: string[] | undefined = undefined; if (inspectParams.port) { diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 6bd1d52b986..8386e94dab2 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -212,7 +212,10 @@ export class UtilityProcess extends Disposable { const started = this.doStart(configuration); if (started && configuration.payload) { - this.postMessage(configuration.payload); + const posted = this.postMessage(configuration.payload); + if (posted) { + this.log('payload sent via postMessage()', Severity.Info); + } } return started; @@ -363,12 +366,14 @@ export class UtilityProcess extends Disposable { })); } - postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): void { + postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): boolean { if (!this.process) { - return; // already killed, crashed or never started + return false; // already killed, crashed or never started } this.process.postMessage(message, transfer); + + return true; } connect(payload?: unknown): Electron.MessagePortMain { From 663376e32d06bf1bf201eb0a01ea5b370ad12b59 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 4 Mar 2024 19:01:13 -0300 Subject: [PATCH 203/228] Move more stuff from interactive to participants (#206647) * Remove 'inputPlaceholder' from InteractiveSession * Get rid of 'responder' on InteractiveSession * Remove 'requester' details from interactive session provider, move to default agent * Set up default chat participant to make this test pass * Update error --- .../workbench/api/browser/mainThreadChat.ts | 8 --- .../workbench/api/common/extHost.protocol.ts | 5 -- src/vs/workbench/api/common/extHostChat.ts | 5 -- .../api/common/extHostChatAgents2.ts | 13 +++- .../contrib/chat/browser/chatListRenderer.ts | 7 +- .../browser/contrib/chatInputEditorContrib.ts | 5 +- .../contrib/chat/common/chatAgents.ts | 10 +++ .../contrib/chat/common/chatModel.ts | 64 +++++++++++-------- .../contrib/chat/common/chatService.ts | 5 -- .../contrib/chat/common/chatServiceImpl.ts | 4 +- .../contrib/chat/common/chatViewModel.ts | 25 ++++---- .../__snapshots__/Chat_can_deserialize.0.snap | 3 +- .../__snapshots__/Chat_can_serialize.0.snap | 4 +- .../__snapshots__/Chat_can_serialize.1.snap | 3 +- .../chat/test/common/chatService.test.ts | 6 +- .../inlineChat/browser/inlineChatWidget.ts | 2 +- .../test/browser/inlineChatController.test.ts | 15 +++++ ...scode.proposed.defaultChatParticipant.d.ts | 10 +++ .../vscode.proposed.interactive.d.ts | 12 ---- 19 files changed, 115 insertions(+), 91 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index e70553d866f..1a4d657cd94 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -55,18 +55,10 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { return undefined; } - const responderAvatarIconUri = session.responderAvatarIconUri && - URI.revive(session.responderAvatarIconUri); - const emitter = new Emitter(); this._stateEmitters.set(session.id, emitter); return { id: session.id, - requesterUsername: session.requesterUsername, - requesterAvatarIconUri: URI.revive(session.requesterAvatarIconUri), - responderUsername: session.responderUsername, - responderAvatarIconUri, - inputPlaceholder: session.inputPlaceholder, dispose: () => { emitter.dispose(); this._stateEmitters.delete(session.id); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 575c58a4d24..fb881d05c4f 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1277,11 +1277,6 @@ export interface MainThreadUrlsShape extends IDisposable { export interface IChatDto { id: number; - requesterUsername: string; - requesterAvatarIconUri?: UriComponents; - responderUsername: string; - responderAvatarIconUri?: UriComponents; - inputPlaceholder?: string; } export interface IChatRequestDto { diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index 9b806f07947..036d64b14d2 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -74,11 +74,6 @@ export class ExtHostChat implements ExtHostChatShape { return { id, - requesterUsername: session.requester?.name, - requesterAvatarIconUri: session.requester?.icon, - responderUsername: session.responder?.name, - responderAvatarIconUri: session.responder?.icon, - inputPlaceholder: session.inputPlaceholder, }; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 07a808d31b9..7a4c69dc8f3 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -213,7 +213,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { } catch (e) { this._logService.error(e, agent.extension); - return { errorDetails: { message: localize('errorResponse', "Error from provider: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; + return { errorDetails: { message: localize('errorResponse', "Error from participant: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; } finally { stream.close(); @@ -353,6 +353,7 @@ class ExtHostChatAgent { private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; private _isSticky: boolean | undefined; + private _requester: vscode.ChatRequesterInformation | undefined; constructor( public readonly extension: IExtensionDescription, @@ -436,7 +437,7 @@ class ExtHostChatAgent { updateScheduled = true; queueMicrotask(() => { this._proxy.$updateAgent(this._handle, { - description: this._description ?? '', + description: this._description, fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : @@ -454,6 +455,7 @@ class ExtHostChatAgent { sampleRequest: this._sampleRequest, supportIssueReporting: this._supportIssueReporting, isSticky: this._isSticky, + requester: this._requester }); updateScheduled = false; }); @@ -602,6 +604,13 @@ class ExtHostChatAgent { that._isSticky = v; updateMetadataSoon(); }, + set requester(v) { + that._requester = v; + updateMetadataSoon(); + }, + get requester() { + return that._requester; + }, dispose() { disposed = true; that._followupProvider = undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 94b0a9b2dc1..f97e96548ff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -384,13 +384,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIconUri).toString(true); + avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIcon).toString(true); templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarImgIcon)); } else { const defaultIcon = isRequestVM(element) ? Codicon.account : Codicon.copilot; - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(defaultIcon)); + const icon = element.avatarIcon ?? defaultIcon; + const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 287db1631ac..0bc244b4cb4 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -124,8 +124,7 @@ class InputEditorDecorations extends Disposable { } if (!inputValue) { - const viewModelPlaceholder = this.widget.viewModel?.inputPlaceholder; - const placeholder = viewModelPlaceholder ?? ''; + const defaultAgent = this.chatAgentService.getDefaultAgent(); const decoration: IDecorationOptions[] = [ { range: { @@ -136,7 +135,7 @@ class InputEditorDecorations extends Disposable { }, renderOptions: { after: { - contentText: placeholder, + contentText: viewModel.inputPlaceholder ?? defaultAgent?.metadata.description ?? '', color: this.getPlaceholderColor() } } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 2d9318102b8..46854de6a5e 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -71,6 +71,15 @@ export interface IChatAgentCommand { sampleRequest?: string; } +export interface IChatRequesterInformation { + name: string; + + /** + * A full URI for the icon of the requester. + */ + icon?: URI; +} + export interface IChatAgentMetadata { description?: string; helpTextPrefix?: string | IMarkdownString; @@ -85,6 +94,7 @@ export interface IChatAgentMetadata { supportIssueReporting?: boolean; followupPlaceholder?: string; isSticky?: boolean; + requester?: IChatRequesterInformation; } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index ed06c32958f..a2c05d68ca0 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -10,9 +10,11 @@ import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/commo import { Disposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { basename } from 'vs/base/common/resources'; -import { URI, UriComponents, UriDto } from 'vs/base/common/uri'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI, UriComponents, UriDto, isUriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { 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'; @@ -54,7 +56,7 @@ export interface IChatResponseModel { readonly providerId: string; readonly requestId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon | URI; readonly session: IChatModel; readonly agent?: IChatAgentData; readonly usedContext: IChatUsedContext | undefined; @@ -234,8 +236,8 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this.session.responderUsername; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | URI | undefined { + return this.session.responderAvatarIcon; } private _followups?: IChatFollowup[]; @@ -392,7 +394,7 @@ export interface IExportableChatData { requesterUsername: string; responderUsername: string; requesterAvatarIconUri: UriComponents | undefined; - responderAvatarIconUri: UriComponents | undefined; + responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat } export interface ISerializableChatData extends IExportableChatData { @@ -405,8 +407,7 @@ export function isExportableSessionData(obj: unknown): obj is IExportableChatDat const data = obj as IExportableChatData; return typeof data === 'object' && typeof data.providerId === 'string' && - typeof data.requesterUsername === 'string' && - typeof data.responderUsername === 'string'; + typeof data.requesterUsername === 'string'; } export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData { @@ -483,10 +484,6 @@ export class ChatModel extends Disposable implements IChatModel { return this._sessionId; } - get inputPlaceholder(): string | undefined { - return this._session?.inputPlaceholder; - } - get requestInProgress(): boolean { const lastRequest = this._requests[this._requests.length - 1]; return !!lastRequest && !!lastRequest.response && !lastRequest.response.isComplete; @@ -497,22 +494,34 @@ export class ChatModel extends Disposable implements IChatModel { return this._creationDate; } + private get _defaultAgent() { + return this.chatAgentService.getDefaultAgent(); + } + get requesterUsername(): string { - return this._session?.requesterUsername ?? this.initialData?.requesterUsername ?? ''; + return (this._defaultAgent ? + this._defaultAgent.metadata.requester?.name : + this.initialData?.requesterUsername) ?? ''; } get responderUsername(): string { - return this._session?.responderUsername ?? this.initialData?.responderUsername ?? ''; + return (this._defaultAgent ? + this._defaultAgent.metadata.fullName : + this.initialData?.responderUsername) ?? ''; } private readonly _initialRequesterAvatarIconUri: URI | undefined; get requesterAvatarIconUri(): URI | undefined { - return this._session ? this._session.requesterAvatarIconUri : this._initialRequesterAvatarIconUri; + return this._defaultAgent ? + this._defaultAgent.metadata.requester?.icon : + this._initialRequesterAvatarIconUri; } - private readonly _initialResponderAvatarIconUri: URI | undefined; - get responderAvatarIconUri(): URI | undefined { - return this._session ? this._session.responderAvatarIconUri : this._initialResponderAvatarIconUri; + private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined; + get responderAvatarIcon(): ThemeIcon | URI | undefined { + return this._defaultAgent ? + this._defaultAgent?.metadata.themeIcon : + this._initialResponderAvatarIconUri; } get initState(): ChatModelInitState { @@ -533,6 +542,7 @@ export class ChatModel extends Disposable implements IChatModel { private readonly initialData: ISerializableChatData | IExportableChatData | undefined, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -542,7 +552,7 @@ export class ChatModel extends Disposable implements IChatModel { this._creationDate = (isSerializableSessionData(initialData) && initialData.creationDate) || Date.now(); this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri); - this._initialResponderAvatarIconUri = initialData?.responderAvatarIconUri && URI.revive(initialData.responderAvatarIconUri); + this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri; } private _deserialize(obj: IExportableChatData): ChatRequestModel[] { @@ -554,7 +564,7 @@ export class ChatModel extends Disposable implements IChatModel { if (obj.welcomeMessage) { const content = obj.welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item); - this._welcomeMessage = new ChatWelcomeMessageModel(this, content, []); + this._welcomeMessage = this.instantiationService.createInstance(ChatWelcomeMessageModel, content, []); } try { @@ -748,7 +758,7 @@ export class ChatModel extends Disposable implements IChatModel { requesterUsername: this.requesterUsername, requesterAvatarIconUri: this.requesterAvatarIconUri, responderUsername: this.responderUsername, - responderAvatarIconUri: this.responderAvatarIconUri, + responderAvatarIconUri: this.responderAvatarIcon, welcomeMessage: this._welcomeMessage?.content.map(c => { if (Array.isArray(c)) { return c; @@ -782,7 +792,7 @@ export class ChatModel extends Disposable implements IChatModel { vote: r.response?.vote, agent: r.response?.agent ? // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. - { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [] } + { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], isDefault: r.response.agent.isDefault } : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, @@ -818,7 +828,7 @@ export interface IChatWelcomeMessageModel { readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon; } @@ -831,19 +841,19 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } constructor( - private readonly session: ChatModel, public readonly content: IChatWelcomeMessageContent[], - public readonly sampleQuestions: IChatFollowup[] + public readonly sampleQuestions: IChatFollowup[], + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { this._id = 'welcome_' + ChatWelcomeMessageModel.nextId++; } public get username(): string { - return this.session.responderUsername; + return this.chatAgentService.getDefaultAgent()?.metadata.fullName ?? ''; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | undefined { + return this.chatAgentService.getDefaultAgent()?.metadata.themeIcon; } } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 360d8ea0e34..c7b1a1eec48 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -19,11 +19,6 @@ import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chat export interface IChat { id: number; // TODO Maybe remove this and move to a subclass that only the provider knows about - requesterUsername: string; - requesterAvatarIconUri?: URI; - responderUsername: string; - responderAvatarIconUri?: URI; - inputPlaceholder?: string; dispose?(): void; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index e254395407f..ac232bc4dba 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -388,8 +388,8 @@ export class ChatService extends Disposable implements IChatService { } const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(token) ?? undefined; - const welcomeModel = welcomeMessage && new ChatWelcomeMessageModel( - model, + const welcomeModel = welcomeMessage && this.instantiationService.createInstance( + ChatWelcomeMessageModel, welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item), await defaultAgent.provideSampleQuestions?.(token) ?? [] ); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index fc1328ca803..fa329a908bd 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -6,6 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -65,7 +66,7 @@ export interface IChatRequestViewModel { /** This ID updates every time the underlying data changes */ readonly dataId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly message: IParsedChatRequest | IChatFollowup; readonly messageText: string; currentRenderedHeight: number | undefined; @@ -115,7 +116,7 @@ export interface IChatResponseViewModel { /** The ID of the associated IChatRequestViewModel */ readonly requestId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly agent?: IChatAgentData; readonly slashCommand?: IChatAgentCommand; readonly response: IResponse; @@ -150,7 +151,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private _inputPlaceholder: string | undefined = undefined; get inputPlaceholder(): string | undefined { - return this._inputPlaceholder ?? this._model.inputPlaceholder; + return this._inputPlaceholder; } get model(): IChatModel { @@ -192,7 +193,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { super(); _model.getRequests().forEach((request, i) => { - const requestModel = new ChatRequestViewModel(request); + const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, request); this._items.push(requestModel); this.updateCodeBlockTextModels(requestModel); @@ -204,7 +205,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { this._register(_model.onDidDispose(() => this._onDidDisposeModel.fire())); this._register(_model.onDidChange(e => { if (e.kind === 'addRequest') { - const requestModel = new ChatRequestViewModel(e.request); + const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, e.request); this._items.push(requestModel); this.updateCodeBlockTextModels(requestModel); @@ -348,7 +349,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.username; } - get avatarIconUri() { + get avatarIcon() { return this._model.avatarIconUri; } @@ -362,7 +363,9 @@ export class ChatRequestViewModel implements IChatRequestViewModel { currentRenderedHeight: number | undefined; - constructor(readonly _model: IChatRequestModel) { } + constructor( + readonly _model: IChatRequestModel, + ) { } } export class ChatResponseViewModel extends Disposable implements IChatResponseViewModel { @@ -391,8 +394,8 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.username; } - get avatarIconUri() { - return this._model.avatarIconUri; + get avatarIcon() { + return this._model.avatarIcon; } get agent() { @@ -484,7 +487,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi constructor( private readonly _model: IChatResponseModel, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, ) { super(); @@ -535,7 +538,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi export interface IChatWelcomeMessageViewModel { readonly id: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; currentRenderedHeight?: number; diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap index 9d4646eb6a9..cbd61e5811d 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap @@ -55,7 +55,8 @@ _lower: "nullextensiondescription" }, metadata: { description: undefined }, - slashCommands: [ ] + slashCommands: [ ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap index 0939983222f..75c5fa71f40 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap @@ -1,7 +1,7 @@ { - requesterUsername: "", + requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "", + responderUsername: "test", responderAvatarIconUri: undefined, welcomeMessage: undefined, requests: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap index 98d57a6bd2b..f231c781190 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap @@ -55,7 +55,8 @@ _lower: "nullextensiondescription" }, metadata: { description: undefined }, - slashCommands: [ ] + slashCommands: [ ], + isDefault: undefined }, slashCommand: undefined, usedContext: { 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 ab9ef913673..eb6bee1c3fa 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -46,8 +46,6 @@ class SimpleTestProvider extends Disposable implements IChatProvider { async prepareSession(): Promise { return { id: SimpleTestProvider.sessionId++, - responderUsername: 'test', - requesterUsername: 'test', }; } @@ -108,7 +106,7 @@ suite('Chat', () => { instantiationService.stub(IChatContributionService, new MockChatContributionService( [ { extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }, - { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId, isDefault: true }, + { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId }, ])); chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); @@ -120,6 +118,7 @@ suite('Chat', () => { }, } satisfies IChatAgentImplementation; testDisposables.add(chatAgentService.registerAgent('testAgent', agent)); + chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); }); test('retrieveSession', async () => { @@ -210,6 +209,7 @@ suite('Chat', () => { test('can serialize', async () => { testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); + chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5af8f2acc53..6f1cd922882 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -615,7 +615,7 @@ export class InlineChatWidget { this._ctxMessageCropState.reset(); expansionState = ExpansionState.NOT_CROPPED; } else { - const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService)); + const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService)); const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; 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 6fd5722bc72..276c86cb858 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -44,6 +44,10 @@ import { TestWorkerService } from './testWorkerService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { Schemas } from 'vs/base/common/network'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; suite('InteractiveChatController', function () { class TestController extends InlineChatController { @@ -113,6 +117,9 @@ suite('InteractiveChatController', function () { const serviceCollection = new ServiceCollection( [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], + [IChatContributionService, new MockChatContributionService( + [{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])], + [IChatAgentService, new SyncDescriptor(ChatAgentService)], [IInlineChatService, inlineChatService], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], @@ -146,6 +153,14 @@ suite('InteractiveChatController', function () { ); instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); + const chatAgentService = instaService.get(IChatAgentService); + const agent = { + async invoke(request, progress, history, token) { + return {}; + }, + } satisfies IChatAgentImplementation; + store.add(chatAgentService.registerAgent('testAgent', agent)); + inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); diff --git a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts index 07a2b6f5c42..377187539be 100644 --- a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts @@ -12,6 +12,15 @@ declare module 'vscode' { provideSampleQuestions?(token: CancellationToken): ProviderResult; } + export interface ChatRequesterInformation { + name: string; + + /** + * A full URI for the icon of the request. + */ + icon?: Uri; + } + export interface ChatParticipant { /** * When true, this participant is invoked by default when no other participant is being invoked @@ -45,5 +54,6 @@ declare module 'vscode' { helpTextPostfix?: string | MarkdownString; welcomeMessageProvider?: ChatWelcomeMessageProvider; + requester?: ChatRequesterInformation; } } diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 852f8830de2..bf9d7d26045 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -110,19 +110,7 @@ declare module 'vscode' { handleInteractiveEditorResponseFeedback?(session: S, response: R, kind: InteractiveEditorResponseFeedbackKind): void; } - export interface InteractiveSessionParticipantInformation { - name: string; - - /** - * A full URI for the icon of the participant. - */ - icon?: Uri; - } - export interface InteractiveSession { - requester: InteractiveSessionParticipantInformation; - responder: InteractiveSessionParticipantInformation; - inputPlaceholder?: string; } export interface InteractiveSessionProvider { From 5abb3084473d96ce0ef7ace38ccd3d8dd8e6a9f3 Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:26:18 -0600 Subject: [PATCH 204/228] Fix go to file button on quick search (#206846) --- .../browser/quickTextSearch/textSearchQuickAccess.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index c4e31c3c903..2c9474be1e9 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -15,9 +15,9 @@ import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; -import { FastAndSlowPicks, IPickerQuickAccessItem, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { FastAndSlowPicks, IPickerQuickAccessItem, IPickerQuickAccessSeparator, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { DefaultQuickAccessFilterValue, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; -import { IKeyMods, IQuickPick, IQuickPickItem, IQuickPickSeparator, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPick, IQuickPickItem, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { searchDetailsIcon, searchOpenInFileIcon, searchActivityBarIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -217,11 +217,11 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider limit ? matches.slice(0, limit) : matches; - const picks: Array = []; + const picks: Array = []; for (let fileIndex = 0; fileIndex < matches.length; fileIndex++) { if (fileIndex === limit) { @@ -254,6 +254,10 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider => { + await this.handleAccept(fileMatch, {}); + return TriggerAction.CLOSE_PICKER; + }, }); const results: Match[] = fileMatch.matches() ?? []; From 191be39e5ac872e03f9d79cc859d9917f40ad935 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 4 Mar 2024 15:01:51 -0800 Subject: [PATCH 205/228] Make sure the same GitHub account is used until we support multiple GH accounts (#206847) Fixes https://github.com/microsoft/vscode/issues/203850 --- .../src/common/errors.ts | 4 ++ extensions/github-authentication/src/flows.ts | 13 ++++++- .../github-authentication/src/github.ts | 37 +++++++++++++++++-- .../github-authentication/src/githubServer.ts | 11 ++---- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/extensions/github-authentication/src/common/errors.ts b/extensions/github-authentication/src/common/errors.ts index 3ba3dfc006a..f60b7233499 100644 --- a/extensions/github-authentication/src/common/errors.ts +++ b/extensions/github-authentication/src/common/errors.ts @@ -8,3 +8,7 @@ export const TIMED_OUT_ERROR = 'Timed out'; // These error messages are internal and should not be shown to the user in any way. export const USER_CANCELLATION_ERROR = 'User Cancelled'; export const NETWORK_ERROR = 'network error'; + +// This is the error message that we throw if the login was cancelled for any reason. Extensions +// calling `getSession` can handle this error to know that the user cancelled the login. +export const CANCELLATION_ERROR = 'Cancelled'; diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 3641ffb3a36..7498a2b2202 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -68,6 +68,7 @@ interface IFlowTriggerOptions { callbackUri: Uri; uriHandler: UriEventHandler; enterpriseUri?: Uri; + existingLogin?: string; } interface IFlow { @@ -149,7 +150,8 @@ const allFlows: IFlow[] = [ nonce, callbackUri, uriHandler, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying without local server... (${scopes})`); return await window.withProgress({ @@ -169,6 +171,9 @@ const allFlows: IFlow[] = [ ['scope', scopes], ['state', encodeURIComponent(callbackUri.toString(true))] ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } // The extra toString, parse is apparently needed for env.openExternal // to open the correct URL. @@ -215,7 +220,8 @@ const allFlows: IFlow[] = [ baseUri, redirectUri, logger, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying with local server... (${scopes})`); return await window.withProgress({ @@ -232,6 +238,9 @@ const allFlows: IFlow[] = [ ['redirect_uri', redirectUri.toString(true)], ['scope', scopes], ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } const loginUrl = baseUri.with({ path: '/login/oauth/authorize', diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 71aa17bd5cc..6c9b1f20294 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 { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -296,13 +296,44 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid scopes: JSON.stringify(scopes), }); + const sessions = await this._sessionsPromise; const scopeString = sortedScopes.join(' '); - const token = await this._githubServer.login(scopeString); + const existingLogin = sessions[0]?.account.label; + const token = await this._githubServer.login(scopeString, existingLogin); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - const sessions = await this._sessionsPromise; + if (sessions.some(s => s.id !== session.id)) { + const otherAccountsIndexes = new Array(); + const otherAccountsLabels = new Set(); + for (let i = 0; i < sessions.length; i++) { + if (sessions[i].id !== session.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); diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 0729c4c5077..af2cf22724f 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -11,19 +11,15 @@ import { isSupportedClient, isSupportedTarget } from './common/env'; import { crypto } from './node/crypto'; import { fetching } from './node/fetch'; import { ExtensionHost, GitHubTarget, getFlows } from './flows'; -import { NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; import { Config } from './config'; import { base64Encode } from './node/buffer'; -// This is the error message that we throw if the login was cancelled for any reason. Extensions -// calling `getSession` can handle this error to know that the user cancelled the login. -const CANCELLATION_ERROR = 'Cancelled'; - const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect'; const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect'; export interface IGitHubServer { - login(scopes: string): Promise; + login(scopes: string, existingLogin?: string): Promise; logout(session: vscode.AuthenticationSession): Promise; getUserInfo(token: string): Promise<{ id: string; accountName: string }>; sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise; @@ -91,7 +87,7 @@ export class GitHubServer implements IGitHubServer { return this._isNoCorsEnvironment; } - public async login(scopes: string): Promise { + public async login(scopes: string, existingLogin?: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); // Used for showing a friendlier message to the user when the explicitly cancel a flow. @@ -143,6 +139,7 @@ export class GitHubServer implements IGitHubServer { uriHandler: this._uriHandler, enterpriseUri: this._ghesUri, redirectUri: vscode.Uri.parse(await this.getRedirectEndpoint()), + existingLogin }); } catch (e) { userCancelled = this.processLoginError(e); From d73fa8b14a6c873958d00a7d7ad13fcb540a052c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 4 Mar 2024 20:08:00 -0800 Subject: [PATCH 206/228] debug: support data breakpoints by address (#206855) This creates a new "Add Data Breakpoint at Address" action in the breakpoints view when a debugger that supports the capability is available. It prompts the user to enter their address in a quickpick, and then allows them to choose appropriate data access settings. The editor side of https://github.com/microsoft/debug-adapter-protocol/issues/455 --- .../api/browser/mainThreadDebugService.ts | 20 +- .../contrib/debug/browser/breakpointsView.ts | 171 +++++++++++++++++- .../contrib/debug/browser/debugIcons.ts | 1 + .../contrib/debug/browser/debugService.ts | 16 +- .../contrib/debug/browser/debugSession.ts | 52 ++++-- .../contrib/debug/browser/variablesView.ts | 8 +- .../workbench/contrib/debug/common/debug.ts | 25 ++- .../contrib/debug/common/debugModel.ts | 38 +++- .../contrib/debug/common/debugProtocol.d.ts | 53 ++---- .../contrib/debug/common/debugViewModel.ts | 23 ++- .../debug/test/browser/breakpoints.test.ts | 10 +- .../contrib/debug/test/common/mockDebug.ts | 8 +- 12 files changed, 325 insertions(+), 100 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 3178df6d093..94641115abb 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -5,7 +5,7 @@ import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI as uri, UriComponents } from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from 'vs/workbench/contrib/debug/common/debug'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto @@ -225,7 +225,14 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } else if (dto.type === 'function') { this.debugService.addFunctionBreakpoint(dto.functionName, dto.id, dto.mode); } else if (dto.type === 'data') { - this.debugService.addDataBreakpoint(dto.label, dto.dataId, dto.canPersist, dto.accessTypes, dto.accessType, dto.mode); + this.debugService.addDataBreakpoint({ + description: dto.label, + src: { type: DataBreakpointSetType.Variable, dataId: dto.dataId }, + canPersist: dto.canPersist, + accessTypes: dto.accessTypes, + accessType: dto.accessType, + mode: dto.mode + }); } } return Promise.resolve(); @@ -436,19 +443,20 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb logMessage: fbp.logMessage, functionName: fbp.name }; - } else if ('dataId' in bp) { + } else if ('src' in bp) { const dbp = bp; - return { + return { type: 'data', id: dbp.getId(), - dataId: dbp.dataId, + dataId: dbp.src.type === DataBreakpointSetType.Variable ? dbp.src.dataId : dbp.src.address, enabled: dbp.enabled, condition: dbp.condition, hitCondition: dbp.hitCondition, logMessage: dbp.logMessage, + accessType: dbp.accessType, label: dbp.description, canPersist: dbp.canPersist - }; + } satisfies IDataBreakpointDto; } else { const sbp = bp; return { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 13b72018035..ddfb63625fd 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -51,10 +51,11 @@ import { IEditorPane } from 'vs/workbench/common/editor'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; -import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DEBUG_SCHEME, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; +import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, DEBUG_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; const $ = dom.$; @@ -87,6 +88,7 @@ export class BreakpointsView extends ViewPane { private ignoreLayout = false; private menu: IMenu; private breakpointItemType: IContextKey; + private breakpointIsDataBytes: IContextKey; private breakpointHasMultipleModes: IContextKey; private breakpointSupportsCondition: IContextKey; private _inputBoxData: InputBoxData | undefined; @@ -120,6 +122,7 @@ export class BreakpointsView extends ViewPane { this.menu = menuService.createMenu(MenuId.DebugBreakpointsContext, contextKeyService); this._register(this.menu); this.breakpointItemType = CONTEXT_BREAKPOINT_ITEM_TYPE.bindTo(contextKeyService); + this.breakpointIsDataBytes = CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES.bindTo(contextKeyService); this.breakpointHasMultipleModes = CONTEXT_BREAKPOINT_HAS_MODES.bindTo(contextKeyService); this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); @@ -142,7 +145,7 @@ export class BreakpointsView extends ViewPane { new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService), this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), - this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType), + this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.breakpointIsDataBytes), new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), this.instantiationService.createInstance(InstructionBreakpointsRenderer), ], { @@ -266,6 +269,7 @@ export class BreakpointsView extends ViewPane { const session = this.debugService.getViewModel().focusedSession; const conditionSupported = element instanceof ExceptionBreakpoint ? element.supportsCondition : (!session || !!session.capabilities.supportsConditionalBreakpoints); this.breakpointSupportsCondition.set(conditionSupported); + this.breakpointIsDataBytes.set(element instanceof DataBreakpoint && element.src.type === DataBreakpointSetType.Address); const secondary: IAction[] = []; createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, { primary: [], secondary }, 'inline'); @@ -740,6 +744,7 @@ class DataBreakpointsRenderer implements IListRenderer, private breakpointSupportsCondition: IContextKey, private breakpointItemType: IContextKey, + private breakpointIsDataBytes: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -816,10 +821,12 @@ class DataBreakpointsRenderer implements IListRenderer 1); this.breakpointItemType.set('dataBreakpoint'); + this.breakpointIsDataBytes.set(dataBreakpoint.src.type === DataBreakpointSetType.Address); createAndFillInActionBarActions(this.menu, { arg: dataBreakpoint, shouldForwardArgs: true }, { primary, secondary: [] }, 'inline'); data.actionBar.clear(); data.actionBar.push(primary, { icon: true, label: false }); breakpointIdToActionBarDomeNode.set(dataBreakpoint.getId(), data.actionBar.domNode); + this.breakpointIsDataBytes.reset(); } disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void { @@ -1421,6 +1428,166 @@ registerAction2(class extends Action2 { } }); +abstract class MemoryBreakpointAction extends Action2 { + async run(accessor: ServicesAccessor, existingBreakpoint?: IDataBreakpoint): Promise { + const debugService = accessor.get(IDebugService); + const session = debugService.getViewModel().focusedSession; + if (!session) { + return; + } + + let defaultValue = undefined; + if (existingBreakpoint && existingBreakpoint.src.type === DataBreakpointSetType.Address) { + defaultValue = `${existingBreakpoint.src.address} + ${existingBreakpoint.src.bytes}`; + } + + const quickInput = accessor.get(IQuickInputService); + const notifications = accessor.get(INotificationService); + const range = await this.getRange(quickInput, defaultValue); + if (!range) { + return; + } + + let info: IDataBreakpointInfoResponse | undefined; + try { + info = await session.dataBytesBreakpointInfo(range.address, range.bytes); + } catch (e) { + notifications.error(localize('dataBreakpointError', "Failed to set data breakpoint at {0}: {1}", range.address, e.message)); + } + + if (!info?.dataId) { + return; + } + + let accessType: DebugProtocol.DataBreakpointAccessType = 'write'; + if (info.accessTypes && info.accessTypes?.length > 1) { + const accessTypes = info.accessTypes.map(type => ({ label: type })); + const selectedAccessType = await quickInput.pick(accessTypes, { placeHolder: localize('dataBreakpointAccessType', "Select the access type to monitor") }); + if (!selectedAccessType) { + return; + } + + accessType = selectedAccessType.label; + } + + const src: DataBreakpointSource = { type: DataBreakpointSetType.Address, ...range }; + if (existingBreakpoint) { + await debugService.removeDataBreakpoints(existingBreakpoint.getId()); + } + + await debugService.addDataBreakpoint({ + description: info.description, + src, + canPersist: true, + accessTypes: info.accessTypes, + accessType: accessType, + initialSessionData: { session, dataId: info.dataId } + }); + } + + private getRange(quickInput: IQuickInputService, defaultValue?: string) { + return new Promise<{ address: string; bytes: number } | undefined>(resolve => { + const input = quickInput.createInputBox(); + input.prompt = localize('dataBreakpointMemoryRangePrompt', "Enter a memory range in which to break"); + input.placeholder = localize('dataBreakpointMemoryRangePlaceholder', 'Absolute range (0x1234 - 0x1300) or range of bytes after an address (0x1234 + 0xff)'); + if (defaultValue) { + input.value = defaultValue; + input.valueSelection = [0, defaultValue.length]; + } + input.onDidChangeValue(e => { + const err = this.parseAddress(e, false); + input.validationMessage = err?.error; + }); + input.onDidAccept(() => { + const r = this.parseAddress(input.value, true); + if ('error' in r) { + input.validationMessage = r.error; + } else { + resolve(r); + } + input.dispose(); + }); + input.onDidHide(() => { + resolve(undefined); + input.dispose(); + }); + input.ignoreFocusOut = true; + input.show(); + }); + } + + private parseAddress(range: string, isFinal: false): { error: string } | undefined; + private parseAddress(range: string, isFinal: true): { error: string } | { address: string; bytes: number }; + private parseAddress(range: string, isFinal: boolean): { error: string } | { address: string; bytes: number } | undefined { + const parts = /^(\S+)\s*(?:([+-])\s*(\S+))?/.exec(range); + if (!parts) { + return { error: localize('dataBreakpointAddrFormat', 'Address should be a range of numbers the form "[Start] - [End]" or "[Start] + [Bytes]"') }; + } + + const isNum = (e: string) => isFinal ? /^0x[0-9a-f]*|[0-9]*$/i.test(e) : /^0x[0-9a-f]+|[0-9]+$/i.test(e); + const [, startStr, sign = '+', endStr = '1'] = parts; + + for (const n of [startStr, endStr]) { + if (!isNum(n)) { + return { error: localize('dataBreakpointAddrStartEnd', 'Number must be a decimal integer or hex value starting with \"0x\", got {0}', n) }; + } + } + + if (!isFinal) { + return; + } + + const start = BigInt(startStr); + const end = BigInt(endStr); + const address = `0x${start.toString(16)}`; + if (sign === '-') { + return { address, bytes: Number(start - end) }; + } + + return { address, bytes: Number(end) }; + } +} + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.addDataBreakpointOnAddress', + title: { + ...localize2('addDataBreakpointOnAddress', "Add Data Breakpoint at Address"), + mnemonicTitle: localize({ key: 'miDataBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Data Breakpoint..."), + }, + f1: true, + icon: icons.watchExpressionsAddDataBreakpoint, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 11, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, ContextKeyExpr.equals('view', BREAKPOINTS_VIEW_ID)) + }, { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 4, + when: CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED + }] + }); + } +}); + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.editDataBreakpointOnAddress', + title: localize2('editDataBreakpointOnAddress', "Edit Address..."), + menu: [{ + id: MenuId.DebugBreakpointsContext, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES), + group: 'navigation', + order: 15, + }] + }); + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index b1a9a4a0789..12376d1a83f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -79,6 +79,7 @@ export const watchExpressionsRemoveAll = registerIcon('watch-expressions-remove- export const watchExpressionRemove = registerIcon('watch-expression-remove', Codicon.removeClose, localize('watchExpressionRemove', 'Icon for the Remove action in the watch view.')); export const watchExpressionsAdd = registerIcon('watch-expressions-add', Codicon.add, localize('watchExpressionsAdd', 'Icon for the add action in the watch view.')); export const watchExpressionsAddFuncBreakpoint = registerIcon('watch-expressions-add-function-breakpoint', Codicon.add, localize('watchExpressionsAddFuncBreakpoint', 'Icon for the add function breakpoint action in the watch view.')); +export const watchExpressionsAddDataBreakpoint = registerIcon('watch-expressions-add-data-breakpoint', Codicon.variableGroup, localize('watchExpressionsAddDataBreakpoint', 'Icon for the add data breakpoint action in the breakpoints view.')); export const breakpointsRemoveAll = registerIcon('breakpoints-remove-all', Codicon.closeAll, localize('breakpointsRemoveAll', 'Icon for the Remove All action in the breakpoints view.')); export const breakpointsActivate = registerIcon('breakpoints-activate', Codicon.activateBreakpoints, localize('breakpointsActivate', 'Icon for the activate action in the breakpoints view.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 5478398dfb6..84e946ecf70 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -6,7 +6,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { Action, IAction } from 'vs/base/common/actions'; import { distinct } from 'vs/base/common/arrays'; -import { raceTimeout, RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, raceTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isErrorWithActions } from 'vs/base/common/errorMessage'; import * as errors from 'vs/base/common/errors'; @@ -24,7 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -34,22 +34,21 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { EditorsOrder } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { DebugMemoryFileSystemProvider } from 'vs/workbench/contrib/debug/browser/debugMemory'; 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_HAS_DEBUGGED, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, DEBUG_SCHEME, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; +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 { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; -import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, 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'; import { getExtensionHostDebugSession, saveAllBeforeDebugStart } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; +import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; @@ -58,6 +57,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -1081,8 +1081,8 @@ export class DebugService implements IDebugService { await this.sendFunctionBreakpoints(); } - async addDataBreakpoint(description: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise { - this.model.addDataBreakpoint({ description, dataId, canPersist, accessTypes, accessType, mode }); + async addDataBreakpoint(opts: IDataBreakpointOptions): Promise { + this.model.addDataBreakpoint(opts); this.debugStorage.storeBreakpoints(this.model); await this.sendDataBreakpoints(); this.debugStorage.storeBreakpoints(this.model); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 861135c6f6a..0d56d661268 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -29,7 +29,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -41,6 +41,7 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { isDefined } from 'vs/base/common/types'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; @@ -461,7 +462,7 @@ export class DebugSession implements IDebugSession, IDisposable { breakpoints: breakpointsToSend.map(bp => bp.toDAP()), sourceModified }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < breakpointsToSend.length; i++) { data.set(breakpointsToSend[i].getId(), response.body.breakpoints[i]); @@ -478,7 +479,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setFunctionBreakpoints({ breakpoints: fbpts.map(bp => bp.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < fbpts.length; i++) { data.set(fbpts[i].getId(), response.body.breakpoints[i]); @@ -506,7 +507,7 @@ export class DebugSession implements IDebugSession, IDisposable { } : { filters: exbpts.map(exb => exb.filter) }; const response = await this.raw.setExceptionBreakpoints(args); - if (response && response.body && response.body.breakpoints) { + if (response?.body && response.body.breakpoints) { const data = new Map(); for (let i = 0; i < exbpts.length; i++) { data.set(exbpts[i].getId(), response.body.breakpoints[i]); @@ -517,7 +518,19 @@ export class DebugSession implements IDebugSession, IDisposable { } } - async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + if (this.raw?.capabilities.supportsDataBreakpointBytes === false) { + throw new Error(localize('sessionDoesNotSupporBytesBreakpoints', "Session does not support breakpoints with bytes")); + } + + return this._dataBreakpointInfo({ name: address, bytes, asAddress: true }); + } + + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + return this._dataBreakpointInfo({ name, variablesReference }); + } + + private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info')); } @@ -525,7 +538,7 @@ export class DebugSession implements IDebugSession, IDisposable { throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); } - const response = await this.raw.dataBreakpointInfo({ name, variablesReference }); + const response = await this.raw.dataBreakpointInfo(args); return response?.body; } @@ -535,11 +548,24 @@ export class DebugSession implements IDebugSession, IDisposable { } if (this.raw.readyForBreakpoints) { - const response = await this.raw.setDataBreakpoints({ breakpoints: dataBreakpoints.map(bp => bp.toDAP()) }); - if (response && response.body) { + const converted = await Promise.all(dataBreakpoints.map(async bp => { + try { + const dap = await bp.toDAP(this); + return { dap, bp }; + } catch (e) { + return { bp, message: e.message }; + } + })); + const response = await this.raw.setDataBreakpoints({ breakpoints: converted.map(d => d.dap).filter(isDefined) }); + if (response?.body) { const data = new Map(); - for (let i = 0; i < dataBreakpoints.length; i++) { - data.set(dataBreakpoints[i].getId(), response.body.breakpoints[i]); + let i = 0; + for (const dap of converted) { + if (!dap.dap) { + data.set(dap.bp.getId(), dap.message); + } else if (i < response.body.breakpoints.length) { + data.set(dap.bp.getId(), response.body.breakpoints[i++]); + } } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } @@ -553,7 +579,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints.map(ib => ib.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < instructionBreakpoints.length; i++) { data.set(instructionBreakpoints[i].getId(), response.body.breakpoints[i]); @@ -790,7 +816,7 @@ export class DebugSession implements IDebugSession, IDisposable { } const response = await this.raw.loadedSources({}); - if (response && response.body && response.body.sources) { + if (response?.body && response.body.sources) { return response.body.sources.map(src => this.getSource(src)); } else { return []; @@ -959,7 +985,7 @@ export class DebugSession implements IDebugSession, IDisposable { private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise { if (this.raw) { const response = await this.raw.threads(); - if (response && response.body && response.body.threads) { + if (response?.body && response.body.threads) { this.model.rawUpdate({ sessionId: this.getId(), threads: response.body.threads, diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index cce34ecbbe6..56582689c12 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -39,7 +39,7 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getContextForVariable } from 'vs/workbench/contrib/debug/common/debugContext'; import { ErrorScope, Expression, Scope, StackFrame, Variable, VisualizedExpression, getUriForDebugMemory } from 'vs/workbench/contrib/debug/common/debugModel'; import { DebugVisualizer, IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -766,7 +766,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'write', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'write' }); } } }); @@ -777,7 +777,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'readWrite', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'readWrite' }); } } }); @@ -788,7 +788,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'read', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'read' }); } } }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 86d0e94b826..eefbb082ad6 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 { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDataBreakpointOptions, 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'; @@ -62,6 +62,7 @@ export const CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD = new RawContextKey('watchItemType', undefined, { type: 'string', description: nls.localize('watchItemType', "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'") }); export const CONTEXT_CAN_VIEW_MEMORY = new RawContextKey('canViewMemory', undefined, { type: 'boolean', description: nls.localize('canViewMemory', "Indicates whether the item in the view has an associated memory refrence.") }); export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined, { type: 'string', description: nls.localize('breakpointItemType', "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'") }); +export const CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES = new RawContextKey('breakpointItemBytes', undefined, { type: 'boolean', description: nls.localize('breakpointItemIsDataBytes', "Whether the breakpoint item is a data breakpoint on a byte range.") }); export const CONTEXT_BREAKPOINT_HAS_MODES = new RawContextKey('breakpointHasModes', false, { type: 'boolean', description: nls.localize('breakpointHasModes', "Whether the breakpoint has multiple modes it can switch to.") }); export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false, { type: 'boolean', description: nls.localize('breakpointSupportsCondition', "True when the focused breakpoint supports conditions.") }); export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false, { type: 'boolean', description: nls.localize('loadedScriptsSupported', "True when the focused sessions supports the LOADED SCRIPTS view") }); @@ -78,6 +79,7 @@ export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey('debuggers export const CONTEXT_DEBUG_EXTENSION_AVAILABLE = new RawContextKey('debugExtensionAvailable', true, { type: 'boolean', description: nls.localize('debugExtensionsAvailable', "True when there is at least one debug extension installed and enabled.") }); export const CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT = new RawContextKey('debugProtocolVariableMenuContext', undefined, { type: 'string', description: nls.localize('debugProtocolVariableMenuContext', "Represents the context the debug adapter sets on the focused variable in the VARIABLES view.") }); export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugSetVariableSupported', false, { type: 'boolean', description: nls.localize('debugSetVariableSupported', "True when the focused session supports 'setVariable' request.") }); +export const CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED = new RawContextKey('debugSetDataBreakpointAddressSupported', false, { type: 'boolean', description: nls.localize('debugSetDataBreakpointAddressSupported', "True when the focused session supports 'getBreakpointInfo' request on an address.") }); export const CONTEXT_SET_EXPRESSION_SUPPORTED = new RawContextKey('debugSetExpressionSupported', false, { type: 'boolean', description: nls.localize('debugSetExpressionSupported', "True when the focused session supports 'setExpression' request.") }); export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueChangesSupported', "True when the focused session supports to break when value changes.") }); export const CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED = new RawContextKey('breakWhenValueIsAccessedSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsAccessedSupported', "True when the focused breakpoint supports to break when value is accessed.") }); @@ -404,6 +406,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; dataBreakpointInfo(name: string, variablesReference?: number): Promise; + dataBytesBreakpointInfo(address: string, bytes: number): Promise; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendInstructionBreakpoints(dbps: IInstructionBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; @@ -607,12 +610,26 @@ export interface IExceptionBreakpoint extends IBaseBreakpoint { readonly description: string | undefined; } +export const enum DataBreakpointSetType { + Variable, + Address, +} + +/** + * Source for a data breakpoint. A data breakpoint on a variable always has a + * `dataId` because it cannot reference that variable globally, but addresses + * can request info repeated and use session-specific data. + */ +export type DataBreakpointSource = + | { type: DataBreakpointSetType.Variable; dataId: string } + | { type: DataBreakpointSetType.Address; address: string; bytes: number }; + export interface IDataBreakpoint extends IBaseBreakpoint { readonly description: string; - readonly dataId: string; readonly canPersist: boolean; + readonly src: DataBreakpointSource; readonly accessType: DebugProtocol.DataBreakpointAccessType; - toDAP(): DebugProtocol.DataBreakpoint; + toDAP(session: IDebugSession): Promise; } export interface IInstructionBreakpoint extends IBaseBreakpoint { @@ -1144,7 +1161,7 @@ export interface IDebugService { /** * Adds a new data breakpoint. */ - addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise; + addDataBreakpoint(opts: IDataBreakpointOptions): Promise; /** * Updates an already existing data breakpoint. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 8098d1ce57b..b12e9b726ff 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, 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 } 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'; @@ -1150,15 +1150,18 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak export interface IDataBreakpointOptions extends IBaseBreakpointOptions { description: string; - dataId: string; + src: DataBreakpointSource; canPersist: boolean; + initialSessionData?: { session: IDebugSession; dataId: string }; accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; accessType: DebugProtocol.DataBreakpointAccessType; } export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { + private readonly sessionDataIdForAddr = new WeakMap(); + public readonly description: string; - public readonly dataId: string; + public readonly src: DataBreakpointSource; public readonly canPersist: boolean; public readonly accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; public readonly accessType: DebugProtocol.DataBreakpointAccessType; @@ -1169,15 +1172,36 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { ) { super(id, opts); this.description = opts.description; - this.dataId = opts.dataId; + if ('dataId' in opts) { // back compat with old saved variables in 1.87 + opts.src = { type: DataBreakpointSetType.Variable, dataId: opts.dataId as string }; + } + this.src = opts.src; this.canPersist = opts.canPersist; this.accessTypes = opts.accessTypes; this.accessType = opts.accessType; + if (opts.initialSessionData) { + this.sessionDataIdForAddr.set(opts.initialSessionData.session, opts.initialSessionData.dataId); + } } - toDAP(): DebugProtocol.DataBreakpoint { + async toDAP(session: IDebugSession): Promise { + let dataId: string; + if (this.src.type === DataBreakpointSetType.Variable) { + dataId = this.src.dataId; + } else { + let sessionDataId = this.sessionDataIdForAddr.get(session); + if (!sessionDataId) { + sessionDataId = (await session.dataBytesBreakpointInfo(this.src.address, this.src.bytes))?.dataId; + if (!sessionDataId) { + return undefined; + } + this.sessionDataIdForAddr.set(session, sessionDataId); + } + dataId = sessionDataId; + } + return { - dataId: this.dataId, + dataId, accessType: this.accessType, condition: this.condition, hitCondition: this.hitCondition, @@ -1188,7 +1212,7 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { return { ...super.toJSON(), description: this.description, - dataId: this.dataId, + src: this.src, accessTypes: this.accessTypes, accessType: this.accessType, canPersist: this.canPersist, diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index b00a4fd466a..50eacfd65e2 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -813,11 +813,22 @@ declare module DebugProtocol { /** Reference to the variable container if the data breakpoint is requested for a child of the container. The `variablesReference` must have been obtained in the current suspended state. See 'Lifetime of Object References' in the Overview section for details. */ variablesReference?: number; /** The name of the variable's child to obtain data breakpoint information for. - If `variablesReference` isn't specified, this can be an expression. + If `variablesReference` isn't specified, this can be an expression, or an address if `asAddress` is also true. */ name: string; /** When `name` is an expression, evaluate it in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. When `variablesReference` is specified, this property has no effect. */ frameId?: number; + /** If specified, a debug adapter should return information for the range of memory extending `bytes` number of bytes from the address or variable specified by `name`. Breakpoints set using the resulting data ID should pause on data access anywhere within that range. + + Clients may set this property only if the `supportsDataBreakpointBytes` capability is true. + */ + bytes?: number; + /** If `true`, the `name` is a memory address and the debugger should interpret it as a decimal value, or hex value if it is prefixed with `0x`. + + Clients may set this property only if the `supportsDataBreakpointBytes` + capability is true. + */ + asAddress?: boolean; /** The mode of the desired breakpoint. If defined, this must be one of the `breakpointModes` the debug adapter advertised in its `Capabilities`. */ mode?: string; } @@ -1680,42 +1691,6 @@ declare module DebugProtocol { }; } - /** DataAddressBreakpointInfo request; value of command field is 'DataAddressBreakpointInfo'. - Obtains information on a possible data breakpoint that could be set on a memory address or memory address range. - - Clients should only call this request if the corresponding capability `supportsDataAddressInfo` is true. - */ - interface DataAddressBreakpointInfoRequest extends Request { - // command: 'DataAddressBreakpointInfo'; - arguments: DataAddressBreakpointInfoArguments; - } - - /** Arguments for `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoArguments { - /** The address of the data for which to obtain breakpoint information. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - address?: string; - /** If passed, requests breakpoint information for an exclusive byte range rather than a single address. The range extends the given number of `bytes` from the start `address`. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - bytes?: string; - } - - /** Response to `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoResponse extends Response { - body: { - /** An identifier for the data on which a data breakpoint can be registered with the `setDataBreakpoints` request or null if no data breakpoint is available. If a `variablesReference` or `frameId` is passed, the `dataId` is valid in the current suspended state, otherwise it's valid indefinitely. See 'Lifetime of Object References' in the Overview section for details. Breakpoints set using the `dataId` in the `setDataBreakpoints` request may outlive the lifetime of the associated `dataId`. */ - dataId: string | null; - /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ - description: string; - /** Attribute lists the available access types for a potential data breakpoint. A UI client could surface this information. */ - accessTypes?: DataBreakpointAccessType[]; - /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ - canPersist?: boolean; - }; - } - /** Information about the capabilities of a debug adapter. */ interface Capabilities { /** The debug adapter supports the `configurationDone` request. */ @@ -1788,8 +1763,6 @@ declare module DebugProtocol { supportsBreakpointLocationsRequest?: boolean; /** The debug adapter supports the `clipboard` context value in the `evaluate` request. */ supportsClipboardContext?: boolean; - /** The debug adapter supports the `dataAddressBreakpointInfo` request. */ - supportsDataAddressInfo?: boolean; /** The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests. */ supportsSteppingGranularity?: boolean; /** The debug adapter supports adding breakpoints based on instruction references. */ @@ -1798,6 +1771,8 @@ declare module DebugProtocol { supportsExceptionFilterOptions?: boolean; /** The debug adapter supports the `singleThread` property on the execution requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`). */ supportsSingleThreadExecutionRequests?: boolean; + /** The debug adapter supports the `asAddress` and `bytes` fields in the `dataBreakpointInfo` request. */ + supportsDataBreakpointBytes?: boolean; /** Modes of breakpoints supported by the debug adapter, such as 'hardware' or 'software'. If present, the client may allow the user to select a mode and include it in its `setBreakpoints` request. Clients may present the first applicable mode in this array as the 'default' mode in gestures that set breakpoints. diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 4b0959a97a8..7221f390771 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { @@ -34,6 +34,7 @@ export class ViewModel implements IViewModel { private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; private setVariableSupported!: IContextKey; + private setDataBreakpointAtByteSupported!: IContextKey; private setExpressionSupported!: IContextKey; private multiSessionDebug!: IContextKey; private terminateDebuggeeSupported!: IContextKey; @@ -52,6 +53,7 @@ export class ViewModel implements IViewModel { this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); + this.setDataBreakpointAtByteSupported = CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED.bindTo(contextKeyService); this.setExpressionSupported = CONTEXT_SET_EXPRESSION_SUPPORTED.bindTo(contextKeyService); this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); this.terminateDebuggeeSupported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); @@ -88,15 +90,16 @@ export class ViewModel implements IViewModel { this._focusedSession = session; this.contextKeyService.bufferChangeEvents(() => { - this.loadedScriptsSupportedContextKey.set(session ? !!session.capabilities.supportsLoadedSourcesRequest : false); - this.stepBackSupportedContextKey.set(session ? !!session.capabilities.supportsStepBack : false); - this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false); - this.stepIntoTargetsSupported.set(session ? !!session.capabilities.supportsStepInTargetsRequest : false); - this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); - this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false); - this.setExpressionSupported.set(session ? !!session.capabilities.supportsSetExpression : false); - this.terminateDebuggeeSupported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); - this.suspendDebuggeeSupported.set(session ? !!session.capabilities.supportSuspendDebuggee : false); + this.loadedScriptsSupportedContextKey.set(!!session?.capabilities.supportsLoadedSourcesRequest); + this.stepBackSupportedContextKey.set(!!session?.capabilities.supportsStepBack); + this.restartFrameSupportedContextKey.set(!!session?.capabilities.supportsRestartFrame); + this.stepIntoTargetsSupported.set(!!session?.capabilities.supportsStepInTargetsRequest); + this.jumpToCursorSupported.set(!!session?.capabilities.supportsGotoTargetsRequest); + this.setVariableSupported.set(!!session?.capabilities.supportsSetVariable); + this.setDataBreakpointAtByteSupported.set(!!session?.capabilities.supportsDataBreakpointBytes); + this.setExpressionSupported.set(!!session?.capabilities.supportsSetExpression); + this.terminateDebuggeeSupported.set(!!session?.capabilities.supportTerminateDebuggee); + this.suspendDebuggeeSupported.set(!!session?.capabilities.supportSuspendDebuggee); this.disassembleRequestSupported.set(!!session?.capabilities.supportsDisassembleRequest); this.focusedStackFrameHasInstructionPointerReference.set(!!stackFrame?.instructionPointerReference); const attach = !!session && isSessionAttach(session); 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 b85e544f9bb..61599c36ce9 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -19,7 +19,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { getBreakpointMessageAndIcon, getExpandedBodySize } from 'vs/workbench/contrib/debug/browser/breakpointsView'; -import { IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DataBreakpointSetType, IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; @@ -313,13 +313,13 @@ suite('Debug - Breakpoints', () => { let eventCount = 0; disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); - model.addDataBreakpoint({ description: 'label', dataId: 'id', canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); - model.addDataBreakpoint({ description: 'second', dataId: 'secondId', canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); + model.addDataBreakpoint({ description: 'label', src: { type: DataBreakpointSetType.Variable, dataId: 'id' }, canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); + model.addDataBreakpoint({ description: 'second', src: { type: DataBreakpointSetType.Variable, dataId: 'secondId' }, canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); model.updateDataBreakpoint('1', { condition: 'aCondition' }); model.updateDataBreakpoint('2', { hitCondition: '10' }); const dataBreakpoints = model.getDataBreakpoints(); assert.strictEqual(dataBreakpoints[0].canPersist, true); - assert.strictEqual(dataBreakpoints[0].dataId, 'id'); + assert.deepStrictEqual(dataBreakpoints[0].src, { type: DataBreakpointSetType.Variable, dataId: 'id' }); assert.strictEqual(dataBreakpoints[0].accessType, 'read'); assert.strictEqual(dataBreakpoints[0].condition, 'aCondition'); assert.strictEqual(dataBreakpoints[1].canPersist, false); @@ -374,7 +374,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(result.message, 'Disabled Logpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log-disabled'); - model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', dataId: 'id' }); + model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', src: { type: DataBreakpointSetType.Variable, dataId: 'id' } }); const dataBreakpoints = model.getDataBreakpoints(); result = getBreakpointMessageAndIcon(State.Stopped, true, dataBreakpoints[0], ls, model); assert.strictEqual(result.message, 'Data Breakpoint'); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 617f46d449f..464a4794def 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -13,7 +13,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -114,7 +114,7 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise { + addDataBreakpoint(): Promise { throw new Error('Method not implemented.'); } @@ -223,6 +223,10 @@ export class MockSession implements IDebugSession { throw new Error('Method not implemented.'); } + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + throw new Error('Method not implemented.'); + } + dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined } | undefined> { throw new Error('Method not implemented.'); } From d0780ec1c1e32bb9de38eb953ac8bf36718106ee Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 5 Mar 2024 09:02:19 +0100 Subject: [PATCH 207/228] Fix Instant Custom Hover Issue (#206870) fixes #206762 --- src/vs/platform/hover/browser/hover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/hover/browser/hover.ts b/src/vs/platform/hover/browser/hover.ts index 3dd6ec94d02..0a60a0b4556 100644 --- a/src/vs/platform/hover/browser/hover.ts +++ b/src/vs/platform/hover/browser/hover.ts @@ -236,7 +236,7 @@ export interface IHoverTarget extends IDisposable { export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate { - private lastHoverHideTime = Number.MAX_VALUE; + private lastHoverHideTime = 0; private timeLimit = 200; private _delay: number; From fd98a6877ab0429ac204c8b8e7ebde96d260dee9 Mon Sep 17 00:00:00 2001 From: Hylke Bons Date: Fri, 1 Mar 2024 20:20:15 +0100 Subject: [PATCH 208/228] codicons: Move out aliases --- src/vs/base/common/codicons.ts | 12 +++++- src/vs/base/common/codiconsLibrary.ts | 58 +++++++++++++-------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 0cde23be0e8..6919e4934a2 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -36,7 +36,17 @@ export const codiconsDerived = { scrollbarButtonUp: register('scrollbar-button-up', 'triangle-up'), scrollbarButtonDown: register('scrollbar-button-down', 'triangle-down'), toolBarMore: register('toolbar-more', 'more'), - quickInputBack: register('quick-input-back', 'arrow-left') + quickInputBack: register('quick-input-back', 'arrow-left'), + dropDownButton: register('drop-down-button', 0xeab4), + symbolCustomColor: register('symbol-customcolor', 0xeb5c), + exportIcon: register('export', 0xebac), + workspaceUnspecified: register('workspace-unspecified', 0xebc3), + newLine: register('newline', 0xebea), + thumbsDownFilled: register('thumbsdown-filled', 0xec13), + thumbsUpFilled: register('thumbsup-filled', 0xec14), + gitFetch: register('git-fetch', 0xec1d), + lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f), + debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), } as const; diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index a350213cc76..4d25fed1023 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -1,13 +1,8 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { register } from 'vs/base/common/codiconsUtil'; +import { register } from 'vs/base/common/codiconsUtil' - -// This list is automatically generated by the vscode-codicons repo. +// This file is automatically generated by (microsoft/vscode-codicon)/scripts/export-to-ts.js // Please don't edit it, as your changes will be overwritten. -// If you want to create a mapping, add it to the codiconsDerived list in codicons.ts. +// Instead, add mappings to codiconsDerived in codicons.ts. export const codiconsLibrary = { add: register('add', 0xea60), plus: register('plus', 0xea60), @@ -24,9 +19,9 @@ export const codiconsLibrary = { recordKeys: register('record-keys', 0xea65), keyboard: register('keyboard', 0xea65), tag: register('tag', 0xea66), + gitPullRequestLabel: register('git-pull-request-label', 0xea66), tagAdd: register('tag-add', 0xea66), tagRemove: register('tag-remove', 0xea66), - gitPullRequestLabel: register('git-pull-request-label', 0xea66), person: register('person', 0xea67), personFollow: register('person-follow', 0xea67), personOutline: register('person-outline', 0xea67), @@ -59,8 +54,8 @@ export const codiconsLibrary = { closeDirty: register('close-dirty', 0xea71), debugBreakpoint: register('debug-breakpoint', 0xea71), debugBreakpointDisabled: register('debug-breakpoint-disabled', 0xea71), - debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), debugHint: register('debug-hint', 0xea71), + terminalDecorationSuccess: register('terminal-decoration-success', 0xea71), primitiveSquare: register('primitive-square', 0xea72), edit: register('edit', 0xea73), pencil: register('pencil', 0xea73), @@ -172,7 +167,6 @@ export const codiconsLibrary = { check: register('check', 0xeab2), checklist: register('checklist', 0xeab3), chevronDown: register('chevron-down', 0xeab4), - dropDownButton: register('drop-down-button', 0xeab4), chevronLeft: register('chevron-left', 0xeab5), chevronRight: register('chevron-right', 0xeab6), chevronUp: register('chevron-up', 0xeab7), @@ -180,9 +174,10 @@ export const codiconsLibrary = { chromeMaximize: register('chrome-maximize', 0xeab9), chromeMinimize: register('chrome-minimize', 0xeaba), chromeRestore: register('chrome-restore', 0xeabb), - circle: register('circle', 0xeabc), circleOutline: register('circle-outline', 0xeabc), + circle: register('circle', 0xeabc), debugBreakpointUnverified: register('debug-breakpoint-unverified', 0xeabc), + terminalDecorationIncomplete: register('terminal-decoration-incomplete', 0xeabc), circleSlash: register('circle-slash', 0xeabd), circuitBoard: register('circuit-board', 0xeabe), clearAll: register('clear-all', 0xeabf), @@ -194,7 +189,6 @@ export const codiconsLibrary = { collapseAll: register('collapse-all', 0xeac5), colorMode: register('color-mode', 0xeac6), commentDiscussion: register('comment-discussion', 0xeac7), - compareChanges: register('compare-changes', 0xeafd), creditCard: register('credit-card', 0xeac9), dash: register('dash', 0xeacc), dashboard: register('dashboard', 0xeacd), @@ -218,6 +212,7 @@ export const codiconsLibrary = { diffRemoved: register('diff-removed', 0xeadf), diffRenamed: register('diff-renamed', 0xeae0), diff: register('diff', 0xeae1), + diffSidebyside: register('diff-sidebyside', 0xeae1), discard: register('discard', 0xeae2), editorLayout: register('editor-layout', 0xeae3), emptyWindow: register('empty-window', 0xeae4), @@ -246,6 +241,7 @@ export const codiconsLibrary = { gist: register('gist', 0xeafb), gitCommit: register('git-commit', 0xeafc), gitCompare: register('git-compare', 0xeafd), + compareChanges: register('compare-changes', 0xeafd), gitMerge: register('git-merge', 0xeafe), githubAction: register('github-action', 0xeaff), githubAlt: register('github-alt', 0xeb00), @@ -258,13 +254,11 @@ export const codiconsLibrary = { horizontalRule: register('horizontal-rule', 0xeb07), hubot: register('hubot', 0xeb08), inbox: register('inbox', 0xeb09), - issueClosed: register('issue-closed', 0xeba4), issueReopened: register('issue-reopened', 0xeb0b), issues: register('issues', 0xeb0c), italic: register('italic', 0xeb0d), jersey: register('jersey', 0xeb0e), json: register('json', 0xeb0f), - bracket: register('bracket', 0xeb0f), kebabVertical: register('kebab-vertical', 0xeb10), key: register('key', 0xeb11), law: register('law', 0xeb12), @@ -343,7 +337,6 @@ export const codiconsLibrary = { starHalf: register('star-half', 0xeb5a), symbolClass: register('symbol-class', 0xeb5b), symbolColor: register('symbol-color', 0xeb5c), - symbolCustomColor: register('symbol-customcolor', 0xeb5c), symbolConstant: register('symbol-constant', 0xeb5d), symbolEnumMember: register('symbol-enum-member', 0xeb5e), symbolField: register('symbol-field', 0xeb5f), @@ -395,6 +388,7 @@ export const codiconsLibrary = { debugStackframeActive: register('debug-stackframe-active', 0xeb89), circleSmallFilled: register('circle-small-filled', 0xeb8a), debugStackframeDot: register('debug-stackframe-dot', 0xeb8a), + terminalDecorationMark: register('terminal-decoration-mark', 0xeb8a), debugStackframe: register('debug-stackframe', 0xeb8b), debugStackframeFocused: register('debug-stackframe-focused', 0xeb8b), debugBreakpointUnsupported: register('debug-breakpoint-unsupported', 0xeb8c), @@ -402,6 +396,7 @@ export const codiconsLibrary = { debugReverseContinue: register('debug-reverse-continue', 0xeb8e), debugStepBack: register('debug-step-back', 0xeb8f), debugRestartFrame: register('debug-restart-frame', 0xeb90), + debugAlt: register('debug-alt', 0xeb91), callIncoming: register('call-incoming', 0xeb92), callOutgoing: register('call-outgoing', 0xeb93), menu: register('menu', 0xeb94), @@ -420,10 +415,10 @@ export const codiconsLibrary = { syncIgnored: register('sync-ignored', 0xeb9f), pinned: register('pinned', 0xeba0), githubInverted: register('github-inverted', 0xeba1), - debugAlt: register('debug-alt', 0xeb91), serverProcess: register('server-process', 0xeba2), serverEnvironment: register('server-environment', 0xeba3), pass: register('pass', 0xeba4), + issueClosed: register('issue-closed', 0xeba4), stopCircle: register('stop-circle', 0xeba5), playCircle: register('play-circle', 0xeba6), record: register('record', 0xeba7), @@ -431,7 +426,7 @@ export const codiconsLibrary = { vmConnect: register('vm-connect', 0xeba9), cloud: register('cloud', 0xebaa), merge: register('merge', 0xebab), - exportIcon: register('export', 0xebac), + export: register('export', 0xebac), graphLeft: register('graph-left', 0xebad), magnet: register('magnet', 0xebae), notebook: register('notebook', 0xebaf), @@ -456,7 +451,7 @@ export const codiconsLibrary = { debugRerun: register('debug-rerun', 0xebc0), workspaceTrusted: register('workspace-trusted', 0xebc1), workspaceUntrusted: register('workspace-untrusted', 0xebc2), - workspaceUnspecified: register('workspace-unspecified', 0xebc3), + workspaceUnknown: register('workspace-unknown', 0xebc3), terminalCmd: register('terminal-cmd', 0xebc4), terminalDebian: register('terminal-debian', 0xebc5), terminalLinux: register('terminal-linux', 0xebc6), @@ -490,12 +485,13 @@ export const codiconsLibrary = { graphLine: register('graph-line', 0xebe2), graphScatter: register('graph-scatter', 0xebe3), pieChart: register('pie-chart', 0xebe4), + bracket: register('bracket', 0xeb0f), bracketDot: register('bracket-dot', 0xebe5), bracketError: register('bracket-error', 0xebe6), lockSmall: register('lock-small', 0xebe7), azureDevops: register('azure-devops', 0xebe8), verifiedFilled: register('verified-filled', 0xebe9), - newLine: register('newline', 0xebea), + newline: register('newline', 0xebea), layout: register('layout', 0xebeb), layoutActivitybarLeft: register('layout-activitybar-left', 0xebec), layoutActivitybarRight: register('layout-activitybar-right', 0xebed), @@ -509,17 +505,19 @@ export const codiconsLibrary = { layoutStatusbar: register('layout-statusbar', 0xebf5), layoutMenubar: register('layout-menubar', 0xebf6), layoutCentered: register('layout-centered', 0xebf7), - layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00), - layoutPanelOff: register('layout-panel-off', 0xec01), - layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02), target: register('target', 0xebf8), indent: register('indent', 0xebf9), recordSmall: register('record-small', 0xebfa), errorSmall: register('error-small', 0xebfb), + terminalDecorationError: register('terminal-decoration-error', 0xebfb), arrowCircleDown: register('arrow-circle-down', 0xebfc), arrowCircleLeft: register('arrow-circle-left', 0xebfd), arrowCircleRight: register('arrow-circle-right', 0xebfe), arrowCircleUp: register('arrow-circle-up', 0xebff), + layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00), + layoutPanelOff: register('layout-panel-off', 0xec01), + layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02), + blank: register('blank', 0xec03), heartFilled: register('heart-filled', 0xec04), map: register('map', 0xec05), mapFilled: register('map-filled', 0xec06), @@ -535,8 +533,8 @@ export const codiconsLibrary = { sparkle: register('sparkle', 0xec10), insert: register('insert', 0xec11), mic: register('mic', 0xec12), - thumbsDownFilled: register('thumbsdown-filled', 0xec13), - thumbsUpFilled: register('thumbsup-filled', 0xec14), + thumbsdownFilled: register('thumbsdown-filled', 0xec13), + thumbsupFilled: register('thumbsup-filled', 0xec14), coffee: register('coffee', 0xec15), snake: register('snake', 0xec16), game: register('game', 0xec17), @@ -545,21 +543,23 @@ export const codiconsLibrary = { piano: register('piano', 0xec1a), music: register('music', 0xec1b), micFilled: register('mic-filled', 0xec1c), - gitFetch: register('git-fetch', 0xec1d), + repoFetch: register('repo-fetch', 0xec1d), copilot: register('copilot', 0xec1e), lightbulbSparkle: register('lightbulb-sparkle', 0xec1f), - lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f), robot: register('robot', 0xec20), sparkleFilled: register('sparkle-filled', 0xec21), diffSingle: register('diff-single', 0xec22), diffMultiple: register('diff-multiple', 0xec23), surroundWith: register('surround-with', 0xec24), + share: register('share', 0xec25), gitStash: register('git-stash', 0xec26), gitStashApply: register('git-stash-apply', 0xec27), gitStashPop: register('git-stash-pop', 0xec28), + vscode: register('vscode', 0xec29), + vscodeInsiders: register('vscode-insiders', 0xec2a), + codeOss: register('code-oss', 0xec2b), + runCoverage: register('run-coverage', 0xec2c), runAllCoverage: register('run-all-coverage', 0xec2d), - runCoverage: register('run-all-coverage', 0xec2c), coverage: register('coverage', 0xec2e), githubProject: register('github-project', 0xec2f), - } as const; From e09633b182cd5703001d170c98df4c5756cf52a4 Mon Sep 17 00:00:00 2001 From: Hylke Bons Date: Fri, 1 Mar 2024 20:56:21 +0100 Subject: [PATCH 209/228] codicons: Add back copyright header --- src/vs/base/common/codiconsLibrary.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 4d25fed1023..95886fa6017 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -1,4 +1,8 @@ -import { register } from 'vs/base/common/codiconsUtil' +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { register } from 'vs/base/common/codiconsUtil'; // This file is automatically generated by (microsoft/vscode-codicon)/scripts/export-to-ts.js // Please don't edit it, as your changes will be overwritten. From 6c9d5b3df6767cb0c87d97b0d755b63304b7ac68 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 5 Mar 2024 09:27:28 +0100 Subject: [PATCH 210/228] Custom hovers for input box except when focused (#206873) Customhovers for inputbox but not when focused --- src/vs/base/browser/ui/hover/updatableHoverWidget.ts | 11 +++++++++-- src/vs/base/browser/ui/inputbox/inputBox.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/ui/hover/updatableHoverWidget.ts b/src/vs/base/browser/ui/hover/updatableHoverWidget.ts index c3c505cef39..d36ebab0d95 100644 --- a/src/vs/base/browser/ui/hover/updatableHoverWidget.ts +++ b/src/vs/base/browser/ui/hover/updatableHoverWidget.ts @@ -270,7 +270,14 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); hoverPreparation = toDispose; }; - const focusDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.FOCUS, onFocus, true); + + // Do not show hover when focusing an input or textarea + let focusDomEmitter: undefined | IDisposable; + const tagName = htmlElement.tagName.toLowerCase(); + if (tagName !== 'input' && tagName !== 'textarea') { + focusDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.FOCUS, onFocus, true); + } + const hover: ICustomHover = { show: focus => { hideHover(false, true); // terminate a ongoing mouse over preparation @@ -288,7 +295,7 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); mouseUpEmitter.dispose(); - focusDomEmitter.dispose(); + focusDomEmitter?.dispose(); hideHover(true, true); } }; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index e4c89dd3aff..0a06ece485f 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -11,6 +11,8 @@ import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { Widget } from 'vs/base/browser/ui/widget'; import { IAction } from 'vs/base/common/actions'; @@ -111,6 +113,7 @@ export class InputBox extends Widget { private cachedContentHeight: number | undefined; private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; + private hover: ICustomHover | undefined; private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -230,7 +233,11 @@ export class InputBox extends Widget { public setTooltip(tooltip: string): void { this.tooltip = tooltip; - this.input.title = tooltip; + if (!this.hover) { + this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); + } else { + this.hover.update(tooltip); + } } public setAriaLabel(label: string): void { From 8aca9a53311890fa54f0459c6ef2e8994d4d8ae4 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 5 Mar 2024 10:01:55 +0100 Subject: [PATCH 211/228] JSON Language Server output channel appears twice (#206877) --- .../client/src/browser/jsonClientMain.ts | 9 ++-- .../client/src/jsonClient.ts | 18 ++++--- .../client/src/node/jsonClientMain.ts | 48 ++++--------------- 3 files changed, 23 insertions(+), 52 deletions(-) diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index f7c87fbf9fa..f78f494d727 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, Uri, l10n } from 'vscode'; +import { Disposable, ExtensionContext, Uri, l10n, window } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable } from '../jsonClient'; +import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; declare const Worker: { @@ -43,7 +43,10 @@ export async function activate(context: ExtensionContext) { } }; - client = await startClient(context, newLanguageClient, { schemaRequests, timer }); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); + + client = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel }); } catch (e) { console.log(e); diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index ce81dcb4c9e..ceb081403dc 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -6,7 +6,7 @@ export type JSONLanguageStatus = { schemas: string[] }; import { - workspace, window, languages, commands, OutputChannel, ExtensionContext, extensions, Uri, ColorInformation, + workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n } from 'vscode'; @@ -130,6 +130,7 @@ export interface Runtime { readonly timer: { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; }; + logOutputChannel: LogOutputChannel; } export interface SchemaRequestService { @@ -150,12 +151,10 @@ export interface AsyncDisposable { } export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { - const outputChannel = window.createOutputChannel(languageServerDescription); - const languageParticipants = getLanguageParticipants(); context.subscriptions.push(languageParticipants); - let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); let restartTrigger: Disposable | undefined; languageParticipants.onDidChange(() => { @@ -164,12 +163,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } restartTrigger = runtime.timer.setTimeout(async () => { if (client) { - outputChannel.appendLine('Extensions have changed, restarting JSON server...'); - outputChannel.appendLine(''); + runtime.logOutputChannel.info('Extensions have changed, restarting JSON server...'); + runtime.logOutputChannel.info(''); const oldClient = client; client = undefined; await oldClient.dispose(); - client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); } }, 2000); }); @@ -178,12 +177,11 @@ export async function startClient(context: ExtensionContext, newLanguageClient: dispose: async () => { restartTrigger?.dispose(); await client?.dispose(); - outputChannel.dispose(); } }; } -async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise { +async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { const toDispose: Disposable[] = []; @@ -348,7 +346,7 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa } }; - clientOptions.outputChannel = outputChannel; + clientOptions.outputChannel = runtime.logOutputChannel; // Create the language client and start the client. const client = newLanguageClient('json', languageServerDescription, clientOptions); client.registerProposedFeatures(); diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index 79d66e32dda..d57ebf80834 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode'; +import { Disposable, ExtensionContext, LogOutputChannel, window, l10n, env, LogLevel } from 'vscode'; import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../jsonClient'; import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; @@ -14,15 +14,16 @@ import { xhr, XHRResponse, getErrorStatusDescription, Headers } from 'request-li import TelemetryReporter from '@vscode/extension-telemetry'; import { JSONSchemaCache } from './schemaCache'; -let telemetry: TelemetryReporter | undefined; let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { const clientPackageJSON = await getPackageInfo(context); - telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + const telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + context.subscriptions.push(telemetry); - const outputChannel = window.createOutputChannel(languageServerDescription); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/jsonServerMain`; const serverModule = context.asAbsolutePath(serverMain); @@ -38,11 +39,8 @@ export async function activate(context: ExtensionContext) { }; const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - clientOptions.outputChannel = outputChannel; return new LanguageClient(id, name, serverOptions, clientOptions); }; - const log = getLog(outputChannel); - context.subscriptions.push(log); const timer = { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { @@ -54,9 +52,9 @@ export async function activate(context: ExtensionContext) { // pass the location of the localization bundle to the server process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; - const schemaRequests = await getSchemaRequestService(context, log); + const schemaRequests = await getSchemaRequestService(context, logOutputChannel); - client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer }); + client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer, logOutputChannel }); } export async function deactivate(): Promise { @@ -64,7 +62,6 @@ export async function deactivate(): Promise { await client.dispose(); client = undefined; } - telemetry?.dispose(); } interface IPackageInfo { @@ -84,36 +81,9 @@ async function getPackageInfo(context: ExtensionContext): Promise } } -interface Log { - trace(message: string): void; - isTrace(): boolean; - dispose(): void; -} - -const traceSetting = 'json.trace.server'; -function getLog(outputChannel: OutputChannel): Log { - let trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - const configListener = workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(traceSetting)) { - trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - } - }); - return { - trace(message: string) { - if (trace) { - outputChannel.appendLine(message); - } - }, - isTrace() { - return trace; - }, - dispose: () => configListener.dispose() - }; -} - const retryTimeoutInHours = 2 * 24; // 2 days -async function getSchemaRequestService(context: ExtensionContext, log: Log): Promise { +async function getSchemaRequestService(context: ExtensionContext, log: LogOutputChannel): Promise { let cache: JSONSchemaCache | undefined = undefined; const globalStorage = context.globalStorageUri; @@ -191,7 +161,7 @@ async function getSchemaRequestService(context: ExtensionContext, log: Log): Pro if (cache && /^https?:\/\/json\.schemastore\.org\//.test(uri)) { const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours); if (content) { - if (log.isTrace()) { + if (log.logLevel === LogLevel.Trace) { log.trace(`[json schema cache] Schema ${uri} from cache without request (last accessed ${cache.getLastUpdatedInHours(uri)} hours ago)`); } From 936a283cf93052193361af54aa60d029e0545d77 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 5 Mar 2024 14:54:23 +0100 Subject: [PATCH 212/228] fix #206764 (#206887) --- .../extensions/browser/extensionsViews.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index adcd04b081f..0d7b219e152 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -42,7 +42,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/browser/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; +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'; @@ -183,7 +183,20 @@ export class ExtensionsListView extends ViewPane { const messageBox = append(messageContainer, $('.message')); const delegate = new Delegate(); const extensionsViewState = new ExtensionsViewState(); - const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { hoverOptions: { position: () => { return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; } } }); + const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { + hoverOptions: { + position: () => { + const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); + if (viewLocation === ViewContainerLocation.Sidebar) { + return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } + if (viewLocation === ViewContainerLocation.AuxiliaryBar) { + return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } + return HoverPosition.RIGHT; + } + } + }); this.list = this.instantiationService.createInstance(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { multipleSelectionSupport: false, setRowLineHeight: false, From e3e0fe00a3ab862fab4c0ead4a148eca3064c704 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 5 Mar 2024 20:05:33 +0100 Subject: [PATCH 213/228] integrity - polish warnings (#206896) --- .../electron-sandbox/integrityService.ts | 166 +++++++++++------- 1 file changed, 98 insertions(+), 68 deletions(-) diff --git a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts index 10caaf7ed6a..1834c7f3f53 100644 --- a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts +++ b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts @@ -15,20 +15,22 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { FileAccess, AppResourcePath } from 'vs/base/common/network'; import { IChecksumService } from 'vs/platform/checksum/common/checksumService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; +import { Codicon } from 'vs/base/common/codicons'; interface IStorageData { - dontShowPrompt: boolean; - commit: string | undefined; + readonly dontShowPrompt: boolean; + readonly commit: string | undefined; } class IntegrityStorage { + private static readonly KEY = 'integrityService'; - private storageService: IStorageService; private value: IStorageData | null; - constructor(storageService: IStorageService) { - this.storageService = storageService; + constructor(private readonly storageService: IStorageService) { this.value = this._read(); } @@ -37,6 +39,7 @@ class IntegrityStorage { if (!jsonValue) { return null; } + try { return JSON.parse(jsonValue); } catch (err) { @@ -58,36 +61,108 @@ export class IntegrityService implements IIntegrityService { declare readonly _serviceBrand: undefined; - private _storage: IntegrityStorage; - private _isPurePromise: Promise; + private readonly _storage = new IntegrityStorage(this.storageService); + + private readonly _isPurePromise = this._isPure(); + isPure(): Promise { + return this._isPurePromise; + } constructor( @INotificationService private readonly notificationService: INotificationService, - @IStorageService storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, - @IChecksumService private readonly checksumService: IChecksumService + @IChecksumService private readonly checksumService: IChecksumService, + @ILogService private readonly logService: ILogService, + @IBannerService private readonly bannerService: IBannerService ) { - this._storage = new IntegrityStorage(storageService); - - this._isPurePromise = this._isPure(); - - this.isPure().then(r => { - if (r.isPure) { - return; // all is good - } - - this._prompt(); - }); + this._compute(); } - private _prompt(): void { + private async _compute(): Promise { + const { isPure } = await this.isPure(); + if (isPure) { + return; // all is good + } + + this.logService.warn(` + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!! Installation has been modified on disk and is UNSUPPORTED. Please reinstall !!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +`); + const storedData = this._storage.get(); if (storedData?.dontShowPrompt && storedData.commit === this.productService.commit) { return; // Do not prompt } + this._showBanner(); + this._showNotification(); + } + + private async _isPure(): Promise { + const expectedChecksums = this.productService.checksums || {}; + + await this.lifecycleService.when(LifecyclePhase.Eventually); + + const allResults = await Promise.all(Object.keys(expectedChecksums).map(filename => this._resolve(filename, expectedChecksums[filename]))); + + let isPure = true; + for (let i = 0, len = allResults.length; i < len; i++) { + if (!allResults[i].isPure) { + isPure = false; + break; + } + } + + return { + isPure, + proof: allResults + }; + } + + private async _resolve(filename: AppResourcePath, expected: string): Promise { + const fileUri = FileAccess.asFileUri(filename); + + try { + const checksum = await this.checksumService.checksum(fileUri); + + return IntegrityService._createChecksumPair(fileUri, checksum, expected); + } catch (error) { + return IntegrityService._createChecksumPair(fileUri, '', expected); + } + } + + private static _createChecksumPair(uri: URI, actual: string, expected: string): ChecksumPair { + return { + uri: uri, + actual: actual, + expected: expected, + isPure: (actual === expected) + }; + } + + private _showBanner(): void { + const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; + + this.bannerService.show({ + id: 'installation.corrupt', + message: localize('integrity.banner', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort), + icon: Codicon.warning, + actions: checksumFailMoreInfoUrl ? [ + { + label: localize('integrity.moreInformation', "More Information"), + href: checksumFailMoreInfoUrl + } + ] : undefined + }); + } + + private _showNotification(): void { const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; const message = localize('integrity.prompt', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort); if (checksumFailMoreInfoUrl) { @@ -114,56 +189,11 @@ export class IntegrityService implements IIntegrityService { this.notificationService.notify({ severity: Severity.Warning, message, - sticky: true + sticky: true, + priority: NotificationPriority.URGENT }); } } - - isPure(): Promise { - return this._isPurePromise; - } - - private async _isPure(): Promise { - const expectedChecksums = this.productService.checksums || {}; - - await this.lifecycleService.when(LifecyclePhase.Eventually); - - const allResults = await Promise.all(Object.keys(expectedChecksums).map(filename => this._resolve(filename, expectedChecksums[filename]))); - - let isPure = true; - for (let i = 0, len = allResults.length; i < len; i++) { - if (!allResults[i].isPure) { - isPure = false; - break; - } - } - - return { - isPure: isPure, - proof: allResults - }; - } - - private async _resolve(filename: AppResourcePath, expected: string): Promise { - const fileUri = FileAccess.asFileUri(filename); - - try { - const checksum = await this.checksumService.checksum(fileUri); - - return IntegrityService._createChecksumPair(fileUri, checksum, expected); - } catch (error) { - return IntegrityService._createChecksumPair(fileUri, '', expected); - } - } - - private static _createChecksumPair(uri: URI, actual: string, expected: string): ChecksumPair { - return { - uri: uri, - actual: actual, - expected: expected, - isPure: (actual === expected) - }; - } } registerSingleton(IIntegrityService, IntegrityService, InstantiationType.Delayed); From 8374b219a7ebff8f750abc5df9da9bfc2dc4c067 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 5 Mar 2024 11:41:24 -0800 Subject: [PATCH 214/228] Fix open walkthrough command for selecting a specific step (#206909) --- .../browser/gettingStarted.contribution.ts | 2 +- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index fc6c589ca16..66b9062611f 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -60,7 +60,7 @@ registerAction2(class extends Action2 { if (walkthroughID) { const selectedCategory = typeof walkthroughID === 'string' ? walkthroughID : walkthroughID.category; - const selectedStep = typeof walkthroughID === 'string' ? undefined : walkthroughID.step; + const selectedStep = typeof walkthroughID === 'string' ? undefined : walkthroughID.category + '#' + walkthroughID.step; // We're trying to open the welcome page from the Help menu if (!selectedCategory && !selectedStep) { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 78a07e62ce2..a34712f15cb 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -1159,7 +1159,7 @@ export class GettingStartedPage extends EditorPane { this.editorInput.selectedCategory = categoryID; this.editorInput.selectedStep = stepId; this.currentWalkthrough = ourCategory; - this.buildCategorySlide(categoryID); + this.buildCategorySlide(categoryID, stepId); this.setSlide('details'); }); } From 957ccc60506e170450794caceca70f560f4d14cc Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 5 Mar 2024 11:42:06 -0800 Subject: [PATCH 215/228] Set session id as chat code block authority (#206912) Set session id and chat code block authority --- .../contrib/chat/browser/chatListRenderer.ts | 3 ++- .../workbench/contrib/chat/common/chatViewModel.ts | 2 +- .../chat/common/codeBlockModelCollection.ts | 14 +++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index f97e96548ff..f06f0a9e5a4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -873,7 +873,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { languageId ??= ''; const newText = this.fixCodeText(value, languageId); - const textModel = this.codeBlockModelCollection.getOrCreate(model.id, codeBlockIndex++); + const textModel = this.codeBlockModelCollection.getOrCreate(this._model.sessionId, model.id, codeBlockIndex++); textModel.then(ref => { const model = ref.object.textEditorModel; if (languageId) { diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index edf5b4ae445..763773d71c2 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -25,18 +25,18 @@ export class CodeBlockModelCollection extends Disposable { this.clear(); } - get(responseId: string, codeBlockIndex: number): Promise> | undefined { - const uri = this.getUri(responseId, codeBlockIndex); + get(sessionId: string, responseId: string, codeBlockIndex: number): Promise> | undefined { + const uri = this.getUri(sessionId, responseId, codeBlockIndex); return this._models.get(uri); } - getOrCreate(responseId: string, codeBlockIndex: number): Promise> { - const existing = this.get(responseId, codeBlockIndex); + getOrCreate(sessionId: string, responseId: string, codeBlockIndex: number): Promise> { + const existing = this.get(sessionId, responseId, codeBlockIndex); if (existing) { return existing; } - const uri = this.getUri(responseId, codeBlockIndex); + const uri = this.getUri(sessionId, responseId, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); this._models.set(uri, ref); return ref; @@ -47,7 +47,7 @@ export class CodeBlockModelCollection extends Disposable { this._models.clear(); } - private getUri(responseId: string, index: number): URI { - return URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: `/${responseId}/${index}` }); + private getUri(sessionId: string, responseId: string, index: number): URI { + return URI.from({ scheme: Schemas.vscodeChatCodeBlock, authority: sessionId, path: `/${responseId}/${index}` }); } } From c0300af60aefe6fe121d3bf7111d30ade08f77d6 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 5 Mar 2024 11:48:38 -0800 Subject: [PATCH 216/228] Trigger notebook list rerender when whitespace is dismissed. --- .../browser/view/notebookCellListView.ts | 16 ++++++++++++++-- 1 file changed, 14 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 e7f7d018a80..399934d7c49 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -295,8 +295,20 @@ export class NotebookCellListView extends ListView { } removeWhitespace(id: string): void { - this.notebookRangeMap.removeWhitespace(id); - 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.removeWhitespace(id); + this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); + this._rerender(scrollTop, this.renderHeight, false); + this.eventuallyUpdateScrollDimensions(); + } else { + this.notebookRangeMap.removeWhitespace(id); + this.eventuallyUpdateScrollDimensions(); + } + } getWhitespacePosition(id: string): number { From defc1d52b7f9ca3a7799b722ec3f4d0f1cd4d438 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 5 Mar 2024 12:23:09 -0800 Subject: [PATCH 217/228] Focus stays in generated cell. --- .../controller/chat/cellChatActions.ts | 2 +- .../controller/chat/notebookChatController.ts | 43 +++++++++---------- 2 files changed, 22 insertions(+), 23 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 430062261c9..faca219b69c 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -209,7 +209,7 @@ registerAction2(class extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.dismiss(); + NotebookChatController.get(context.notebookEditor)?.dismiss(false); } }); 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 93a98b32d60..d4b59189404 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -702,7 +702,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._inlineChatSessionService.releaseSession(this._activeSession); } catch (_err) { } - this.dismiss(); + this.dismiss(false); } async focusAbove() { @@ -772,7 +772,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._strategy?.cancel(); this._activeRequestCts?.cancel(); this._widget?.discardChange(); - this.dismiss(); + this.dismiss(true); } async feedbackLast(kind: InlineChatResponseFeedbackKind) { @@ -783,25 +783,25 @@ export class NotebookChatController extends Disposable implements INotebookEdito } - dismiss() { - // move focus back to the cell above - if (this._widget) { - const widgetIndex = this._widget.afterModelPosition; - const currentFocus = this._notebookEditor.getFocus(); + dismiss(discard: boolean) { + const widget = this._widget; + const widgetIndex = widget?.afterModelPosition; + const currentFocus = this._notebookEditor.getFocus(); + const isWidgetFocused = currentFocus.start === widgetIndex && currentFocus.end === widgetIndex; - if (currentFocus.start === widgetIndex && currentFocus.end === widgetIndex) { - // focus is on the widget - if (widgetIndex === 0) { - // on top of all cells - if (this._notebookEditor.getLength() > 0) { - this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(0)!, 'container'); - } - } else { - const cell = this._notebookEditor.cellAt(widgetIndex - 1); - if (cell) { - this._notebookEditor.focusNotebookCell(cell, 'container'); - } - } + if (widget && isWidgetFocused) { + // change focus only when the widget is focused + const editingCell = widget.getEditingCell(); + const shouldFocusEditingCell = editingCell && !discard; + const shouldFocusTopCell = widgetIndex === 0 && this._notebookEditor.getLength() > 0; + const shouldFocusAboveCell = widgetIndex !== 0 && this._notebookEditor.cellAt(widgetIndex - 1); + + if (shouldFocusEditingCell) { + this._notebookEditor.focusNotebookCell(editingCell, 'container'); + } else if (shouldFocusTopCell) { + this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(0)!, 'container'); + } else if (shouldFocusAboveCell) { + this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(widgetIndex - 1)!, 'container'); } } @@ -815,8 +815,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito } public override dispose(): void { - this.dismiss(); - + this.dismiss(false); super.dispose(); } } From 1be73b4a517ad5c4b6361b1fdbeedb8fc6d0d3a1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 6 Mar 2024 01:18:59 +0100 Subject: [PATCH 218/228] Update extensions compatible with newly available VS Code version (#206924) #125417 update extensions compatible with newly available VS Code version --- .../abstractExtensionManagementService.ts | 38 ++--- .../common/extensionGalleryService.ts | 21 +-- .../common/extensionManagement.ts | 14 +- .../common/extensionManagementIpc.ts | 8 +- .../common/extensionsProfileScannerService.ts | 20 ++- .../common/extensionsScannerService.ts | 28 ++-- .../node/extensionManagementService.ts | 27 ++-- .../node/installGalleryExtensionTask.test.ts | 2 +- src/vs/platform/update/common/update.ts | 1 + .../electron-main/updateService.darwin.ts | 4 +- .../extensions/browser/extensionsActions.ts | 45 +++++- .../extensions/browser/extensionsViewlet.ts | 12 +- .../extensions/browser/extensionsViews.ts | 10 +- .../extensions/browser/extensionsWidgets.ts | 8 +- .../browser/extensionsWorkbenchService.ts | 142 ++++++++++++------ .../contrib/extensions/common/extensions.ts | 11 +- .../extensionRecommendationsService.test.ts | 4 +- .../extensionsActions.test.ts | 20 +-- .../electron-sandbox/extensionsViews.test.ts | 4 +- .../extensionsWorkbenchService.test.ts | 4 +- .../extensionManagementChannelClient.ts | 6 +- .../common/extensionManagementService.ts | 7 +- .../common/webExtensionManagementService.ts | 6 +- .../remoteExtensionManagementService.ts | 2 +- 24 files changed, 280 insertions(+), 164 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index d0cedb8d962..f7c4a89d735 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -16,7 +16,8 @@ import * as nls from 'vs/nls'; 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 + InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -29,7 +30,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use export type ExtensionVerificationStatus = boolean | string; export type InstallableExtension = { readonly manifest: IExtensionManifest; extension: IGalleryExtension | URI; options: InstallOptions }; -export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI }; +export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI; readonly productVersion: IProductVersion }; export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; @@ -124,7 +125,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl await Promise.allSettled(extensions.map(async ({ extension, options }) => { try { - const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion); + const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion, options.productVersion ?? { version: this.productService.version, date: this.productService.date }); installableExtensions.push({ ...compatible, options }); } catch (error) { results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error }); @@ -230,7 +231,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl ...options, installOnlyNewlyAddedFromExtensionPack: options.installOnlyNewlyAddedFromExtensionPack ?? !URI.isUri(extension) /* always true for gallery extensions */, isApplicationScoped, - profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation() + profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation(), + productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }; const existingInstallExtensionTask = !URI.isUri(extension) ? this.installingExtensions.get(getInstallExtensionTaskKey(extension, installExtensionTaskOptions.profileLocation)) : undefined; @@ -248,8 +250,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id); } else { try { - const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation); - const installed = await this.getInstalled(undefined, task.options.profileLocation); + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation, task.options.productVersion); + const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion); const options: InstallExtensionTaskOptions = { ...task.options, donotIncludePackAndDependencies: true, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) { if (installingExtensionsMap.has(`${gallery.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`)) { @@ -405,12 +407,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return results; } - private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { + private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { if (!this.galleryService.isEnabled()) { return []; } - const installed = await this.getInstalled(undefined, profile); + const installed = await this.getInstalled(undefined, profile, productVersion); const knownIdentifiers: IExtensionIdentifier[] = []; const allDependenciesAndPacks: { gallery: IGalleryExtension; manifest: IExtensionManifest }[] = []; @@ -442,7 +444,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier)); let compatible; try { - compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease); + compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease, productVersion); } catch (error) { if (!isDependency) { this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id, getErrorMessage(error)); @@ -462,7 +464,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return allDependenciesAndPacks; } - private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { + private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean, productVersion: IProductVersion): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { let compatibleExtension: IGalleryExtension | null; const extensionsControlManifest = await this.getExtensionsControlManifest(); @@ -473,7 +475,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const deprecationInfo = extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()]; if (deprecationInfo?.extension?.autoMigrate) { this.logService.info(`The '${extension.identifier.id}' extension is deprecated, fetching the compatible '${deprecationInfo.extension.id}' extension instead.`); - compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true }, CancellationToken.None))[0]; + compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true, productVersion }, CancellationToken.None))[0]; if (!compatibleExtension) { throw new ExtensionManagementError(nls.localize('notFoundDeprecatedReplacementExtension', "Can't install '{0}' extension since it was deprecated and the replacement extension '{1}' can't be found.", extension.identifier.id, deprecationInfo.extension.id), ExtensionManagementErrorCode.Deprecated); } @@ -485,7 +487,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform); } - compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease); + compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion); if (!compatibleExtension) { /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { @@ -508,23 +510,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return { extension: compatibleExtension, manifest }; } - protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise { + protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise { const targetPlatform = await this.getTargetPlatform(); let compatibleExtension: IGalleryExtension | null = null; if (!sameVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) { - compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null; } - if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) { + if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform, productVersion)) { compatibleExtension = extension; } if (!compatibleExtension) { if (sameVersion) { - compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null; } else { - compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform); + compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform, productVersion); } } @@ -718,7 +720,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract install(vsix: URI, options?: InstallOptions): Promise; abstract installFromLocation(location: URI, profileLocation: URI): Promise; abstract installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; - abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + abstract getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; abstract copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; abstract download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; abstract reinstallFromGallery(extension: ILocalExtension): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 8833103e59c..1bec23931b3 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -295,6 +295,7 @@ type GalleryServiceAdditionalQueryEvent = { }; interface IExtensionCriteria { + readonly productVersion: IProductVersion; readonly targetPlatform: TargetPlatform; readonly compatible: boolean; readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[]; @@ -662,14 +663,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi query = query.withSource(options.source); } - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible }, token); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); if (options.source) { extensions.forEach((e, index) => setTelemetry(e, index, options.source)); } return extensions; } - async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) { return null; } @@ -680,11 +681,11 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi .withFlags(Flags.IncludeVersions) .withPage(1, 1) .withFilter(FilterType.ExtensionId, extension.identifier.uuid); - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease }, CancellationToken.None); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease, productVersion }, CancellationToken.None); return extensions[0] || null; } - async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) { return false; } @@ -702,10 +703,10 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } engine = manifest.engines.vscode; } - return isEngineValid(engine, this.productService.version, this.productService.date); + return isEngineValid(engine, productVersion.version, productVersion.date); } - private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise { + private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), allTargetPlatforms, targetPlatform)) { return false; } @@ -717,7 +718,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (compatible) { try { const engine = await this.getEngine(rawGalleryExtensionVersion); - if (!isEngineValid(engine, this.productService.version, this.productService.date)) { + if (!isEngineValid(engine, productVersion.version, productVersion.date)) { return false; } } catch (error) { @@ -784,7 +785,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } const runQuery = async (query: Query, token: CancellationToken) => { - const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease }, token); + const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source)); return { extensions, total }; }; @@ -913,7 +914,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } // Allow any version if includePreRelease flag is set otherwise only release versions are allowed - if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform)) { + if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); } if (version && rawGalleryExtensionVersion.version === version) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 09a2b1dddcd..30c4f1c51eb 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -20,6 +20,11 @@ export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; +export interface IProductVersion { + readonly version: string; + readonly date?: string; +} + export function TargetPlatformToString(targetPlatform: TargetPlatform) { switch (targetPlatform) { case TargetPlatform.WIN32_X64: return 'Windows 64 bit'; @@ -281,6 +286,7 @@ export interface IQueryOptions { sortOrder?: SortOrder; source?: string; includePreRelease?: boolean; + productVersion?: IProductVersion; } export const enum StatisticType { @@ -330,6 +336,7 @@ export interface IExtensionInfo extends IExtensionIdentifier { export interface IExtensionQueryOptions { targetPlatform?: TargetPlatform; + productVersion?: IProductVersion; compatible?: boolean; queryAllVersions?: boolean; source?: string; @@ -347,8 +354,8 @@ export interface IExtensionGalleryService { query(options: IQueryOptions, token: CancellationToken): Promise>; getExtensions(extensionInfos: ReadonlyArray, token: CancellationToken): Promise; getExtensions(extensionInfos: ReadonlyArray, options: IExtensionQueryOptions, token: CancellationToken): Promise; - isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; - getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; + isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; + getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; @@ -454,6 +461,7 @@ export type InstallOptions = { operation?: InstallOperation; profileLocation?: URI; installOnlyNewlyAddedFromExtensionPack?: boolean; + productVersion?: IProductVersion; /** * Context passed through to InstallExtensionResult */ @@ -490,7 +498,7 @@ export interface IExtensionManagementService { uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; - getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index c4403134b93..b5f0317af55 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -139,7 +139,7 @@ export class ExtensionManagementChannel implements IServerChannel { return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); } case 'getInstalled': { - const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer)); + const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer), args[2]); return extensions.map(e => transformOutgoingExtension(e, uriTransformer)); } case 'toggleAppliationScope': { @@ -271,8 +271,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('reinstallFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } - getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI): Promise { - return Promise.resolve(this.channel.call('getInstalled', [type, extensionsProfileResource])) + getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI, productVersion?: IProductVersion): Promise { + return Promise.resolve(this.channel.call('getInstalled', [type, extensionsProfileResource, productVersion])) .then(extensions => extensions.map(extension => transformIncomingExtension(extension, null))); } diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 4fcf403d999..fddaf329af0 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -83,7 +83,7 @@ export interface IExtensionsProfileScannerService { readonly onDidRemoveExtensions: Event; scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise; - addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; + addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise; updateMetadata(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise; } @@ -120,18 +120,22 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable return this.withProfileExtensions(profileLocation, undefined, options); } - async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise { + async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise { const extensionsToRemove: IScannedProfileExtension[] = []; const extensionsToAdd: IScannedProfileExtension[] = []; try { await this.withProfileExtensions(profileLocation, existingExtensions => { const result: IScannedProfileExtension[] = []; - for (const existing of existingExtensions) { - if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { - // Remove the existing extension with different version - extensionsToRemove.push(existing); - } else { - result.push(existing); + if (keepExistingVersions) { + result.push(...existingExtensions); + } else { + for (const existing of existingExtensions) { + if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { + // Remove the existing extension with different version + extensionsToRemove.push(existing); + } else { + result.push(existing); + } } } for (const [extension, metadata] of extensions) { diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 4ac28d17b5f..1174b510b51 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -22,7 +22,7 @@ import { isEmptyObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductVersion, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; @@ -108,6 +108,7 @@ export type ScanOptions = { readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; + readonly productVersion?: IProductVersion; }; export const IExtensionsScannerService = createDecorator('IExtensionsScannerService'); @@ -195,7 +196,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const location = scanOptions.profileLocation ?? this.userExtensionsLocation; this.logService.trace('Started scanning user extensions', location); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions); + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { @@ -217,7 +218,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -233,7 +234,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -245,7 +246,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); return this.applyScanOptions(extensions, extensionType, scanOptions, true); } @@ -392,7 +393,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()); const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); @@ -422,7 +423,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem break; } } - const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined))))); + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()))))); this.logService.trace('Scanned dev system extensions:', result.length); return coalesce(result); } @@ -436,7 +437,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } - private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined): Promise { + private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { const translations = await this.getTranslations(language ?? platform.language); const mtime = await this.getMtime(location); const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; @@ -451,8 +452,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem type, excludeObsolete, validate, - this.productService.version, - this.productService.date, + productVersion.version, + productVersion.date, this.productService.commit, !this.environmentService.isBuilt, language, @@ -472,6 +473,13 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return undefined; } + private getProductVersion(): IProductVersion { + return { + version: this.productService.version, + date: this.productService.date, + }; + } + } export class ExtensionScannerInput { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index bb2828a935f..3d7c9420e50 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -28,7 +28,8 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, - Metadata, InstallOptions + Metadata, InstallOptions, + IProductVersion } 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'; @@ -128,8 +129,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } - getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { - return this.extensionsScanner.scanExtensions(type ?? null, profileLocation); + getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { + return this.extensionsScanner.scanExtensions(type ?? null, profileLocation, productVersion); } scanAllUserInstalledExtensions(): Promise { @@ -179,7 +180,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { this.logService.trace('ExtensionManagementService#installExtensionsFromProfile', extensions, fromProfileLocation.toString(), toProfileLocation.toString()); - const extensionsToInstall = (await this.extensionsScanner.scanExtensions(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); + const extensionsToInstall = (await this.getInstalled(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); if (extensionsToInstall.length) { const metadata = await Promise.all(extensionsToInstall.map(e => this.extensionsScanner.scanMetadata(e, fromProfileLocation))); await this.addExtensionsToProfile(extensionsToInstall.map((e, index) => [e, metadata[index]]), toProfileLocation); @@ -236,7 +237,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { - return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation); + return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date }); } markAsUninstalled(...extensions: IExtension[]): Promise { @@ -333,7 +334,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } if (added) { - const extensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, added.profileLocation); + const extensions = await this.getInstalled(ExtensionType.User, added.profileLocation); const addedExtensions = extensions.filter(e => added.extensions.some(identifier => areSameExtensions(identifier, e.identifier))); this._onDidInstallExtensions.fire(addedExtensions.map(local => { this.logService.info('Extensions added from another source', local.identifier.id, added.profileLocation.toString()); @@ -449,8 +450,8 @@ export class ExtensionsScanner extends Disposable { await this.removeUninstalledExtensions(); } - async scanExtensions(type: ExtensionType | null, profileLocation: URI): Promise { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation }; + 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)); @@ -613,8 +614,8 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(extension.location, extension.type, toProfileLocation); } - async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { - const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation); + async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, productVersion: IProductVersion): Promise { + const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation, productVersion); const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(fromExtensions .filter(e => !e.isApplicationScoped) /* remove application scoped extensions */ .map(async e => ([e, await this.scanMetadata(e, fromProfileLocation)]))); @@ -819,7 +820,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { let installed; try { - installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation); + installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); } catch (error) { throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); } @@ -969,7 +970,7 @@ class InstallVSIXTask extends InstallExtensionTask { 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); + 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, diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts index 1767e8931cb..7c40a4af114 100644 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -102,7 +102,7 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { engines: { vscode: '*' }, }, extension, - { profileLocation: userDataProfilesService.defaultProfile.extensionsResource }, + { profileLocation: userDataProfilesService.defaultProfile.extensionsResource, productVersion: { version: '' } }, extensionDownloader, new TestExtensionsScanner(), uriIdentityService, diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 4cc8994bd01..02aeac681b5 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -9,6 +9,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface IUpdate { version: string; productVersion: string; + timestamp?: number; url?: string; sha256hash?: string; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 329488abb51..d79c9b6927a 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -24,8 +24,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } - @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version) => ({ url, version, productVersion: version })); } - @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, date) => ({ releaseNotes, version, productVersion: version, date })); } + @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version, timestamp) => ({ url, version, productVersion: version, timestamp })); } + @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, version, timestamp) => ({ version, productVersion: version, timestamp })); } constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 1ff80d2256d..8a4056cdf9b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { disposeIfDisposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -73,6 +73,7 @@ import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConst import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IUpdateService } from 'vs/platform/update/common/update'; export class PromptExtensionInstallFailureAction extends Action { @@ -1562,7 +1563,9 @@ export class ReloadAction extends ExtensionAction { constructor( @IHostService private readonly hostService: IHostService, + @IUpdateService private readonly updateService: IUpdateService, @IExtensionService private readonly extensionService: IExtensionService, + @IProductService private readonly productService: IProductService, ) { super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); @@ -1572,27 +1575,53 @@ export class ReloadAction extends ExtensionAction { update(): void { this.enabled = false; this.tooltip = ''; + this.class = ReloadAction.DisabledClass; + if (!this.extension) { return; } + const state = this.extension.state; if (state === ExtensionState.Installing || state === ExtensionState.Uninstalling) { return; } + if (this.extension.local && this.extension.local.manifest && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.localizations && this.extension.local.manifest.contributes.localizations.length > 0) { return; } - const reloadTooltip = this.extension.reloadRequiredStatus; - this.enabled = reloadTooltip !== undefined; - this.label = reloadTooltip !== undefined ? localize('reload required', 'Reload Required') : ''; - this.tooltip = reloadTooltip !== undefined ? reloadTooltip : ''; + const runtimeState = this.extension.runtimeState; + if (!runtimeState) { + return; + } - this.class = this.enabled ? ReloadAction.EnabledClass : ReloadAction.DisabledClass; + this.enabled = true; + this.class = ReloadAction.EnabledClass; + this.tooltip = runtimeState.reason; + this.label = runtimeState.action === ExtensionRuntimeActionType.Reload ? localize('reload required', 'Reload {0}', this.productService.nameShort) + : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart {0}', this.productService.nameShort) + : runtimeState.action === ExtensionRuntimeActionType.ApplyUpdate || runtimeState.action === ExtensionRuntimeActionType.DownloadUpdate ? localize('update product', 'Update {0}', this.productService.nameShort) : ''; } - override run(): Promise { - return Promise.resolve(this.hostService.reload()); + override async run(): Promise { + const runtimeState = this.extension?.runtimeState; + + if (runtimeState?.action === ExtensionRuntimeActionType.Reload) { + return this.hostService.reload(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.DownloadUpdate) { + return this.updateService.downloadUpdate(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.ApplyUpdate) { + return this.updateService.applyUpdate(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.QuitAndInstall) { + return this.updateService.quitAndInstall(); + } + } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 38c0e666af1..43c231d7c45 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -853,19 +853,19 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution private onServiceChange(): void { this.badgeHandle.clear(); - const extensionsReloadRequired = this.extensionsWorkbenchService.installed.filter(e => e.reloadRequiredStatus !== undefined); - const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !extensionsReloadRequired.includes(e) ? 1 : 0), 0); - const newBadgeNumber = outdated + extensionsReloadRequired.length; + const actionRequired = this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); + const newBadgeNumber = outdated + actionRequired.length; if (newBadgeNumber > 0) { let msg = ''; if (outdated) { msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated); } - if (outdated > 0 && extensionsReloadRequired.length > 0) { + if (outdated > 0 && actionRequired.length > 0) { msg += ', '; } - if (extensionsReloadRequired.length) { - msg += extensionsReloadRequired.length === 1 ? localize('extensionToReload', '{0} requires reload', extensionsReloadRequired.length) : localize('extensionsToReload', '{0} require reload', extensionsReloadRequired.length); + if (actionRequired.length) { + msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length); } const badge = new NumberBadge(newBadgeNumber, () => msg); this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 0d7b219e152..8e64f3bacaa 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -522,7 +522,7 @@ export class ExtensionsListView extends ViewPane { result = local.filter(e => !e.isBuiltin && matchingText(e)); result = this.sortExtensions(result, options); } else { - result = local.filter(e => (!e.isBuiltin || e.outdated || e.reloadRequiredStatus !== undefined) && matchingText(e)); + result = local.filter(e => (!e.isBuiltin || e.outdated || e.runtimeState !== undefined) && matchingText(e)); const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(e.identifier.value, e); return result; }, new ExtensionIdentifierMap()); const defaultSort = (e1: IExtension, e2: IExtension) => { @@ -551,21 +551,21 @@ export class ExtensionsListView extends ViewPane { }; const outdated: IExtension[] = []; - const reloadRequired: IExtension[] = []; + const actionRequired: IExtension[] = []; const noActionRequired: IExtension[] = []; result.forEach(e => { if (e.outdated) { outdated.push(e); } - else if (e.reloadRequiredStatus) { - reloadRequired.push(e); + else if (e.runtimeState) { + actionRequired.push(e); } else { noActionRequired.push(e); } }); - result = [...outdated.sort(defaultSort), ...reloadRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; + result = [...outdated.sort(defaultSort), ...actionRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; } return result; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 9245e2b8b4f..01bd5f4b89b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -616,10 +616,10 @@ export class ExtensionHoverWidget extends ExtensionWidget { const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension); const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension); const extensionStatus = this.extensionStatusAction.status; - const reloadRequiredMessage = this.extension.reloadRequiredStatus; + const runtimeState = this.extension.runtimeState; const recommendationMessage = this.getRecommendationMessage(this.extension); - if (extensionRuntimeStatus || extensionStatus || reloadRequiredMessage || recommendationMessage || preReleaseMessage) { + if (extensionRuntimeStatus || extensionStatus || runtimeState || recommendationMessage || preReleaseMessage) { markdown.appendMarkdown(`---`); markdown.appendText(`\n`); @@ -656,9 +656,9 @@ export class ExtensionHoverWidget extends ExtensionWidget { markdown.appendText(`\n`); } - if (reloadRequiredMessage) { + if (runtimeState) { markdown.appendMarkdown(`$(${infoIcon.id}) `); - markdown.appendMarkdown(`${reloadRequiredMessage}`); + markdown.appendMarkdown(`${runtimeState.reason}`); markdown.appendText(`\n`); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 12460b0406d..bab4b2cc8c1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -16,7 +16,7 @@ import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, WEB_EXTENSION_TAG, InstallExtensionResult, IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, - InstallOptions + InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; @@ -54,6 +54,7 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { mainWindow } from 'vs/base/browser/window'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; +import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; interface IExtensionStateProvider { (extension: Extension): T; @@ -76,7 +77,7 @@ export class Extension implements IExtension { constructor( private stateProvider: IExtensionStateProvider, - private runtimeStateProvider: IExtensionStateProvider, + private runtimeStateProvider: IExtensionStateProvider, public readonly server: IExtensionManagementServer | undefined, public local: ILocalExtension | undefined, public gallery: IGalleryExtension | undefined, @@ -274,7 +275,7 @@ export class Extension implements IExtension { && semver.eq(this.latestVersion, this.version); } - get reloadRequiredStatus(): string | undefined { + get runtimeState(): ExtensionRuntimeState | undefined { return this.runtimeStateProvider(this); } @@ -463,7 +464,7 @@ class Extensions extends Disposable { constructor( readonly server: IExtensionManagementServer, private readonly stateProvider: IExtensionStateProvider, - private readonly runtimeStateProvider: IExtensionStateProvider, + private readonly runtimeStateProvider: IExtensionStateProvider, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -496,15 +497,14 @@ class Extensions extends Disposable { return this._local; } - async queryInstalled(): Promise { - await this.fetchInstalledExtensions(); + async queryInstalled(productVersion: IProductVersion): Promise { + await this.fetchInstalledExtensions(productVersion); this._onChange.fire(undefined); return this.local; } - async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[]): Promise { - let hasChanged: boolean = false; - const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions); + async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise { + const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions, productVersion); for (const [extension, gallery] of extensions) { // update metadata of the extension if it does not exist if (extension.local && !extension.local.identifier.uuid) { @@ -513,20 +513,18 @@ class Extensions extends Disposable { if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) { extension.gallery = gallery; this._onChange.fire({ extension }); - hasChanged = true; } } - return hasChanged; } - private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[]): Promise<[Extension, IGalleryExtension][]> { + private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise<[Extension, IGalleryExtension][]> { const mappedExtensions = this.mapInstalledExtensionWithGalleryExtension(galleryExtensions); const targetPlatform = await this.server.extensionManagementService.getTargetPlatform(); const compatibleGalleryExtensions: IGalleryExtension[] = []; const compatibleGalleryExtensionsToFetch: IExtensionInfo[] = []; await Promise.allSettled(mappedExtensions.map(async ([extension, gallery]) => { if (extension.local) { - if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform)) { + if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform, productVersion)) { compatibleGalleryExtensions.push(gallery); } else { compatibleGalleryExtensionsToFetch.push({ ...extension.local.identifier, preRelease: extension.local.preRelease }); @@ -534,7 +532,7 @@ class Extensions extends Disposable { } })); if (compatibleGalleryExtensionsToFetch.length) { - const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true }, CancellationToken.None); + const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true, productVersion }, CancellationToken.None); compatibleGalleryExtensions.push(...result); } return this.mapInstalledExtensionWithGalleryExtension(compatibleGalleryExtensions); @@ -591,9 +589,9 @@ class Extensions extends Disposable { } } - private async fetchInstalledExtensions(): Promise { + private async fetchInstalledExtensions(productVersion?: IProductVersion): Promise { const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest(); - const all = await this.server.extensionManagementService.getInstalled(); + const all = await this.server.extensionManagementService.getInstalled(undefined, undefined, productVersion); // dedup user and system extensions by giving priority to user extensions. const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => { @@ -793,19 +791,19 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { - this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.localExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.localExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.localExtensions); } if (extensionManagementServerService.remoteExtensionManagementServer) { - this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.remoteExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.remoteExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.remoteExtensions); } if (extensionManagementServerService.webExtensionManagementServer) { - this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.webExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.webExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.webExtensions); @@ -861,8 +859,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); this._register(this.updateService.onStateChange(e => { - if ((e.type === StateType.AvailableForDownload || e.type === StateType.Downloading) && this.isAutoUpdateEnabled()) { - this.checkForUpdates(); + if (!this.isAutoUpdateEnabled()) { + return; + } + if ((e.type === StateType.CheckingForUpdates && e.explicit) || e.type === StateType.AvailableForDownload || e.type === StateType.Downloading) { + this.eventuallyCheckForUpdates(true); } })); @@ -964,19 +965,19 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension async queryLocal(server?: IExtensionManagementServer): Promise { if (server) { if (this.localExtensions && this.extensionManagementServerService.localExtensionManagementServer === server) { - return this.localExtensions.queryInstalled(); + return this.localExtensions.queryInstalled(this.getProductVersion()); } if (this.remoteExtensions && this.extensionManagementServerService.remoteExtensionManagementServer === server) { - return this.remoteExtensions.queryInstalled(); + return this.remoteExtensions.queryInstalled(this.getProductVersion()); } if (this.webExtensions && this.extensionManagementServerService.webExtensionManagementServer === server) { - return this.webExtensions.queryInstalled(); + return this.webExtensions.queryInstalled(this.getProductVersion()); } } if (this.localExtensions) { try { - await this.localExtensions.queryInstalled(); + await this.localExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -984,7 +985,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (this.remoteExtensions) { try { - await this.remoteExtensions.queryInstalled(); + await this.remoteExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -992,7 +993,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (this.webExtensions) { try { - await this.webExtensions.queryInstalled(); + await this.webExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -1068,7 +1069,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension { let extension = this.getInstalledExtensionMatchingGallery(gallery); if (!extension) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); } return extension; @@ -1110,7 +1111,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } - private getReloadStatus(extension: IExtension): string | undefined { + private getRuntimeState(extension: IExtension): ExtensionRuntimeState | undefined { const isUninstalled = extension.state === ExtensionState.Uninstalled; const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier)); @@ -1118,7 +1119,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); const isSameExtensionRunning = runningExtension && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); if (!canRemoveRunningExtension && isSameExtensionRunning && !runningExtension.isUnderDevelopment) { - return nls.localize('postUninstallTooltip', "Please reload Visual Studio Code to complete the uninstallation of this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postUninstallTooltip', "Please reload {0} to complete the uninstallation of this extension.", this.productService.nameLong) }; } return undefined; } @@ -1138,7 +1139,25 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isSameExtensionRunning) { // Different version or target platform of same extension is running. Requires reload to run the current version if (!runningExtension.isUnderDevelopment && (extension.version !== runningExtension.version || extension.local.targetPlatform !== runningExtension.targetPlatform)) { - return nls.localize('postUpdateTooltip', "Please reload Visual Studio Code to enable the updated extension."); + const productCurrentVersion = this.getProductCurrentVersion(); + const productUpdateVersion = this.getProductUpdateVersion(); + if (productUpdateVersion + && !isEngineValid(extension.local.manifest.engines.vscode, productCurrentVersion.version, productCurrentVersion.date) + && isEngineValid(extension.local.manifest.engines.vscode, productUpdateVersion.version, productUpdateVersion.date) + ) { + const state = this.updateService.state; + if (state.type === StateType.AvailableForDownload) { + return { action: ExtensionRuntimeActionType.DownloadUpdate, reason: nls.localize('postUpdateDownloadTooltip', "Please update {0} to enable the updated extension.", this.productService.nameLong) }; + } + if (state.type === StateType.Downloaded) { + return { action: ExtensionRuntimeActionType.ApplyUpdate, reason: nls.localize('postUpdateUpdateTooltip', "Please update {0} to enable the updated extension.", this.productService.nameLong) }; + } + if (state.type === StateType.Ready) { + return { action: ExtensionRuntimeActionType.QuitAndInstall, reason: nls.localize('postUpdateRestartTooltip', "Please restart {0} to enable the updated extension.", this.productService.nameLong) }; + } + return undefined; + } + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postUpdateTooltip', "Please reload {0} to enable the updated extension.", this.productService.nameLong) }; } if (this.extensionsServers.length > 1) { @@ -1146,12 +1165,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensionInOtherServer) { // This extension prefers to run on UI/Local side but is running in remote if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.localExtensionManagementServer) { - return nls.localize('enable locally', "Please reload Visual Studio Code to enable this extension locally."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('enable locally', "Please reload {0} to enable this extension locally.", this.productService.nameLong) }; } // This extension prefers to run on Workspace/Remote side but is running in local if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.remoteExtensionManagementServer) { - return nls.localize('enable remote', "Please reload Visual Studio Code to enable this extension in {0}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('enable remote', "Please reload {0} to enable this extension in {1}.", this.productService.nameLong, this.extensionManagementServerService.remoteExtensionManagementServer?.label) }; } } } @@ -1161,20 +1180,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { // This extension prefers to run on UI/Local side but is running in remote if (this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { // This extension prefers to run on Workspace/Remote side but is running in local if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } } return undefined; } else { if (isSameExtensionRunning) { - return nls.localize('postDisableTooltip', "Please reload Visual Studio Code to disable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postDisableTooltip', "Please reload {0} to disable this extension.", this.productService.nameLong) }; } } return undefined; @@ -1183,7 +1202,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Extension is not running else { if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } const otherServer = extension.server ? extension.server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer : null; @@ -1191,7 +1210,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const extensionInOtherServer = this.local.filter(e => areSameExtensions(e.identifier, extension.identifier) && e.server === otherServer)[0]; // Same extension in other server exists and if (extensionInOtherServer && extensionInOtherServer.local && this.extensionEnablementService.isEnabled(extensionInOtherServer.local)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } } @@ -1376,7 +1395,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.telemetryService.publicLog2('galleryService:checkingForUpdates', { count: infos.length, }); - const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true }, CancellationToken.None); + const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true, productVersion: this.getProductVersion() }, CancellationToken.None); if (galleryExtensions.length) { await this.syncInstalledExtensionsWithGallery(galleryExtensions); } @@ -1414,8 +1433,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!extensions.length) { return; } - const result = await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery))); - if (this.isAutoUpdateEnabled() && result.some(r => r.status === 'fulfilled' && r.value)) { + await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery, this.getProductVersion()))); + if (this.isAutoUpdateEnabled()) { this.eventuallyAutoUpdateExtensions(); } } @@ -1434,12 +1453,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private eventuallyCheckForUpdates(immediate = false): void { + this.updatesCheckDelayer.cancel(); this.updatesCheckDelayer.trigger(async () => { if (this.isAutoUpdateEnabled() || this.isAutoCheckUpdatesEnabled()) { await this.checkForUpdates(); } this.eventuallyCheckForUpdates(); - }, immediate ? 0 : ExtensionsWorkbenchService.UpdatesCheckInterval).then(undefined, err => null); + }, immediate ? 0 : this.getUpdatesCheckInterval()).then(undefined, err => null); + } + + private getUpdatesCheckInterval(): number { + if (this.productService.quality === 'insider' && this.getProductUpdateVersion()) { + return 1000 * 60 * 60 * 1; // 1 hour + } + return ExtensionsWorkbenchService.UpdatesCheckInterval; } private eventuallyAutoUpdateExtensions(): void { @@ -1474,8 +1501,32 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } const toUpdate = this.outdated.filter(e => !e.local?.pinned && this.shouldAutoUpdateExtension(e)); + if (!toUpdate.length) { + return; + } - await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true } : undefined))); + const productVersion = this.getProductVersion(); + await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true, productVersion } : { productVersion }))); + } + + private getProductVersion(): IProductVersion { + return this.getProductUpdateVersion() ?? this.getProductCurrentVersion(); + } + + private getProductCurrentVersion(): IProductVersion { + return { version: this.productService.version, date: this.productService.date }; + } + + private getProductUpdateVersion(): IProductVersion | undefined { + switch (this.updateService.state.type) { + case StateType.AvailableForDownload: + case StateType.Downloading: + case StateType.Downloaded: + case StateType.Updating: + case StateType.Ready: + return { version: this.updateService.state.update.version, date: this.updateService.state.update.timestamp ? new Date(this.updateService.state.update.timestamp).toISOString() : undefined }; + } + return undefined; } private async updateExtensionsPinnedState(): Promise { @@ -1682,7 +1733,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension gallery = firstOrDefault(await this.galleryService.getExtensions([installableInfo], { targetPlatform }, CancellationToken.None)); } if (!extension && gallery) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery); Extensions.updateExtensionFromControlManifest(extension as Extension, await this.extensionManagementService.getExtensionsControlManifest()); } if (extension?.isMalicious) { @@ -1953,6 +2004,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension installOptions = installOptions ?? {}; installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); if (extension.local) { + installOptions.productVersion = this.getProductVersion(); return this.extensionManagementService.updateFromGallery(gallery, extension.local, installOptions); } else { return this.extensionManagementService.installFromGallery(gallery, installOptions); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 0158101cc31..b523d97d6b2 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -39,6 +39,15 @@ export const enum ExtensionState { Uninstalled } +export const enum ExtensionRuntimeActionType { + Reload = 'reload', + DownloadUpdate = 'downloadUpdate', + ApplyUpdate = 'applyUpdate', + QuitAndInstall = 'quitAndInstall', +} + +export type ExtensionRuntimeState = { action: ExtensionRuntimeActionType; reason: string }; + export interface IExtension { readonly type: ExtensionType; readonly isBuiltin: boolean; @@ -69,7 +78,7 @@ export interface IExtension { readonly ratingCount?: number; readonly outdated: boolean; readonly outdatedTargetPlatform: boolean; - readonly reloadRequiredStatus?: string; + readonly runtimeState: ExtensionRuntimeState | undefined; readonly enablementState: EnablementState; readonly tags: readonly string[]; readonly categories: readonly string[]; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 0db4f047c60..d0bb47b7529 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -63,7 +63,7 @@ import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; -import { IUpdateService } from 'vs/platform/update/common/update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -275,7 +275,7 @@ suite('ExtensionRecommendationsService Test', () => { }, }); - instantiationService.stub(IUpdateService, { onStateChange: Event.None }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index ea44eb82a58..3ed7d4a1b94 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -56,7 +56,7 @@ import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/envi import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IUpdateService } from 'vs/platform/update/common/update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -137,7 +137,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); - instantiationService.stub(IUpdateService, { onStateChange: Event.None }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } @@ -1010,7 +1010,7 @@ suite('ReloadAction', () => { didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction when extension is newly installed and reload is not required', async () => { @@ -1078,7 +1078,7 @@ suite('ReloadAction', () => { uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to complete the uninstallation of this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to complete the uninstallation of this extension.`); }); test('Test ReloadAction when extension is uninstalled and can be removed', async () => { @@ -1146,7 +1146,7 @@ suite('ReloadAction', () => { return new Promise(c => { disposables.add(testObject.onDidChange(() => { - if (testObject.enabled && testObject.tooltip === 'Please reload Visual Studio Code to enable the updated extension.') { + if (testObject.enabled && testObject.tooltip === `Please reload ${instantiationService.get(IProductService).nameLong} to enable the updated extension.`) { c(); } })); @@ -1200,7 +1200,7 @@ suite('ReloadAction', () => { await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to disable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to disable this extension.`, testObject.tooltip); }); test('Test ReloadAction when extension enablement is toggled when running', async () => { @@ -1243,7 +1243,7 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to enable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`, testObject.tooltip); }); test('Test ReloadAction when extension enablement is toggled when not running', async () => { @@ -1290,7 +1290,7 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to enable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`, testObject.tooltip); }); test('Test ReloadAction when a localization extension is newly installed', async () => { @@ -1441,7 +1441,7 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction when ui extension is disabled on remote server and installed in local server', async () => { @@ -1480,7 +1480,7 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction for remote ui extension is disabled when it is installed and enabled in local server', async () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index a349dbbf69c..d37a7df8775 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -48,7 +48,7 @@ import { arch } from 'vs/base/common/process'; import { IProductService } from 'vs/platform/product/common/productService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IUpdateService } from 'vs/platform/update/common/update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; suite('ExtensionsViews Tests', () => { @@ -188,7 +188,7 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledTheme], EnablementState.DisabledGlobally); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); - instantiationService.stub(IUpdateService, { onStateChange: Event.None }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 7d56ee6d115..701a823db7d 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -51,7 +51,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Mutable } from 'vs/base/common/types'; -import { IUpdateService } from 'vs/platform/update/common/update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -132,7 +132,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', []); instantiationService.stubPromise(INotificationService, 'prompt', 0); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); - instantiationService.stub(IUpdateService, { onStateChange: Event.None }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); }); test('test gallery extension', async () => { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts index 600b2380d39..8c86a741081 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionManagementChannelClient as BaseExtensionManagementChannelClient, ExtensionEventResult } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; @@ -89,8 +89,8 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE return super.uninstall(extension, options); } - override async getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI): Promise { - return super.getInstalled(type, await this.getProfileLocation(extensionsProfileResource)); + override async getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI, productVersion?: IProductVersion): Promise { + return super.getInstalled(type, await this.getProfileLocation(extensionsProfileResource), productVersion); } override async updateMetadata(local: ILocalExtension, metadata: Partial, extensionsProfileResource?: URI): Promise { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 45978737960..c60c940c4d5 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,7 +5,8 @@ import { 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 + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -80,8 +81,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.onDidChangeProfile = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; } - async getInstalled(type?: ExtensionType, profileLocation?: URI): Promise { - const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, profileLocation))); + async getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise { + const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, profileLocation, productVersion))); return flatten(result); } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index e58051fda3f..80ff4f879d8 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -170,8 +170,8 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe await this.webExtensionsScannerService.copyExtensions(fromProfileLocation, toProfileLocation, e => !e.metadata?.isApplicationScoped); } - protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise { - const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease); + protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise { + const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease, productVersion); if (compatibleExtension) { return compatibleExtension; } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index d5e8f029682..5e78b841010 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -87,7 +87,7 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag this.logService.info(`Downloading the '${extension.identifier.id}' extension locally and install`); const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion); installOptions = { ...installOptions, donotIncludePackAndDependencies: true }; - const installed = await this.getInstalled(ExtensionType.User); + const installed = await this.getInstalled(ExtensionType.User, undefined, installOptions.productVersion); const workspaceExtensions = await this.getAllWorkspaceDependenciesAndPackedExtensions(compatible, CancellationToken.None); if (workspaceExtensions.length) { this.logService.info(`Downloading the workspace dependencies and packed extensions of '${compatible.identifier.id}' locally and install`); From ea142b5ccdcb797b1de6b1a46fecbf25dea2e229 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Wed, 6 Mar 2024 04:15:43 +0100 Subject: [PATCH 219/228] fix: memory leak in notebook baseCellViewModel (#205499) * fix: memory leak in notebook baseCellViewModel * fix: dispose when detaching text editor * use mutabledisposable --- .../notebook/browser/viewModel/baseCellViewModel.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 548a5a7c043..dd136ac471b 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, IReference, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -19,11 +19,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { IWordWrapTransientState, readTransientState, writeTransientState } from 'vs/workbench/contrib/codeEditor/browser/toggleWordWrap'; import { CellEditState, CellFocusMode, CursorAtBoundary, CursorAtLineBoundary, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookCellStatusBarItem, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookOptionsChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; export abstract class BaseCellViewModel extends Disposable { @@ -103,6 +103,7 @@ export abstract class BaseCellViewModel extends Disposable { private _editorViewStates: editorCommon.ICodeEditorViewState | null = null; private _editorTransientState: IWordWrapTransientState | null = null; private _resolvedCellDecorations = new Map(); + private _textModelRefChangeDisposable = this._register(new MutableDisposable()); private readonly _cellDecorationsChanged = this._register(new Emitter<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }>()); onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }> = this._cellDecorationsChanged.event; @@ -299,6 +300,7 @@ export abstract class BaseCellViewModel extends Disposable { this._textModelRef.dispose(); this._textModelRef = undefined; } + this._textModelRefChangeDisposable.clear(); } getText(): string { @@ -618,8 +620,7 @@ export abstract class BaseCellViewModel extends Disposable { if (!this._textModelRef) { throw new Error(`Cannot resolve text model for ${this.uri}`); } - - this._register(this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent())); + this._textModelRefChangeDisposable.value = this.textModel!.onDidChangeContent(() => this.onDidChangeTextModelContent()); } return this.textModel!; From 982998a94cb37107c291e30458eb17c6fb759f96 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 6 Mar 2024 11:00:50 +0100 Subject: [PATCH 220/228] fix populating update properties for darwin (#206947) --- src/vs/platform/update/electron-main/updateService.darwin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index d79c9b6927a..bdc6e2d6766 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -25,7 +25,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version, timestamp) => ({ url, version, productVersion: version, timestamp })); } - @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, version, timestamp) => ({ version, productVersion: version, timestamp })); } + @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, timestamp) => ({ version, productVersion: version, timestamp })); } constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, From 9626fb0cd29113bc7bbaf7035e0b0cbe96baae59 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 6 Mar 2024 12:51:50 +0100 Subject: [PATCH 221/228] Inline chat widget tweaks (#206954) * make status menu configurable https://github.com/microsoft/vscode/issues/206940 * pull inline chat zone widget into its own file * :lipstick: * remove expand/crop feature * make chat message contents scrollable --- .../lib/stylelint/vscode-known-variables.json | 2 - .../browser/inlineChat.contribution.ts | 2 - .../contrib/inlineChat/browser/inlineChat.css | 24 -- .../inlineChat/browser/inlineChatActions.ts | 42 +-- .../browser/inlineChatController.ts | 16 +- .../inlineChat/browser/inlineChatSession.ts | 14 - .../browser/inlineChatStrategies.ts | 2 +- .../inlineChat/browser/inlineChatWidget.ts | 332 ++++-------------- .../browser/inlineChatZoneWidget.ts | 187 ++++++++++ .../contrib/inlineChat/common/inlineChat.ts | 2 - .../controller/chat/notebookChatController.ts | 4 +- 11 files changed, 267 insertions(+), 360 deletions(-) create mode 100644 src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 51d8c95ae17..a965cfcdb9f 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -790,8 +790,6 @@ "--vscode-hover-maxWidth", "--vscode-hover-sourceWhiteSpace", "--vscode-hover-whiteSpace", - "--vscode-inline-chat-cropped", - "--vscode-inline-chat-expanded", "--vscode-inline-chat-quick-voice-height", "--vscode-inline-chat-quick-voice-width", "--vscode-editor-dictation-widget-height", diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index ea70b146a8c..c6a996a178c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -51,8 +51,6 @@ registerAction2(InlineChatActions.FocusInlineChat); registerAction2(InlineChatActions.PreviousFromHistory); registerAction2(InlineChatActions.NextFromHistory); registerAction2(InlineChatActions.ViewInChatAction); -registerAction2(InlineChatActions.ExpandMessageAction); -registerAction2(InlineChatActions.ContractMessageAction); registerAction2(InlineChatActions.ToggleDiffForChange); registerAction2(InlineChatActions.FeebackHelpfulCommand); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 7d299bbc1e5..21c2b268b5e 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -175,37 +175,13 @@ cursor: pointer; } -/* .monaco-editor .inline-chat .markdownMessage .message * { - margin: unset; -} - -.monaco-editor .inline-chat .markdownMessage .message code { - font-family: var(--monaco-monospace-font); - font-size: 12px; - color: var(--vscode-textPreformat-foreground); - background-color: var(--vscode-textPreformat-background); - padding: 1px 3px; - border-radius: 4px; -} */ - .monaco-editor .inline-chat .chatMessage .chatMessageContent .value { - -webkit-line-clamp: initial; - -webkit-box-orient: vertical; overflow: hidden; - display: -webkit-box; -webkit-user-select: text; user-select: text; } -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="cropped"] .value { - -webkit-line-clamp: var(--vscode-inline-chat-cropped, 3); -} - -.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="expanded"] .value { - -webkit-line-clamp: var(--vscode-inline-chat-expanded, 10); -} - .monaco-editor .inline-chat .followUps { padding: 5px 5px; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 75ca16a1f24..7a6b95e0767 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_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } 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'; @@ -737,46 +737,6 @@ export class ViewInChatAction extends AbstractInlineChatAction { } } -export class ExpandMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.expandMessageAction', - title: localize('expandMessage', 'Show More'), - icon: Codicon.chevronDown, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('cropped')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(true); - } -} - -export class ContractMessageAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineChat.contractMessageAction', - title: localize('contractMessage', 'Show Less'), - icon: Codicon.chevronUp, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, - when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('expanded')), - group: '2_expandOrContract', - order: 1 - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.updateExpansionState(false); - } -} - export class InlineAccessibilityHelpContribution extends Disposable { constructor() { super(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index cfb18aa49f0..e2feff8c52f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -39,10 +39,11 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IInlineChatSavingService } from './inlineChatSavingService'; -import { EmptyResponse, ErrorResponse, ExpansionState, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { IInlineChatSessionService } from './inlineChatSessionService'; import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { IInlineChatMessageAppender } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { StashedSession } from './inlineChatSession'; @@ -379,7 +380,6 @@ export class InlineChatController implements IEditorContribution { this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []); this._updatePlaceholder(); this._zone.value.widget.updateInfo(this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect")); - this._zone.value.widget.preferredExpansionState = this._session.lastExpansionState; this._zone.value.widget.value = this._session.session.input ?? this._session.lastInput?.value ?? this._zone.value.widget.value; if (this._session.session.input) { this._zone.value.widget.selectAll(); @@ -795,8 +795,6 @@ export class InlineChatController implements IEditorContribution { const message = { message: response.mdContent, providerId: this._session.provider.debugName, requestId: response.requestId }; this._zone.value.widget.updateChatMessage(message); - //this._zone.value.widget.updateMarkdownMessage(response.mdContent); - this._session.lastExpansionState = this._zone.value.widget.expansionState; this._zone.value.widget.updateToolbar(true); newPosition = await this._strategy.renderChanges(response); @@ -1088,14 +1086,6 @@ export class InlineChatController implements IEditorContribution { } } - updateExpansionState(expand: boolean) { - if (this._session) { - const expansionState = expand ? ExpansionState.EXPANDED : ExpansionState.CROPPED; - this._zone.value.widget.updateChatMessageExpansionState(expansionState); - this._session.lastExpansionState = expansionState; - } - } - toggleDiff() { this._strategy?.toggleDiff?.(); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts index a3e3865631d..109875de35f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts @@ -67,11 +67,6 @@ export type TelemetryDataClassification = { responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; }; -export enum ExpansionState { - EXPANDED = 'expanded', - CROPPED = 'cropped', - NOT_CROPPED = 'not_cropped' -} export class SessionWholeRange { @@ -142,7 +137,6 @@ export class SessionWholeRange { export class Session { private _lastInput: SessionPrompt | undefined; - private _lastExpansionState: ExpansionState | undefined; private _isUnstashed: boolean = false; private readonly _exchange: SessionExchange[] = []; private readonly _startTime = new Date(); @@ -204,14 +198,6 @@ export class Session { this._isUnstashed = true; } - get lastExpansionState(): ExpansionState | undefined { - return this._lastExpansionState; - } - - set lastExpansionState(state: ExpansionState) { - this._lastExpansionState = state; - } - get textModelNSnapshotAltVersion(): number | undefined { return this._textModelNSnapshotAltVersion; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 30aa3f46b10..500d978c389 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -30,7 +30,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { InlineChatFileCreatePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget'; import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { HunkState } from './inlineChatSession'; import { assertType } from 'vs/base/common/types'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 6f1cd922882..67cb9f527c0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -13,7 +13,6 @@ import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ISettableObservable, constObservable, derived, observableValue } from 'vs/base/common/observable'; -import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./inlineChat'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; @@ -23,10 +22,10 @@ import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/co import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; -import { EditorLayoutInfo, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; @@ -37,7 +36,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; @@ -63,11 +61,12 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection'; -import { ExpansionState, HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { asRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_VISIBLE, IInlineChatFollowup, IInlineChatSlashCommand, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -137,9 +136,25 @@ export interface InlineChatWidgetViewState { } export interface IInlineChatWidgetConstructionOptions { - menuId: MenuId; + /** + * The telemetry source for all commands of this widget + */ + telemetrySource: string; + /** + * The menu that is inside the input editor, use for send, dictation + */ + inputMenuId: MenuId; + /** + * The menu that next to the input editor, use for close, config etc + */ widgetMenuId: MenuId; - statusMenuId: MenuId; + /** + * The menu that rendered as button bar, use for accept, discard etc + */ + statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions }; + /** + * The men that rendered in the lower right corner, use for feedback + */ feedbackMenuId: MenuId; } @@ -177,10 +192,7 @@ export class InlineChatWidget { h('div.previewDiff.hidden@previewDiff'), h('div.previewCreateTitle.show-file-icons@previewCreateTitle'), h('div.previewCreate.hidden@previewCreate'), - h('div.chatMessage.hidden@chatMessage', [ - h('div.chatMessageContent@chatMessageContent'), - h('div.messageActions@messageActions') - ]), + h('div.chatMessage.hidden@chatMessage'), h('div.followUps.hidden@followUps'), h('div.accessibleViewer@accessibleViewer'), h('div.status@status', [ @@ -192,13 +204,15 @@ export class InlineChatWidget { ] ); + private readonly _chatMessageContents: HTMLDivElement; + private readonly _chatMessageScrollable: DomScrollableElement; + private readonly _store = new DisposableStore(); private readonly _slashCommands = this._store.add(new DisposableStore()); private readonly _inputEditor: IActiveCodeEditor; private readonly _inputModel: ITextModel; private readonly _ctxInputEmpty: IContextKey; - private readonly _ctxMessageCropState: IContextKey<'cropped' | 'not_cropped' | 'expanded'>; private readonly _ctxInnerCursorFirst: IContextKey; private readonly _ctxInnerCursorLast: IContextKey; private readonly _ctxInnerCursorStart: IContextKey; @@ -229,8 +243,6 @@ export class InlineChatWidget { private _lastDim: Dimension | undefined; private _isLayouting: boolean = false; - private _preferredExpansionState: ExpansionState | undefined; - private _expansionState: ExpansionState = ExpansionState.NOT_CROPPED; private _slashCommandDetails: { command: string; detail: string }[] = []; private _slashCommandContentWidget: SlashCommandContentWidget; @@ -244,8 +256,8 @@ export class InlineChatWidget { private readonly _codeBlockModelCollection: CodeBlockModelCollection; constructor( - private readonly parentEditor: ICodeEditor, - _options: IInlineChatWidgetConstructionOptions, + private readonly _parentEditor: ICodeEditor, + options: IInlineChatWidgetConstructionOptions, @IModelService private readonly _modelService: IModelService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @@ -268,20 +280,13 @@ export class InlineChatWidget { ]) }; - this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this.parentEditor); + this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this._parentEditor); this._updateAriaLabel(); this._store.add(this._inputEditor); this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire())); this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire())); - this._store.add(addDisposableListener(this._elements.chatMessageContent, 'focus', () => this._ctxResponseFocused.set(true))); - this._store.add(addDisposableListener(this._elements.chatMessageContent, 'blur', () => this._ctxResponseFocused.reset())); - this._store.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._updateAriaLabel(); - } - })); const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${InlineChatWidget._modelPool++}.txt` }); this._inputModel = this._store.add(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri)); @@ -289,10 +294,24 @@ export class InlineChatWidget { this._editorOptions = this._store.add(_instantiationService.createInstance(ChatEditorOptions, undefined, editorForeground, inputBackground, editorBackground)); + this._chatMessageContents = document.createElement('div'); + this._chatMessageContents.className = 'chatMessageContent'; + this._chatMessageContents.tabIndex = 0; + this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._chatMessageScrollable = new DomScrollableElement(this._chatMessageContents, { alwaysConsumeMouseWheel: true }); + this._store.add(this._chatMessageScrollable); + this._elements.chatMessage.appendChild(this._chatMessageScrollable.getDomNode()); + this._store.add(addDisposableListener(this._chatMessageContents, 'focus', () => this._ctxResponseFocused.set(true))); + this._store.add(addDisposableListener(this._chatMessageContents, 'blur', () => this._ctxResponseFocused.reset())); + + this._store.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { + this._updateAriaLabel(); + } + })); // --- context keys - this._ctxMessageCropState = CTX_INLINE_CHAT_MESSAGE_CROP_STATE.bindTo(this._contextKeyService); this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(this._contextKeyService); this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(this._contextKeyService); @@ -381,8 +400,8 @@ export class InlineChatWidget { // toolbars - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, _options.menuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, options.inputMenuId, { + telemetrySource: options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu hoverDelegate @@ -392,25 +411,17 @@ export class InlineChatWidget { this._store.add(this._progressBar); - this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, _options.widgetMenuId, { - telemetrySource: 'interactiveEditorWidget-toolbar', + this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, options.widgetMenuId, { + telemetrySource: options.telemetrySource, toolbarOptions: { primaryGroup: 'main' }, hoverDelegate })); - const workbenchMenubarOptions: IWorkbenchButtonBarOptions = { - telemetrySource: 'interactiveEditorWidget-toolbar', - buttonConfigProvider: action => { - if (action.id === ACTION_REGENERATE_RESPONSE) { - return { showIcon: true, showLabel: false, isSecondary: true }; - } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { - return { isSecondary: false }; - } else { - return { isSecondary: true }; - } - } - }; - const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, _options.statusMenuId, workbenchMenubarOptions); + + const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; + const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; + + const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions); this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); @@ -423,7 +434,7 @@ export class InlineChatWidget { } }; - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, _options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); + const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); this._store.add(feedbackToolbar); @@ -432,24 +443,19 @@ export class InlineChatWidget { useInlineViewWhenSpaceIsLimited: false, ..._previewEditorEditorOptions, onlyShowAccessibleDiffViewer: this._accessibilityService.isScreenReaderOptimized(), - }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor))); + }, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, _parentEditor))); this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true })); - this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor))); + this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, _parentEditor))); - this._elements.chatMessageContent.tabIndex = 0; - this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.followUps.tabIndex = 0; this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.statusLabel.tabIndex = 0; - const markdownMessageToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.messageActions, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, workbenchToolbarOptions); - this._store.add(markdownMessageToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - this._store.add(markdownMessageToolbar); this._store.add(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); + this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); } })); @@ -475,7 +481,6 @@ export class InlineChatWidget { dispose(): void { this._store.dispose(); this._ctxInputEmpty.reset(); - this._ctxMessageCropState.reset(); } get domNode(): HTMLElement { @@ -488,6 +493,9 @@ export class InlineChatWidget { if (this._accessibleViewer.value) { this._accessibleViewer.value.width = _dim.width - 12; } + this._chatMessageContents.style.width = `${_dim.width - 10}px`; + this._chatMessageContents.style.maxHeight = `270px`; + const widgetToolbarWidth = getTotalWidth(this._elements.widgetToolbar); const editorToolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; const innerEditorWidth = _dim.width - editorToolbarWidth - widgetToolbarWidth; @@ -510,12 +518,6 @@ export class InlineChatWidget { this._elements.previewCreate.style.height = `${previewCreateDim.height}px`; } - - const lineHeight = this.parentEditor.getOption(EditorOption.lineHeight); - const editorHeight = this.parentEditor.getLayoutInfo().height; - const editorHeightInLines = Math.floor(editorHeight / lineHeight); - this._elements.root.style.setProperty('--vscode-inline-chat-cropped', String(Math.floor(editorHeightInLines / 5))); - this._elements.root.style.setProperty('--vscode-inline-chat-expanded', String(Math.floor(editorHeightInLines / 3))); this._onDidChangeLayout.fire(); } } finally { @@ -524,16 +526,17 @@ export class InlineChatWidget { } getHeight(): number { - const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status); const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */; + const progressHeight = getTotalHeight(this._elements.progress); const detectedIntentHeight = getTotalHeight(this._elements.detectedIntent); + const chatResponseHeight = getTotalHeight(this._chatMessageContents) + 16 /*padding*/; const followUpsHeight = getTotalHeight(this._elements.followUps); - const chatResponseHeight = getTotalHeight(this._elements.chatMessage); const previewDiffHeight = this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel() ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight())) : 0; const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle); const previewCreateHeight = this._previewCreateEditor.hasValue && this._previewCreateEditor.value.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight())) : 0; const accessibleViewHeight = this._accessibleViewer.value?.height ?? 0; - return base + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + 18 /* padding */ + 8 /*shadow*/; + const statusHeight = getTotalHeight(this._elements.status); + return progressHeight + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + statusHeight + 18 /* padding */ + 8 /*shadow*/; } updateProgress(show: boolean) { @@ -589,14 +592,6 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - get expansionState(): ExpansionState { - return this._expansionState; - } - - set preferredExpansionState(expansionState: ExpansionState | undefined) { - this._preferredExpansionState = expansionState; - } - get responseContent(): string | undefined { return this._chatMessage?.value; } @@ -604,42 +599,31 @@ export class InlineChatWidget { updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender; updateChatMessage(message: IInlineChatMessage | undefined): void; updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean): IInlineChatMessageAppender | undefined { - let expansionState: ExpansionState; + this._chatMessageDisposables.clear(); this._chatMessage = message ? new MarkdownString(message.message.value) : undefined; const hasMessage = message?.message.value; this._elements.chatMessage.classList.toggle('hidden', !hasMessage); - reset(this._elements.chatMessageContent); + reset(this._chatMessageContents); let resultingAppender: IInlineChatMessageAppender | undefined; - if (!hasMessage) { - this._ctxMessageCropState.reset(); - expansionState = ExpansionState.NOT_CROPPED; - } else { + if (hasMessage) { const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService)); const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } }; const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, this._codeBlockModelCollection, undefined)); - renderer.layout(this._elements.chatMessageContent.clientWidth - 4); // 2 for the padding used for the tab index border + renderer.layout(this._chatMessageContents.clientWidth - 4); // 2 for the padding used for the tab index border this._chatMessageDisposables.add(this._onDidChangeLayout.event(() => { - renderer.layout(this._elements.chatMessageContent.clientWidth - 4); + renderer.layout(this._chatMessageContents.clientWidth - 4); + this._chatMessageScrollable.scanDomNode(); })); - const template = renderer.renderTemplate(this._elements.chatMessageContent); + const template = renderer.renderTemplate(this._chatMessageContents); this._chatMessageDisposables.add(template.elementDisposables); this._chatMessageDisposables.add(template.templateDisposables); renderer.renderChatTreeItem(viewModel, 0, template); this._chatMessageDisposables.add(renderer.onDidChangeItemHeight(() => this._onDidChangeHeight.fire())); - if (this._preferredExpansionState) { - expansionState = this._preferredExpansionState; - this._preferredExpansionState = undefined; - } else { - this._updateLineClamp(ExpansionState.CROPPED); - expansionState = template.value.scrollHeight > template.value.clientHeight ? ExpansionState.CROPPED : ExpansionState.NOT_CROPPED; - } - this._ctxMessageCropState.set(expansionState); - this._updateLineClamp(expansionState); resultingAppender = isIncomplete ? { cancel: () => responseModel.cancel(), complete: () => responseModel.complete(), @@ -649,7 +633,6 @@ export class InlineChatWidget { } } : undefined; } - this._expansionState = expansionState; this._onDidChangeHeight.fire(); return resultingAppender; } @@ -667,21 +650,6 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } - updateChatMessageExpansionState(expansionState: ExpansionState) { - this._ctxMessageCropState.set(expansionState); - const heightBefore = this._elements.chatMessageContent.scrollHeight; - this._updateLineClamp(expansionState); - const heightAfter = this._elements.chatMessageContent.scrollHeight; - if (heightBefore === heightAfter) { - this._ctxMessageCropState.set(ExpansionState.NOT_CROPPED); - } - this._onDidChangeHeight.fire(); - } - - private _updateLineClamp(expansionState: ExpansionState) { - this._elements.chatMessageContent.setAttribute('state', expansionState); - } - updateSlashCommandUsed(command: string): void { const details = this._slashCommandDetails.find(candidate => candidate.command === command); if (!details) { @@ -948,7 +916,7 @@ export class InlineChatWidget { this._elements.accessibleViewer, session, hunkData, - new AccessibleHunk(this.parentEditor, session, hunkData) + new AccessibleHunk(this._parentEditor, session, hunkData) ); this._onDidChangeHeight.fire(); @@ -956,160 +924,6 @@ export class InlineChatWidget { } } -export class InlineChatZoneWidget extends ZoneWidget { - - readonly widget: InlineChatWidget; - - private readonly _ctxVisible: IContextKey; - private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; - private _dimension?: Dimension; - private _indentationWidth: number | undefined; - - constructor( - editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - ) { - super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); - - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); - - this._disposables.add(toDisposable(() => { - this._ctxVisible.reset(); - this._ctxCursorPosition.reset(); - })); - - this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, { - menuId: MENU_INLINE_CHAT_INPUT, - widgetMenuId: MENU_INLINE_CHAT_WIDGET, - statusMenuId: MENU_INLINE_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK - }); - this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); - this._disposables.add(this.widget); - this.create(); - - - this._disposables.add(addDisposableListener(this.domNode, 'click', e => { - if (!this.widget.hasFocus()) { - this.widget.focus(); - } - }, true)); - - // todo@jrieken listen ONLY when showing - const updateCursorIsAboveContextKey = () => { - if (!this.position || !this.editor.hasModel()) { - this._ctxCursorPosition.reset(); - } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('above'); - } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('below'); - } else { - this._ctxCursorPosition.reset(); - } - }; - this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); - this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); - updateCursorIsAboveContextKey(); - } - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this.widget.domNode); - } - - - protected override _doLayout(heightInPixel: number): void { - - const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; - const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth)); - this._dimension = new Dimension(width, heightInPixel); - this.widget.domNode.style.width = `${width}px`; - this.widget.layout(this._dimension); - } - - private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { - const info = this.editor.getLayoutInfo(); - return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); - } - - private _computeHeightInLines(): number { - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - return this.widget.getHeight() / lineHeight; - } - - protected override _relayout() { - if (this._dimension) { - this._doLayout(this._dimension.height); - } - super._relayout(this._computeHeightInLines()); - } - - override show(position: Position): void { - super.show(position, this._computeHeightInLines()); - this.widget.focus(); - this._ctxVisible.set(true); - } - - protected override _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimap.minimapWidth; - } - - updateBackgroundColor(newPosition: Position, wholeRange: IRange) { - assertType(this.container); - const widgetLineNumber = newPosition.lineNumber; - this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); - } - - private _calculateIndentationWidth(position: Position): number { - const viewModel = this.editor._getViewModel(); - if (!viewModel) { - return 0; - } - const visibleRange = viewModel.getCompletelyVisibleViewRange(); - const startLineVisibleRange = visibleRange.startLineNumber; - const positionLine = position.lineNumber; - let indentationLineNumber: number | undefined; - let indentationLevel: number | undefined; - for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) { - const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); - if (currentIndentationLevel !== 0) { - indentationLineNumber = lineNumber; - indentationLevel = currentIndentationLevel; - break; - } - } - return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); - } - - setContainerMargins(): void { - assertType(this.container); - - const info = this.editor.getLayoutInfo(); - const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; - this.container.style.marginLeft = `${marginWithoutIndentation}px`; - } - - setWidgetMargins(position: Position): void { - const indentationWidth = this._calculateIndentationWidth(position); - if (this._indentationWidth === indentationWidth) { - return; - } - this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; - this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; - this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; - } - - override hide(): void { - this.container!.classList.remove('inside-selection'); - this._ctxVisible.reset(); - this._ctxCursorPosition.reset(); - this.widget.reset(); - super.hide(); - aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); - } -} - class HunkAccessibleDiffViewer extends AccessibleDiffViewer { readonly height: number; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts new file mode 100644 index 00000000000..f9853b40261 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -0,0 +1,187 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Dimension, addDisposableListener } 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'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { IRange } from 'vs/editor/common/core/range'; +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_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { InlineChatWidget } from './inlineChatWidget'; + + +export class InlineChatZoneWidget extends ZoneWidget { + + readonly widget: InlineChatWidget; + + private readonly _ctxVisible: IContextKey; + private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; + private _dimension?: Dimension; + private _indentationWidth: number | undefined; + + constructor( + editor: ICodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); + + this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); + + this._disposables.add(toDisposable(() => { + this._ctxVisible.reset(); + this._ctxCursorPosition.reset(); + })); + + this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, { + telemetrySource: 'interactiveEditorWidget-toolbar', + inputMenuId: MENU_INLINE_CHAT_INPUT, + widgetMenuId: MENU_INLINE_CHAT_WIDGET, + feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK, + statusMenuId: { + menu: MENU_INLINE_CHAT_WIDGET_STATUS, + options: { + buttonConfigProvider: action => { + if (action.id === ACTION_REGENERATE_RESPONSE) { + return { showIcon: true, showLabel: false, isSecondary: true }; + } else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) { + return { isSecondary: false }; + } else { + return { isSecondary: true }; + } + } + } + } + }); + this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout())); + this._disposables.add(this.widget); + this.create(); + + + this._disposables.add(addDisposableListener(this.domNode, 'click', e => { + if (!this.widget.hasFocus()) { + this.widget.focus(); + } + }, true)); + + // todo@jrieken listen ONLY when showing + const updateCursorIsAboveContextKey = () => { + if (!this.position || !this.editor.hasModel()) { + this._ctxCursorPosition.reset(); + } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { + this._ctxCursorPosition.set('above'); + } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { + this._ctxCursorPosition.set('below'); + } else { + this._ctxCursorPosition.reset(); + } + }; + this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); + this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); + updateCursorIsAboveContextKey(); + } + + protected override _fillContainer(container: HTMLElement): void { + container.appendChild(this.widget.domNode); + } + + + protected override _doLayout(heightInPixel: number): void { + + const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER; + const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth)); + this._dimension = new Dimension(width, heightInPixel); + this.widget.domNode.style.width = `${width}px`; + this.widget.layout(this._dimension); + } + + private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number { + const info = this.editor.getLayoutInfo(); + return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0)); + } + + private _computeHeightInLines(): number { + const lineHeight = this.editor.getOption(EditorOption.lineHeight); + return this.widget.getHeight() / lineHeight; + } + + protected override _relayout() { + if (this._dimension) { + this._doLayout(this._dimension.height); + } + super._relayout(this._computeHeightInLines()); + } + + override show(position: Position): void { + super.show(position, this._computeHeightInLines()); + this.widget.focus(); + this._ctxVisible.set(true); + } + + protected override _getWidth(info: EditorLayoutInfo): number { + return info.width - info.minimap.minimapWidth; + } + + updateBackgroundColor(newPosition: Position, wholeRange: IRange) { + assertType(this.container); + const widgetLineNumber = newPosition.lineNumber; + this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber); + } + + private _calculateIndentationWidth(position: Position): number { + const viewModel = this.editor._getViewModel(); + if (!viewModel) { + return 0; + } + const visibleRange = viewModel.getCompletelyVisibleViewRange(); + const startLineVisibleRange = visibleRange.startLineNumber; + const positionLine = position.lineNumber; + let indentationLineNumber: number | undefined; + let indentationLevel: number | undefined; + for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) { + const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber); + if (currentIndentationLevel !== 0) { + indentationLineNumber = lineNumber; + indentationLevel = currentIndentationLevel; + break; + } + } + return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine)); + } + + setContainerMargins(): void { + assertType(this.container); + + const info = this.editor.getLayoutInfo(); + const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; + this.container.style.marginLeft = `${marginWithoutIndentation}px`; + } + + setWidgetMargins(position: Position): void { + const indentationWidth = this._calculateIndentationWidth(position); + if (this._indentationWidth === indentationWidth) { + return; + } + this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0; + this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`; + this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`; + } + + override hide(): void { + this.container!.classList.remove('inside-selection'); + this._ctxVisible.reset(); + this._ctxCursorPosition.reset(); + this.widget.reset(); + super.hide(); + aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 10e6097adea..d44c8e11d1e 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -156,7 +156,6 @@ export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('in export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); -export const CTX_INLINE_CHAT_MESSAGE_CROP_STATE = new RawContextKey<'cropped' | 'not_cropped' | 'expanded'>('inlineChatMarkdownMessageCropState', 'not_cropped', localize('inlineChatMarkdownMessageCropState', "Whether the interactive editor message is cropped, not cropped or expanded")); export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); export const CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey('inlineChatHasActiveRequest', false, localize('inlineChatHasActiveRequest', "Whether interactive editor has an active request")); export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); @@ -181,7 +180,6 @@ export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat'; export const MENU_INLINE_CHAT_INPUT = MenuId.for('inlineChatInput'); export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget'); -export const MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE = MenuId.for('inlineChatWidget.markdownMessage'); export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status'); export const MENU_INLINE_CHAT_WIDGET_FEEDBACK = MenuId.for('inlineChatWidget.feedback'); export const MENU_INLINE_CHAT_WIDGET_DISCARD = MenuId.for('inlineChatWidget.undo'); 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 d4b59189404..d97a8f09aa7 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -292,7 +292,8 @@ export class NotebookChatController extends Disposable implements INotebookEdito InlineChatWidget, fakeParentEditor, { - menuId: MENU_CELL_CHAT_INPUT, + telemetrySource: 'notebook-generate-cell', + inputMenuId: MENU_CELL_CHAT_INPUT, widgetMenuId: MENU_CELL_CHAT_WIDGET, statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS, feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK @@ -895,4 +896,3 @@ export class EditStrategy { registerNotebookContribution(NotebookChatController.id, NotebookChatController); - From 6bdee9d1e3f11e53fff148198054dc63a77b55d9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 6 Mar 2024 13:12:18 +0100 Subject: [PATCH 222/228] do not use downloading state (#206955) --- .../contrib/extensions/browser/extensionsWorkbenchService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index bab4b2cc8c1..903dcac8f25 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -862,7 +862,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!this.isAutoUpdateEnabled()) { return; } - if ((e.type === StateType.CheckingForUpdates && e.explicit) || e.type === StateType.AvailableForDownload || e.type === StateType.Downloading) { + if ((e.type === StateType.CheckingForUpdates && e.explicit) || e.type === StateType.AvailableForDownload || e.type === StateType.Downloaded) { this.eventuallyCheckForUpdates(true); } })); @@ -1520,7 +1520,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private getProductUpdateVersion(): IProductVersion | undefined { switch (this.updateService.state.type) { case StateType.AvailableForDownload: - case StateType.Downloading: case StateType.Downloaded: case StateType.Updating: case StateType.Ready: From b92a5dd3f04979ab0da420a8ff861be47ba76c93 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 6 Mar 2024 14:01:19 +0100 Subject: [PATCH 223/228] relayout when zone widget width changes (#206963) * inline - change the input editor to be a normal editor, not an embedded one https://github.com/microsoft/vscode/issues/206940 * relayout when zone widget width changes --- .../inlineChat/browser/inlineChatActions.ts | 23 +++++++++++++++++-- .../inlineChat/browser/inlineChatWidget.ts | 4 ++-- .../browser/inlineChatZoneWidget.ts | 6 +++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index 7a6b95e0767..596d14c9eac 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -5,7 +5,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; @@ -31,6 +31,7 @@ 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'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); @@ -129,10 +130,28 @@ export abstract class AbstractInlineChatAction extends EditorAction2 { } override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { + const editorService = accessor.get(IEditorService); + const logService = accessor.get(ILogService); + + let ctrl = InlineChatController.get(editor); + if (!ctrl) { + const { activeTextEditorControl } = editorService; + if (isCodeEditor(activeTextEditorControl)) { + editor = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + editor = activeTextEditorControl.getModifiedEditor(); + } + ctrl = InlineChatController.get(editor); + } + + if (!ctrl) { + logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri); + return; + } + if (editor instanceof EmbeddedCodeEditorWidget) { editor = editor.getParentEditor(); } - const ctrl = InlineChatController.get(editor); if (!ctrl) { for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 67cb9f527c0..2b9b26cf68f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -18,7 +18,7 @@ import 'vs/css!./inlineChat'; import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; -import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; @@ -280,7 +280,7 @@ export class InlineChatWidget { ]) }; - this._inputEditor = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this._parentEditor); + this._inputEditor = this._instantiationService.createInstance(CodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions); this._updateAriaLabel(); this._store.add(this._inputEditor); this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this))); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index f9853b40261..a6cdc9e5a29 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -114,6 +114,12 @@ export class InlineChatZoneWidget extends ZoneWidget { return this.widget.getHeight() / lineHeight; } + protected override _onWidth(_widthInPixel: number): void { + if (this._dimension) { + this._doLayout(this._dimension.height); + } + } + protected override _relayout() { if (this._dimension) { this._doLayout(this._dimension.height); From 639b71bdceaeda0b1b95ec2e0aa826cbba5f5e91 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 6 Mar 2024 14:28:46 +0100 Subject: [PATCH 224/228] fix downloading state in darwin (#206960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix downloading state in darwin * do not show user forced update notification in specific cases --------- Co-authored-by: João Moreno --- src/vs/platform/update/common/update.ts | 4 ++-- .../update/electron-main/updateService.darwin.ts | 6 +++--- .../update/electron-main/updateService.win32.ts | 15 ++++++--------- src/vs/workbench/contrib/update/browser/update.ts | 7 ++++++- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 02aeac681b5..87c40596916 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -64,7 +64,7 @@ export type Disabled = { type: StateType.Disabled; reason: DisablementReason }; export type Idle = { type: StateType.Idle; updateType: UpdateType; error?: string }; export type CheckingForUpdates = { type: StateType.CheckingForUpdates; explicit: boolean }; export type AvailableForDownload = { type: StateType.AvailableForDownload; update: IUpdate }; -export type Downloading = { type: StateType.Downloading; update: IUpdate }; +export type Downloading = { type: StateType.Downloading }; export type Downloaded = { type: StateType.Downloaded; update: IUpdate }; export type Updating = { type: StateType.Updating; update: IUpdate }; export type Ready = { type: StateType.Ready; update: IUpdate }; @@ -77,7 +77,7 @@ export const State = { Idle: (updateType: UpdateType, error?: string) => ({ type: StateType.Idle, updateType, error }) as Idle, CheckingForUpdates: (explicit: boolean) => ({ type: StateType.CheckingForUpdates, explicit } as CheckingForUpdates), AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), - Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), + Downloading: { type: StateType.Downloading } as Downloading, Downloaded: (update: IUpdate) => ({ type: StateType.Downloaded, update } as Downloaded), Updating: (update: IUpdate) => ({ type: StateType.Updating, update } as Updating), Ready: (update: IUpdate) => ({ type: StateType.Ready, update } as Ready), diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index bdc6e2d6766..27a2fcbeadf 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -24,7 +24,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } - @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version, timestamp) => ({ url, version, productVersion: version, timestamp })); } + @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available'); } @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, timestamp) => ({ version, productVersion: version, timestamp })); } constructor( @@ -96,12 +96,12 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau electron.autoUpdater.checkForUpdates(); } - private onUpdateAvailable(update: IUpdate): void { + private onUpdateAvailable(): void { if (this.state.type !== StateType.CheckingForUpdates) { return; } - this.setState(State.Downloading(update)); + this.setState(State.Downloading); } private onUpdateDownloaded(update: IUpdate): void { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index ff8bbb0e559..4c49a758185 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -135,7 +135,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return Promise.resolve(null); } - this.setState(State.Downloading(update)); + this.setState(State.Downloading); return this.cleanup(update.version).then(() => { return this.getUpdatePackagePath(update.version).then(updatePackagePath => { @@ -153,15 +153,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun .then(() => updatePackagePath); }); }).then(packagePath => { - const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); - this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); + const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); - } else { - this.setState(State.Downloaded(update)); } } else { this.setState(State.Ready(update)); @@ -209,7 +207,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun } protected override async doApplyUpdate(): Promise { - if (this.state.type !== StateType.Downloaded && this.state.type !== StateType.Downloading) { + if (this.state.type !== StateType.Downloaded) { return Promise.resolve(undefined); } @@ -273,14 +271,13 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates'); const update: IUpdate = { version: 'unknown', productVersion: 'unknown' }; - this.setState(State.Downloading(update)); + this.setState(State.Downloading); this.availableUpdate = { packagePath }; + this.setState(State.Downloaded(update)); if (fastUpdatesEnabled) { if (this.productService.target === 'user') { this.doApplyUpdate(); - } else { - this.setState(State.Downloaded(update)); } } else { this.setState(State.Ready(update)); diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 969c563d5de..45a8227909b 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -173,6 +173,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu @IContextKeyService private readonly contextKeyService: IContextKeyService, @IProductService private readonly productService: IProductService, @IOpenerService private readonly openerService: IOpenerService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IHostService private readonly hostService: IHostService ) { super(); @@ -316,8 +317,12 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu ); } - // windows fast updates (target === system) + // windows fast updates private onUpdateDownloaded(update: IUpdate): void { + if (this.configurationService.getValue('update.enableWindowsBackgroundUpdates') && this.productService.target === 'user') { + return; + } + if (!this.shouldShowNotification()) { return; } From b4e63addcbdea632775adf095773664b204f6d1d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 6 Mar 2024 14:47:45 +0100 Subject: [PATCH 225/228] Reduce memory leaks (fix #195889) (#206950) * leaks - fix group listener leak in open editors * leaks - stop patching `window.focus()` * fix more leaks * fix more leaks --- src/vs/base/browser/dom.ts | 29 --------- src/vs/workbench/browser/part.ts | 5 +- .../browser/parts/editor/editorStatus.ts | 2 +- src/vs/workbench/browser/window.ts | 61 +++++++++++-------- .../actions/voiceChatActions.ts | 2 +- .../emptyTextEditorHint.ts | 6 +- .../files/browser/views/openEditorsView.ts | 7 +-- .../contrib/webview/browser/webviewElement.ts | 9 +-- src/vs/workbench/test/browser/window.test.ts | 1 - 9 files changed, 49 insertions(+), 73 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 907f89d911e..ff113c9baa9 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,35 +921,6 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -/** - * Given an element, will attempt to pass focus() to the window it belongs - * to, depending on the options passed in: - * - force: always focus the element's window - * - otherwise: only focus the element's window if another window in the same - * workspace group has focus (when auxiliary windows are opened). - * - * @param element used to figure out the window the element belongs to - */ -export function focusWindow(element: Node, options?: { force: boolean }): void { - const window = getWindow(element); - - // Force: always focus the element window - if (options?.force) { - window.focus(); - } - - // Not forced: only focus the element window if another - // window in the same workspace group has focus (when auxiliary - // windows are opened). - // This prevents stealing focus from another workspace window. - else { - const activeWindow = getActiveWindow(); - if (activeWindow !== window && activeWindow.document.hasFocus()) { - window.focus(); - } - } -} - const globalStylesheets = new Map>(); export function isGlobalStylesheet(node: Node): boolean { diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index f015fdbdf53..48278fe16b9 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -193,7 +193,10 @@ export abstract class MultiWindowParts extends Compo registerPart(part: T): IDisposable { this._parts.add(part); - return this._register(toDisposable(() => this.unregisterPart(part))); + return this._register(toDisposable(() => { + this.unregisterPart(part); + part = undefined!; // helps to avoid a memory leak with closures where part is captured + })); } protected unregisterPart(part: T): void { diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index d34a58122cd..acf60d208b5 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -366,7 +366,7 @@ class EditorStatus extends Disposable { } private registerCommands(): void { - CommandsRegistry.registerCommand({ id: `changeEditorIndentation${this.targetWindowId}`, handler: () => this.showIndentationPicker() }); + this._register(CommandsRegistry.registerCommand({ id: `changeEditorIndentation${this.targetWindowId}`, handler: () => this.showIndentationPicker() })); } private async showIndentationPicker(): Promise { diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 203275338eb..121f3528fe4 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, EventHelper, EventType, focusWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; +import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { matchesScheme, Schemas } from 'vs/base/common/network'; -import { isIOS, isMacintosh, isNative } from 'vs/base/common/platform'; +import { isIOS, isMacintosh } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -45,11 +45,7 @@ export abstract class BaseWindow extends Disposable { ) { super(); - if (isNative) { - this.enableNativeWindowFocus(targetWindow); - } this.enableWindowFocusOnElementFocus(targetWindow); - this.enableMultiWindowAwareTimeout(targetWindow, dom); this.registerFullScreenListeners(targetWindow.vscodeWindowId); @@ -57,39 +53,50 @@ export abstract class BaseWindow extends Disposable { //#region focus handling in multi-window applications - protected enableNativeWindowFocus(targetWindow: CodeWindow): void { - const originalWindowFocus = targetWindow.focus.bind(targetWindow); + protected enableWindowFocusOnElementFocus(targetWindow: CodeWindow): void { + const originalFocus = targetWindow.HTMLElement.prototype.focus; const that = this; - targetWindow.focus = function () { - originalWindowFocus(); - - if ( - !that.environmentService.extensionTestsLocationURI && // never steal focus when running tests - !targetWindow.document.hasFocus() // skip when already having focus - ) { - // Enable `window.focus()` to work in Electron by - // asking the main process to focus the window. - // https://github.com/electron/electron/issues/25578 - that.hostService.focus(targetWindow); - } - }; - } - - protected enableWindowFocusOnElementFocus(targetWindow: CodeWindow): void { - const originalFocus = HTMLElement.prototype.focus; - targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { // Ensure the window the element belongs to is focused // in scenarios where auxiliary windows are present - focusWindow(this); + that.onElementFocus(getWindow(this)); // Pass to original focus() method originalFocus.apply(this, [options]); }; } + private onElementFocus(targetWindow: CodeWindow): void { + const activeWindow = getActiveWindow(); + if (activeWindow !== targetWindow && activeWindow.document.hasFocus()) { + + // Call original focus() + targetWindow.focus(); + + // In Electron, `window.focus()` fails to bring the window + // to the front if multiple windows exist in the same process + // group (floating windows). As such, we ask the host service + // to focus the window which can take care of bringin the + // window to the front. + // + // To minimise disruption by bringing windows to the front + // by accident, we only do this if the window is not already + // focused and the active window is not the target window + // but has focus. This is an indication that multiple windows + // are opened in the same process group while the target window + // is not focused. + + if ( + !this.environmentService.extensionTestsLocationURI && + !targetWindow.document.hasFocus() + ) { + this.hostService.focus(targetWindow); + } + } + } + //#endregion //#region timeout handling in multi-window applications 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 b0f09ffdf75..2a7077c746b 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -980,7 +980,7 @@ class KeywordActivationStatusEntry extends Disposable { ) { super(); - CommandsRegistry.registerCommand(KeywordActivationStatusEntry.STATUS_COMMAND, () => this.commandService.executeCommand('workbench.action.openSettings', KEYWORD_ACTIVIATION_SETTING_ID)); + this._register(CommandsRegistry.registerCommand(KeywordActivationStatusEntry.STATUS_COMMAND, () => this.commandService.executeCommand('workbench.action.openSettings', KEYWORD_ACTIVIATION_SETTING_ID))); this.registerListeners(); this.updateStatusEntry(); diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index 10e517dc4cb..ab46e943663 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -226,7 +226,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { id: 'inlineChat.hintAction', from: 'hint' }); - void this.commandService.executeCommand(inlineChatId, { from: 'hint' }); + this.commandService.executeCommand(inlineChatId, { from: 'hint' }); }; const hintHandler: IContentActionHandler = { @@ -254,7 +254,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { const hintPart = $('a', undefined, fragment); hintPart.style.fontStyle = 'italic'; hintPart.style.cursor = 'pointer'; - hintPart.onclick = handleClick; + this.toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); return hintPart; } else { const hintPart = $('span', undefined, fragment); @@ -272,7 +272,7 @@ class EmptyTextEditorHintContentWidget implements IContentWidget { if (this.options.clickable) { label.element.style.cursor = 'pointer'; - label.element.onclick = handleClick; + this.toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); } hintElement.appendChild(after); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index c6fd9a02c0d..ed7dcd5a06a 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -26,7 +26,7 @@ import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListDragAn import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { DisposableMap, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { MenuId, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ResourceContextKey, MultipleEditorGroupsContext } from 'vs/workbench/common/contextkeys'; @@ -118,7 +118,7 @@ export class OpenEditorsView extends ViewPane { this.listRefreshScheduler?.schedule(this.structuralRefreshDelay); }; - const groupDisposables = new Map(); + const groupDisposables = this._register(new DisposableMap()); const addGroupListener = (group: IEditorGroup) => { const groupModelChangeListener = group.onDidModelChange(e => { if (this.listRefreshScheduler?.isScheduled()) { @@ -156,7 +156,6 @@ export class OpenEditorsView extends ViewPane { } }); groupDisposables.set(group.id, groupModelChangeListener); - this._register(groupDisposables.get(group.id)!); }; this.editorGroupService.groups.forEach(g => addGroupListener(g)); @@ -167,7 +166,7 @@ export class OpenEditorsView extends ViewPane { this._register(this.editorGroupService.onDidMoveGroup(() => updateWholeList())); this._register(this.editorGroupService.onDidChangeActiveGroup(() => this.focusActiveEditor())); this._register(this.editorGroupService.onDidRemoveGroup(group => { - dispose(groupDisposables.get(group.id)); + groupDisposables.deleteAndDispose(group.id); updateWholeList(); })); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index f9baf25d202..3a258267b1f 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener, EventType, focusWindow, getActiveWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, getActiveWindow } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { promiseWithResolvers, ThrottledDelayer } from 'vs/base/common/async'; import { streamToBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -803,12 +803,9 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD return; } - // Ensure the window the element belongs to is focused - // in scenarios where auxiliary windows are present - focusWindow(this.element); - try { - this.element.contentWindow?.focus(); + this.element.parentElement?.focus(); // this helps to move floating windows to the front if any... + this.element.contentWindow?.focus(); // ...because `contentWindow` is not able to do so } catch { // noop } diff --git a/src/vs/workbench/test/browser/window.test.ts b/src/vs/workbench/test/browser/window.test.ts index 4a395ee47b3..f65d50c3aea 100644 --- a/src/vs/workbench/test/browser/window.test.ts +++ b/src/vs/workbench/test/browser/window.test.ts @@ -22,7 +22,6 @@ suite('Window', () => { super(window, dom, new TestHostService(), TestEnvironmentService); } - protected override enableNativeWindowFocus(): void { } protected override enableWindowFocusOnElementFocus(): void { } } From 1837fba9dc3ba3c77fb95093cfbd772b941a85e4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 6 Mar 2024 06:42:33 -0800 Subject: [PATCH 226/228] Add todo for inline chat context key --- .../contrib/terminalContrib/chat/browser/terminalChatActions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 709b4ac8f24..ff650600398 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -30,6 +30,7 @@ registerActiveXtermAction({ precondition: ContextKeyExpr.and( ContextKeyExpr.has(`config.${TerminalSettingId.ExperimentalInlineChat}`), ContextKeyExpr.and(TerminalContextKeys.processSupported, TerminalContextKeys.focusInAny), + // TODO: This needs to change to check for a terminal location capable agent CTX_INLINE_CHAT_HAS_PROVIDER ), run: (_xterm, _accessor, activeInstance) => { From cdc99038120157bdd0fc30c50ff1f3639ad8a75e Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 6 Mar 2024 15:57:01 +0100 Subject: [PATCH 227/228] update reload action name (#206971) --- .../workbench/contrib/extensions/browser/extensionsActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 8a4056cdf9b..2f2833049ba 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1598,7 +1598,7 @@ export class ReloadAction extends ExtensionAction { this.enabled = true; this.class = ReloadAction.EnabledClass; this.tooltip = runtimeState.reason; - this.label = runtimeState.action === ExtensionRuntimeActionType.Reload ? localize('reload required', 'Reload {0}', this.productService.nameShort) + this.label = runtimeState.action === ExtensionRuntimeActionType.Reload ? localize('reload window', 'Reload Window') : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart {0}', this.productService.nameShort) : runtimeState.action === ExtensionRuntimeActionType.ApplyUpdate || runtimeState.action === ExtensionRuntimeActionType.DownloadUpdate ? localize('update product', 'Update {0}', this.productService.nameShort) : ''; } From 5fd0172e1c150c7f6c6fcd4737ebbf2e5a139f97 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 6 Mar 2024 07:04:18 -0800 Subject: [PATCH 228/228] Fix compile issues --- .../terminalContrib/chat/browser/terminalChatController.ts | 2 +- .../terminalContrib/chat/browser/terminalChatWidget.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index c6f624dd865..e6a95220291 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -183,7 +183,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr } private _getPlaceholderText(): string { - return this._forcedPlaceholder ?? this._model?.inputPlaceholder ?? ''; + return this._forcedPlaceholder ?? ''; } setPlaceholder(text: string): void { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 3bf19460298..320199ad859 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -68,10 +68,11 @@ export class TerminalChatWidget extends Disposable { InlineChatWidget, fakeParentEditor, { - menuId: MENU_TERMINAL_CHAT_INPUT, + inputMenuId: MENU_TERMINAL_CHAT_INPUT, widgetMenuId: MENU_TERMINAL_CHAT_WIDGET, statusMenuId: MENU_TERMINAL_CHAT_WIDGET_STATUS, - feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK + feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, + telemetrySource: 'terminal-inline-chat', } ); this._reset();