diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index bb71e5555d6..36ed201bdc5 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -258,11 +258,11 @@ export class EmptySubmenuAction extends Action { } } -export function toAction(props: { id: string; label: string; enabled?: boolean; checked?: boolean; run: Function }): IAction { +export function toAction(props: { id: string; label: string; enabled?: boolean; checked?: boolean; class?: string; run: Function }): IAction { return { id: props.id, label: props.label, - class: undefined, + class: props.class, enabled: props.enabled ?? true, checked: props.checked ?? false, run: async (...args: unknown[]) => props.run(...args), diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 7ad31f533ec..7f8d25c0e09 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -2312,6 +2312,20 @@ class EditorDecorationsCollection implements editorCommon.IEditorDecorationsColl } return this._decorationIds; } + + public append(newDecorations: readonly IModelDeltaDecoration[]): string[] { + let newDecorationIds: string[] = []; + try { + this._isChangingDecorations = true; + this._editor.changeDecorations((accessor) => { + newDecorationIds = accessor.deltaDecorations([], newDecorations); + this._decorationIds = this._decorationIds.concat(newDecorationIds); + }); + } finally { + this._isChangingDecorations = false; + } + return newDecorationIds; + } } const squigglyStart = encodeURIComponent(` { isSecondary?: boolean; } | undefined; -export interface IMenuWorkbenchButtonBarOptions { +export interface IWorkbenchButtonBarOptions { telemetrySource?: string; buttonConfigProvider?: IButtonConfigProvider; } -export class MenuWorkbenchButtonBar extends ButtonBar { +export class WorkbenchButtonBar extends ButtonBar { - private readonly _store = new DisposableStore(); + protected readonly _store = new DisposableStore(); - private readonly _onDidChangeMenuItems = new Emitter(); - readonly onDidChangeMenuItems: Event = this._onDidChangeMenuItems.event; + private readonly _actionRunner: IActionRunner; + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + + constructor( + container: HTMLElement, + private readonly _options: IWorkbenchButtonBarOptions | undefined, + @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + super(container); + + this._actionRunner = this._store.add(new ActionRunner()); + if (_options?.telemetrySource) { + this._actionRunner.onDidRun(e => { + telemetryService.publicLog2( + 'workbenchActionExecuted', + { id: e.action.id, from: _options.telemetrySource! } + ); + }, undefined, this._store); + } + } + + override dispose() { + this._onDidChange.dispose(); + this._store.dispose(); + super.dispose(); + } + + update(actions: IAction[]): void { + + const conifgProvider: IButtonConfigProvider = this._options?.buttonConfigProvider ?? (() => ({ showLabel: true })); + + this.clear(); + + for (let i = 0; i < actions.length; i++) { + + const secondary = i > 0; + const actionOrSubmenu = actions[i]; + let action: IAction; + let btn: IButton; + + if (actionOrSubmenu instanceof SubmenuAction && actionOrSubmenu.actions.length > 0) { + const [first, ...rest] = actionOrSubmenu.actions; + action = first; + btn = this.addButtonWithDropdown({ + secondary: conifgProvider(action)?.isSecondary ?? secondary, + actionRunner: this._actionRunner, + actions: rest, + contextMenuProvider: this._contextMenuService, + }); + } else { + action = actionOrSubmenu; + btn = this.addButton({ + secondary: conifgProvider(action)?.isSecondary ?? secondary, + }); + } + + btn.enabled = action.enabled; + btn.element.classList.add('default-colors'); + if (conifgProvider(action)?.showLabel ?? true) { + btn.label = action.label; + } else { + btn.element.classList.add('monaco-text-button'); + } + if (conifgProvider(action)?.showIcon) { + if (action instanceof MenuItemAction && ThemeIcon.isThemeIcon(action.item.icon)) { + btn.icon = action.item.icon; + } else if (action.class) { + btn.element.classList.add(...action.class.split(' ')); + } + } + const kb = this._keybindingService.lookupKeybinding(action.id); + if (kb) { + btn.element.title = localize('labelWithKeybinding', "{0} ({1})", action.label, kb.getLabel()); + } else { + btn.element.title = action.label; + + } + btn.onDidClick(async () => { + this._actionRunner.run(action); + }); + } + this._onDidChange.fire(this); + } +} + +export class MenuWorkbenchButtonBar extends WorkbenchButtonBar { constructor( container: HTMLElement, menuId: MenuId, - options: IMenuWorkbenchButtonBarOptions | undefined, + options: IWorkbenchButtonBarOptions | undefined, @IMenuService menuService: IMenuService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(container); + super(container, options, contextMenuService, keybindingService, telemetryService); const menu = menuService.createMenu(menuId, contextKeyService); this._store.add(menu); - const actionRunner = this._store.add(new ActionRunner()); - if (options?.telemetrySource) { - actionRunner.onDidRun(e => { - telemetryService.publicLog2( - 'workbenchActionExecuted', - { id: e.action.id, from: options.telemetrySource! } - ); - }, undefined, this._store); - } - - const conifgProvider: IButtonConfigProvider = options?.buttonConfigProvider ?? (() => ({ showLabel: true })); - const update = () => { this.clear(); @@ -68,59 +144,18 @@ export class MenuWorkbenchButtonBar extends ButtonBar { .getActions({ renderShortTitle: true }) .flatMap(entry => entry[1]); - for (let i = 0; i < actions.length; i++) { + super.update(actions); - const secondary = i > 0; - const actionOrSubmenu = actions[i]; - let action: MenuItemAction | SubmenuItemAction; - let btn: IButton; - - if (actionOrSubmenu instanceof SubmenuItemAction && actionOrSubmenu.actions.length > 0) { - const [first, ...rest] = actionOrSubmenu.actions; - action = first; - btn = this.addButtonWithDropdown({ - secondary: conifgProvider(action)?.isSecondary ?? secondary, - actionRunner, - actions: rest, - contextMenuProvider: contextMenuService, - }); - } else { - action = actionOrSubmenu; - btn = this.addButton({ - secondary: conifgProvider(action)?.isSecondary ?? secondary, - }); - } - - btn.enabled = action.enabled; - btn.element.classList.add('default-colors'); - if (conifgProvider(action)?.showLabel ?? true) { - btn.label = action.label; - } else { - btn.element.classList.add('monaco-text-button'); - } - if (conifgProvider(action)?.showIcon && ThemeIcon.isThemeIcon(action.item.icon)) { - btn.icon = action.item.icon; - } - const kb = keybindingService.lookupKeybinding(action.id); - if (kb) { - btn.element.title = localize('labelWithKeybinding', "{0} ({1})", action.label, kb.getLabel()); - } else { - btn.element.title = action.label; - - } - btn.onDidClick(async () => { - actionRunner.run(action); - }); - } - this._onDidChangeMenuItems.fire(this); }; this._store.add(menu.onDidChange(update)); update(); } override dispose() { - this._onDidChangeMenuItems.dispose(); - this._store.dispose(); super.dispose(); } + + override update(_actions: IAction[]): void { + throw new Error('Use Menu or WorkbenchButtonBar'); + } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index d48e61ff247..11d69c03781 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -310,8 +310,35 @@ display: none; } +.monaco-editor .inline-chat-toolbar { + display: flex; +} + +.monaco-editor .inline-chat-toolbar > .monaco-button{ + margin-right: 6px; +} + +.monaco-editor .inline-chat-toolbar .action-label.checked { + color: var(--vscode-inputOption-activeForeground); + background-color: var(--vscode-inputOption-activeBackground); + outline: 1px solid var(--vscode-inputOption-activeBorder); +} + /* decoration styles */ +.monaco-editor .inline-chat-inserted-range { + background-color: var(--vscode-diffEditor-insertedTextBackground); +} + +.monaco-editor .inline-chat-inserted-range-linehighlight { + background-color: var(--vscode-diffEditor-insertedLineBackground); +} + +.monaco-editor .inline-chat-original-zone2 { + background-color: var(--vscode-diffEditor-removedLineBackground); + opacity: 0.8; +} + .monaco-editor .inline-chat-lines-deleted-range-inline { text-decoration: line-through; background-color: var(--vscode-diffEditor-removedTextBackground); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index f981c60c757..8a32d12f772 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -314,7 +314,7 @@ MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, { icon: Codicon.discard, group: '0_main', order: 2, - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages)), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Live3), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages)), rememberDefaultAction: true }); @@ -513,7 +513,7 @@ export class ApplyPreviewEdits extends AbstractInlineChatAction { when: CTX_INLINE_CHAT_USER_DID_EDIT }], menu: { - when: CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages), + when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Live3)), id: MENU_INLINE_CHAT_WIDGET_STATUS, group: '0_main', order: 0 diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index f9d0c371bcd..ae89acdaba4 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -3,18 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import * as aria from 'vs/base/browser/ui/aria/aria'; +import { coalesceInPlace } from 'vs/base/common/arrays'; import { Barrier, Queue, raceCancellation, raceCancellationError } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { MovingAverage } from 'vs/base/common/numbers'; import { StopWatch } from 'vs/base/common/stopwatch'; import { assertType } from 'vs/base/common/types'; +import { generateUuid } from 'vs/base/common/uuid'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; +import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { TextEdit } from 'vs/editor/common/languages'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { localize } from 'vs/nls'; @@ -23,29 +34,18 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ReplyResponse, EmptyResponse, ErrorResponse, ExpansionState, IInlineChatSessionService, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { EditModeStrategy, LivePreviewStrategy, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; -import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; -import { CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, EditMode, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, InlineChatResponseType, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, InlineChateResponseTypes, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_USER_DID_EDIT, IInlineChatProgressItem, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; -import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { Lazy } from 'vs/base/common/lazy'; +import { ILogService } from 'vs/platform/log/common/log'; import { Progress } from 'vs/platform/progress/common/progress'; -import { generateUuid } from 'vs/base/common/uuid'; -import { TextEdit } from 'vs/editor/common/languages'; -import { ISelection, Selection } from 'vs/editor/common/core/selection'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { MarkdownString } from 'vs/base/common/htmlContent'; -import { MovingAverage } from 'vs/base/common/numbers'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { EmptyResponse, ErrorResponse, ExpansionState, IInlineChatSessionService, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { EditModeStrategy, LivePreviewStrategy, LiveStrategy, LiveStrategy3, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies'; +import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatResponseFeedbackKind, InlineChatResponseType, InlineChateResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; export const enum State { CREATE_SESSION = 'CREATE_SESSION', @@ -287,7 +287,7 @@ export class InlineChatController implements IEditorContribution { this._zone.value.setContainerMargins(); } - if (this._activeSession && (this._activeSession.hasChangedText || this._activeSession.lastExchange)) { + if (this._activeSession && !position && (this._activeSession.hasChangedText || this._activeSession.lastExchange)) { widgetPosition = this._activeSession.wholeRange.value.getStartPosition().delta(-1); } if (this._activeSession) { @@ -368,14 +368,17 @@ export class InlineChatController implements IEditorContribution { switch (session.editMode) { case EditMode.Live: - this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._zone.value.widget); + this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._zone.value); + break; + case EditMode.Live3: + this._strategy = this._instaService.createInstance(LiveStrategy3, session, this._editor, this._zone.value); break; case EditMode.Preview: - this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._zone.value.widget); + this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._zone.value); break; case EditMode.LivePreview: default: - this._strategy = this._instaService.createInstance(LivePreviewStrategy, session, this._editor, this._zone.value.widget); + this._strategy = this._instaService.createInstance(LivePreviewStrategy, session, this._editor, this._zone.value); break; } @@ -385,6 +388,7 @@ export class InlineChatController implements IEditorContribution { private async [State.INIT_UI](options: InlineChatRunOptions): Promise { assertType(this._activeSession); + assertType(this._strategy); // hide/cancel inline completions when invoking IE InlineCompletionsController.get(this._editor)?.hide(); @@ -394,15 +398,10 @@ export class InlineChatController implements IEditorContribution { const wholeRangeDecoration = this._editor.createDecorationsCollection(); const updateWholeRangeDecoration = () => { - const range = this._activeSession!.wholeRange.value; - const decorations: IModelDeltaDecoration[] = []; - if (!range.isEmpty()) { - decorations.push({ - range, - options: InlineChatController._decoBlock - }); - } - wholeRangeDecoration.set(decorations); + const ranges = [this._activeSession!.wholeRange.value];//this._activeSession!.wholeRange.values; + const newDecorations = ranges.map(range => range.isEmpty() ? undefined : ({ range, options: InlineChatController._decoBlock })); + coalesceInPlace(newDecorations); + wholeRangeDecoration.set(newDecorations); }; this._sessionStore.add(toDisposable(() => wholeRangeDecoration.clear())); this._sessionStore.add(this._activeSession.wholeRange.onDidChange(updateWholeRangeDecoration)); @@ -514,13 +513,16 @@ export class InlineChatController implements IEditorContribution { } else { const barrier = new Barrier(); - const msgListener = Event.once(this._messages.event)(m => { + const store = new DisposableStore(); + store.add(this._strategy.onDidAccept(() => this.acceptSession())); + store.add(this._strategy.onDidDiscard(() => this.cancelSession())); + store.add(Event.once(this._messages.event)(m => { this._log('state=_waitForInput) message received', m); message = m; barrier.open(); - }); + })); await barrier.wait(); - msgListener.dispose(); + store.dispose(); } this._zone.value.widget.selectAll(false); @@ -807,7 +809,7 @@ export class InlineChatController implements IEditorContribution { } } - private async[State.SHOW_RESPONSE](): Promise { + private async[State.SHOW_RESPONSE](): Promise { assertType(this._activeSession); assertType(this._strategy); @@ -832,6 +834,8 @@ export class InlineChatController implements IEditorContribution { this._ctxResponseTypes.set(responseTypes); this._ctxDidEdit.set(this._activeSession.hasChangedText); + let newPosition: Position | undefined; + if (response instanceof EmptyResponse) { // show status message const status = localize('empty', "No results, please refine your input and try again"); @@ -854,7 +858,7 @@ export class InlineChatController implements IEditorContribution { this._activeSession.lastExpansionState = this._zone.value.widget.expansionState; this._zone.value.widget.updateToolbar(true); - await this._strategy.renderChanges(response); + newPosition = await this._strategy.renderChanges(response); if (this._activeSession.provider.provideFollowups) { const followupCts = new CancellationTokenSource(); @@ -877,7 +881,7 @@ export class InlineChatController implements IEditorContribution { }); } } - this._showWidget(false); + this._showWidget(false, newPosition); return State.WAIT_FOR_INPUT; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts index c9b2ce8f926..f4badff7d4d 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget.ts @@ -8,7 +8,7 @@ import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { assertType } from 'vs/base/common/types'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { IModelDecorationOptions, ITextModel } from 'vs/editor/common/model'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; @@ -60,6 +60,7 @@ export class InlineChatLivePreviewWidget extends ZoneWidget { constructor( editor: ICodeEditor, private readonly _session: Session, + options: IDiffEditorOptions, onDidChangeDiff: (() => void) | undefined, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -95,6 +96,7 @@ export class InlineChatLivePreviewWidget extends ZoneWidget { useInlineViewWhenSpaceIsLimited: false, overflowWidgetsDomNode: editor.getOverflowWidgetsDomNode(), onlyShowAccessibleDiffViewer: this.accessibilityService.isScreenReaderOptimized(), + ...options }, { originalEditor: { contributions: diffContributions }, modifiedEditor: { contributions: diffContributions } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 9e77b97ff69..e2a0698e757 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -5,23 +5,31 @@ import { disposableWindowInterval } from 'vs/base/browser/dom'; import { $window } from 'vs/base/browser/window'; +import { IAction, toAction } from 'vs/base/common/actions'; import { equals, tail } from 'vs/base/common/arrays'; import { AsyncIterableObject, AsyncIterableSource } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; +import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; import { Lazy } from 'vs/base/common/lazy'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { ICodeEditor, IViewZone } from 'vs/editor/browser/editorBrowser'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll'; +import { LineSource, RenderOptions, renderLines } from 'vs/editor/browser/widget/diffEditor/renderLines'; import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { LineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; +import { IDocumentDiff } from 'vs/editor/common/diff/documentDiffProvider'; +import { DetailedLineRangeMapping, LineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { TextEdit } from 'vs/editor/common/languages'; import { ICursorStateComputer, IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from 'vs/editor/common/model'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; +import { InlineDecoration, InlineDecorationType } from 'vs/editor/common/viewModel'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -31,12 +39,23 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { countWords, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; import { InlineChatFileCreatePreviewWidget, InlineChatLivePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatLivePreviewWidget'; import { ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; -import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; +import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget'; import { CTX_INLINE_CHAT_DOCUMENT_CHANGED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; export abstract class EditModeStrategy { - abstract dispose(): void; + protected readonly _onDidAccept = new Emitter(); + protected readonly _onDidDiscard = new Emitter(); + + readonly onDidAccept: Event = this._onDidAccept.event; + readonly onDidDiscard: Event = this._onDidDiscard.event; + + constructor(protected readonly _zone: InlineChatZoneWidget) { } + + dispose(): void { + this._onDidAccept.dispose(); + this._onDidDiscard.dispose(); + } abstract apply(): Promise; @@ -48,7 +67,7 @@ export abstract class EditModeStrategy { abstract undoChanges(altVersionId: number): Promise; - abstract renderChanges(response: ReplyResponse): Promise; + abstract renderChanges(response: ReplyResponse): Promise; abstract hasFocus(): boolean; @@ -62,10 +81,10 @@ export class PreviewStrategy extends EditModeStrategy { constructor( private readonly _session: Session, - private readonly _widget: InlineChatWidget, + zone: InlineChatZoneWidget, @IContextKeyService contextKeyService: IContextKeyService, ) { - super(); + super(zone); this._ctxDocumentChanged = CTX_INLINE_CHAT_DOCUMENT_CHANGED.bindTo(contextKeyService); this._listener = Event.debounce(_session.textModelN.onDidChangeContent.bind(_session.textModelN), () => { }, 350)(_ => { @@ -78,6 +97,7 @@ export class PreviewStrategy extends EditModeStrategy { override dispose(): void { this._listener.dispose(); this._ctxDocumentChanged.reset(); + super.dispose(); } async apply() { @@ -118,23 +138,23 @@ export class PreviewStrategy extends EditModeStrategy { // nothing to do } - override async renderChanges(response: ReplyResponse): Promise { + override async renderChanges(response: ReplyResponse): Promise { if (response.allLocalEdits.length > 0) { const allEditOperation = response.allLocalEdits.map(edits => edits.map(TextEdit.asEditOperation)); - await this._widget.showEditsPreview(this._session.textModel0, this._session.textModelN, allEditOperation); + await this._zone.widget.showEditsPreview(this._session.textModel0, this._session.textModelN, allEditOperation); } else { - this._widget.hideEditsPreview(); + this._zone.widget.hideEditsPreview(); } if (response.untitledTextModel) { - this._widget.showCreatePreview(response.untitledTextModel); + this._zone.widget.showCreatePreview(response.untitledTextModel); } else { - this._widget.hideCreatePreview(); + this._zone.widget.hideCreatePreview(); } } hasFocus(): boolean { - return this._widget.hasFocus(); + return this._zone.widget.hasFocus(); } needsMargin(): boolean { @@ -142,6 +162,7 @@ export class PreviewStrategy extends EditModeStrategy { } } + class InlineDiffDecorations { private readonly _collection: IEditorDecorationsCollection; @@ -226,14 +247,14 @@ export class LiveStrategy extends EditModeStrategy { constructor( protected readonly _session: Session, protected readonly _editor: ICodeEditor, - protected readonly _widget: InlineChatWidget, + zone: InlineChatZoneWidget, @IConfigurationService configService: IConfigurationService, @IStorageService protected _storageService: IStorageService, @IBulkEditService protected readonly _bulkEditService: IBulkEditService, @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, @IInstantiationService protected readonly _instaService: IInstantiationService, ) { - super(); + super(zone); this._diffEnabled = configService.getValue('inlineChat.showDiff'); this._inlineDiffDecorations = new InlineDiffDecorations(this._editor, this._diffEnabled); @@ -248,6 +269,7 @@ export class LiveStrategy extends EditModeStrategy { } override dispose(): void { + super.dispose(); this._inlineDiffDecorations.clear(); this._store.dispose(); } @@ -316,15 +338,15 @@ export class LiveStrategy extends EditModeStrategy { } } - override async renderChanges(response: ReplyResponse) { + override async renderChanges(response: ReplyResponse): Promise { const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced'); this._updateSummaryMessage(diff?.changes ?? []); this._inlineDiffDecorations.update(); if (response.untitledTextModel) { - this._widget.showCreatePreview(response.untitledTextModel); + this._zone.widget.showCreatePreview(response.untitledTextModel); } else { - this._widget.hideCreatePreview(); + this._zone.widget.hideCreatePreview(); } } @@ -347,7 +369,7 @@ export class LiveStrategy extends EditModeStrategy { } else { message = localize('lines.N', "Changed {0} lines", linesChanged); } - this._widget.updateStatus(message); + this._zone.widget.updateStatus(message); } override needsMargin(): boolean { @@ -355,7 +377,7 @@ export class LiveStrategy extends EditModeStrategy { } hasFocus(): boolean { - return this._widget.hasFocus(); + return this._zone.widget.hasFocus(); } } @@ -368,14 +390,14 @@ export class LivePreviewStrategy extends LiveStrategy { constructor( session: Session, editor: ICodeEditor, - widget: InlineChatWidget, + zone: InlineChatZoneWidget, @IConfigurationService configService: IConfigurationService, @IStorageService storageService: IStorageService, @IBulkEditService bulkEditService: IBulkEditService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IInstantiationService instaService: IInstantiationService, ) { - super(session, editor, widget, configService, storageService, bulkEditService, editorWorkerService, instaService); + super(session, editor, zone, configService, storageService, bulkEditService, editorWorkerService, instaService); this._previewZone = new Lazy(() => instaService.createInstance(InlineChatFileCreatePreviewWidget, editor)); } @@ -452,7 +474,7 @@ export class LivePreviewStrategy extends LiveStrategy { // create enough zones while (groups.length > this._diffZonePool.length) { - this._diffZonePool.push(this._instaService.createInstance(InlineChatLivePreviewWidget, this._editor, this._session, this._diffZonePool.length === 0 ? handleDiff : undefined)); + this._diffZonePool.push(this._instaService.createInstance(InlineChatLivePreviewWidget, this._editor, this._session, {}, this._diffZonePool.length === 0 ? handleDiff : undefined)); } for (let i = 0; i < groups.length; i++) { this._diffZonePool[i].showForChanges(groups[i]); @@ -483,7 +505,7 @@ export class LivePreviewStrategy extends LiveStrategy { await this._updateDiffZones(); } - override async renderChanges(response: ReplyResponse) { + override async renderChanges(response: ReplyResponse): Promise { await this._updateDiffZones(); @@ -575,3 +597,370 @@ export function asProgressiveEdit(edit: IIdentifiedSingleEditOperation, wordsPer newText: stream.asyncIterable }; } + + +// --- + +export class LiveStrategy3 extends EditModeStrategy { + + private readonly _store: DisposableStore = new DisposableStore(); + private readonly _sessionStore: DisposableStore = new DisposableStore(); + + private readonly _modifiedRangesDecorations: IEditorDecorationsCollection; + private readonly _modifiedRangesThatHaveBeenInteractedWith: string[] = []; + + private _editCount: number = 0; + + constructor( + protected readonly _session: Session, + protected readonly _editor: ICodeEditor, + zone: InlineChatZoneWidget, + @IStorageService protected _storageService: IStorageService, + @IBulkEditService protected readonly _bulkEditService: IBulkEditService, + @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, + @IInstantiationService protected readonly _instaService: IInstantiationService, + ) { + super(zone); + + this._modifiedRangesDecorations = this._editor.createDecorationsCollection(); + } + + override dispose(): void { + this._modifiedRangesDecorations.clear(); + this._sessionStore.dispose(); + this._store.dispose(); + super.dispose(); + } + + + async apply() { + this._sessionStore.clear(); + this._modifiedRangesDecorations.clear(); + this._modifiedRangesThatHaveBeenInteractedWith.length = 0; + if (this._editCount > 0) { + this._editor.pushUndoStop(); + } + if (!(this._session.lastExchange?.response instanceof ReplyResponse)) { + return; + } + const { untitledTextModel } = this._session.lastExchange.response; + if (untitledTextModel && !untitledTextModel.isDisposed() && untitledTextModel.isDirty()) { + await untitledTextModel.save({ reason: SaveReason.EXPLICIT }); + } + } + + async cancel() { + this._sessionStore.clear(); + this._modifiedRangesDecorations.clear(); + this._modifiedRangesThatHaveBeenInteractedWith.length = 0; + const { textModelN: modelN, textModelNAltVersion, textModelNSnapshotAltVersion } = this._session; + if (modelN.isDisposed()) { + return; + } + const targetAltVersion = textModelNSnapshotAltVersion ?? textModelNAltVersion; + LiveStrategy3._undoModelUntil(modelN, targetAltVersion); + } + + override async makeChanges(edits: ISingleEditOperation[]): Promise { + const cursorStateComputerAndInlineDiffCollection: ICursorStateComputer = (undoEdits) => { + let last: Position | null = null; + for (const edit of undoEdits) { + last = !last || last.isBefore(edit.range.getEndPosition()) ? edit.range.getEndPosition() : last; + } + return last && [Selection.fromPositions(last)]; + }; + + // push undo stop before first edit + if (++this._editCount === 1) { + this._editor.pushUndoStop(); + } + this._editor.executeEdits('inline-chat-live', edits, cursorStateComputerAndInlineDiffCollection); + } + + override async undoChanges(altVersionId: number): Promise { + this._sessionStore.clear(); + this._modifiedRangesDecorations.clear(); + this._modifiedRangesThatHaveBeenInteractedWith.length = 0; + + const { textModelN } = this._session; + LiveStrategy3._undoModelUntil(textModelN, altVersionId); + } + + override async makeProgressiveChanges(edits: ISingleEditOperation[], opts: ProgressingEditsOptions): Promise { + + // push undo stop before first edit + if (++this._editCount === 1) { + this._editor.pushUndoStop(); + } + + const listener = this._session.textModelN.onDidChangeContent(async () => { + await this._showDiff(false, false); + }); + + try { + const durationInSec = opts.duration / 1000; + for (const edit of edits) { + const wordCount = countWords(edit.text ?? ''); + const speed = wordCount / durationInSec; + // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); + await performAsyncTextEdit(this._session.textModelN, asProgressiveEdit(edit, speed, opts.token)); + } + } finally { + listener.dispose(); + } + } + + private async _computeDiff(): Promise { + const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: 5000, computeMoves: false }, 'advanced'); + + if (!diff || diff.changes.length === 0) { + return { identical: false, quitEarly: false, changes: [], moves: [] }; + } + + // merge changes neighboring changes + const mergedChanges = [diff.changes[0]]; + for (let i = 1; i < diff.changes.length; i++) { + const lastChange = mergedChanges[mergedChanges.length - 1]; + const thisChange = diff.changes[i]; + if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= 5) { + mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping( + lastChange.original.join(thisChange.original), + lastChange.modified.join(thisChange.modified), + (lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? []) + ); + } else { + mergedChanges.push(thisChange); + } + } + + return { + identical: diff.identical, + quitEarly: diff.quitEarly, + changes: mergedChanges, + moves: [] + }; + } + + + private readonly _decoModifiedInteractedWith = ModelDecorationOptions.register({ description: 'inline-chat-modified-interacted-with', stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }); + + private async _showDiff(isFinalChanges: boolean, isAfterManualInteraction: boolean): Promise { + + const diff = await this._computeDiff(); + + this._sessionStore.clear(); + + this._updateSummaryMessage(diff.changes); + + if (diff.identical || diff.changes.length === 0) { + + if (isAfterManualInteraction) { + this._sessionStore.clear(); + this._onDidDiscard.fire(); + } + return undefined; + } + + const viewZoneIds = new Set(); + const newDecorations: IModelDeltaDecoration[] = []; + const mightContainNonBasicASCII = this._session.textModel0.mightContainNonBasicASCII() ?? false; + const mightContainRTL = this._session.textModel0.mightContainRTL() ?? false; + const renderOptions = RenderOptions.fromEditor(this._editor); + + let widgetData: { distance: number; position: Position; actions: IAction[] } | undefined; + + for (const { original, modified, innerChanges } of diff.changes) { + + const modifiedRange: Range = modified.isEmpty + ? new Range(modified.startLineNumber, 1, modified.startLineNumber, this._session.textModelN.getLineLength(modified.startLineNumber)) + : new Range(modified.startLineNumber, 1, modified.endLineNumberExclusive - 1, this._session.textModelN.getLineLength(modified.endLineNumberExclusive - 1)); + + const hasBeenInteractedWith = this._modifiedRangesThatHaveBeenInteractedWith.some(id => { + const range = this._session.textModelN.getDecorationRange(id); + return range && Range.areIntersecting(range, modifiedRange); + }); + + if (hasBeenInteractedWith) { + continue; + } + + if (innerChanges) { + for (const { modifiedRange } of innerChanges) { + newDecorations.push({ + range: modifiedRange, + options: { + description: 'inline-modified', + className: 'inline-chat-inserted-range', + } + }); + newDecorations.push({ + range: modifiedRange, + options: { + description: 'inline-modified', + className: 'inline-chat-inserted-range-linehighlight', + isWholeLine: true + } + }); + } + } + + // original view zone + const source = new LineSource( + original.mapToLineArray(l => this._session.textModel0.tokenization.getLineTokens(l)), + [], + mightContainNonBasicASCII, + mightContainRTL, + ); + + const domNode = document.createElement('div'); + domNode.className = 'inline-chat-original-zone2'; + const result = renderLines(source, renderOptions, [new InlineDecoration(new Range(original.startLineNumber, 1, original.startLineNumber, 1), '', InlineDecorationType.Regular)], domNode); + + let myViewZoneId: string = ''; + const myViewZone: IViewZone = { + afterLineNumber: modifiedRange.startLineNumber - 1, + heightInLines: result.heightInLines, + domNode, + }; + + if (isFinalChanges) { + + const [id] = this._modifiedRangesDecorations.append([{ range: modifiedRange, options: this._decoModifiedInteractedWith }]); + + const actions = [ + toAction({ + id: 'accept', + label: localize('accept', "Accept"), + class: ThemeIcon.asClassName(Codicon.check), + run: () => { + this._modifiedRangesThatHaveBeenInteractedWith.push(id); + return this._showDiff(true, true); + } + }), + toAction({ + id: 'discard', + label: localize('discard', "Discard"), + class: ThemeIcon.asClassName(Codicon.discard), + run: () => { + const edits: ISingleEditOperation[] = []; + for (const innerChange of innerChanges!) { + const originalValue = this._session.textModel0.getValueInRange(innerChange.originalRange); + edits.push(EditOperation.replace(innerChange.modifiedRange, originalValue)); + } + this._session.textModelN.pushEditOperations(null, edits, () => null); + return this._showDiff(true, true); + } + }), + ]; + + if (!original.isEmpty) { + actions.push(toAction({ + id: 'diff', + label: localize('compare', "Compare"), + checked: false, + class: ThemeIcon.asClassName(Codicon.diff), + run: () => { + const scrollState = StableEditorScrollState.capture(this._editor); + if (!viewZoneIds.has(myViewZoneId)) { + this._editor.changeViewZones(accessor => { + myViewZoneId = accessor.addZone(myViewZone); + viewZoneIds.add(myViewZoneId); + }); + } else { + this._editor.changeViewZones(accessor => { + accessor.removeZone(myViewZoneId); + viewZoneIds.delete(myViewZoneId); + }); + } + scrollState.restore(this._editor); + } + })); + } + + this._sessionStore.add(this._session.textModelN.onDidChangeContent(e => { + + for (const editRange of e.changes.map(c => c.range).flat()) { + if (Range.areIntersectingOrTouching(modifiedRange, editRange)) { + // implicit accepted + this._modifiedRangesThatHaveBeenInteractedWith.push(id); + } + } + this._showDiff(true, true); + })); + + + const zoneLineNumber = this._zone.position!.lineNumber; + const myDistance = zoneLineNumber <= modifiedRange.startLineNumber + ? modifiedRange.startLineNumber - zoneLineNumber + : zoneLineNumber - modifiedRange.endLineNumber; + + if (!widgetData || widgetData.distance > myDistance) { + widgetData = { distance: myDistance, position: modifiedRange.getStartPosition().delta(-1), actions }; + } + } + } + + if (widgetData) { + this._zone.widget.setExtraButtons(widgetData.actions); + this._zone.updatePositionAndHeight(widgetData.position); + this._editor.revealPositionInCenterIfOutsideViewport(widgetData.position); + } + + const decorations = this._editor.createDecorationsCollection(newDecorations); + + this._sessionStore.add(toDisposable(() => { + decorations.clear(); + this._editor.changeViewZones(accessor => viewZoneIds.forEach(accessor.removeZone, accessor)); + viewZoneIds.clear(); + this._zone.widget.setExtraButtons([]); + })); + + if (isAfterManualInteraction && newDecorations.length === 0) { + this._sessionStore.clear(); + this._onDidAccept.fire(); + } + + return widgetData?.position; + } + + override async renderChanges(response: ReplyResponse) { + + if (response.untitledTextModel) { + this._zone.widget.showCreatePreview(response.untitledTextModel); + } else { + this._zone.widget.hideCreatePreview(); + } + + return await this._showDiff(true, false); + } + + private static _undoModelUntil(model: ITextModel, targetAltVersion: number): void { + while (targetAltVersion < model.getAlternativeVersionId() && model.canUndo()) { + model.undo(); + } + } + + protected _updateSummaryMessage(mappings: readonly LineRangeMapping[]) { + let linesChanged = 0; + for (const change of mappings) { + linesChanged += change.changedLineCount; + } + let message: string; + if (linesChanged === 0) { + message = localize('lines.0', "Nothing changed"); + } else if (linesChanged === 1) { + message = localize('lines.1', "Changed 1 line"); + } else { + message = localize('lines.N', "Changed {0} lines", linesChanged); + } + this._zone.widget.updateStatus(message); + } + + override needsMargin(): boolean { + return true; + } + + hasFocus(): boolean { + return this._zone.widget.hasFocus(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index f868fdeb93d..725461bced1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -12,7 +12,7 @@ import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; +import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, ACTION_ACCEPT_CHANGES } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { EventType, Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset, getWindow } from 'vs/base/browser/dom'; import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event'; @@ -46,7 +46,7 @@ import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibil import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ExpansionState } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IMenuWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; +import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar, WorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; @@ -65,11 +65,13 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ILogService } from 'vs/platform/log/common/log'; -import { ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; +import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IChatReplyFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; +import { IAction } from 'vs/base/common/actions'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -185,6 +187,7 @@ export class InlineChatWidget { h('div.followUps.hidden@followUps'), h('div.status@status', [ h('div.label.info.hidden@infoLabel'), + h('div.actions.hidden@extraToolbar'), h('div.actions.hidden@statusToolbar'), h('div.label.status.hidden@statusLabel'), h('div.actions.hidden@feedbackToolbar'), @@ -384,19 +387,20 @@ export class InlineChatWidget { this._progressBar = new ProgressBar(this._elements.progress); this._store.add(this._progressBar); - const workbenchMenubarOptions: IMenuWorkbenchButtonBarOptions = { + const workbenchMenubarOptions: IWorkbenchButtonBarOptions = { telemetrySource: 'interactiveEditorWidget-toolbar', buttonConfigProvider: action => { if (action.id === ACTION_REGENERATE_RESPONSE) { - return { showIcon: true, showLabel: false }; - } else if (action.id === ACTION_VIEW_IN_CHAT) { + 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 }; } - return undefined; } }; const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, _options.statusMenuId, workbenchMenubarOptions); - this._store.add(statusButtonBar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); + this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); this._store.add(statusButtonBar); @@ -565,6 +569,7 @@ export class InlineChatWidget { } updateToolbar(show: boolean) { + this._elements.extraToolbar.classList.toggle('hidden', !show); this._elements.statusToolbar.classList.toggle('hidden', !show); this._elements.feedbackToolbar.classList.toggle('hidden', !show); this._elements.status.classList.toggle('actions', show); @@ -572,6 +577,14 @@ export class InlineChatWidget { this._onDidChangeHeight.fire(); } + private _extraButtonsCleanup = this._store.add(new MutableDisposable()); + + setExtraButtons(buttons: IAction[]) { + const bar = this._instantiationService.createInstance(WorkbenchButtonBar, this._elements.extraToolbar, { telemetrySource: 'inlineChat' }); + bar.update(buttons); + this._extraButtonsCleanup.value = bar; + } + get expansionState(): ExpansionState { return this._expansionState; } @@ -742,6 +755,7 @@ export class InlineChatWidget { reset(this._elements.statusLabel); this._elements.statusLabel.classList.toggle('hidden', true); + this._elements.extraToolbar.classList.add('hidden'); this._elements.statusToolbar.classList.add('hidden'); this._elements.feedbackToolbar.classList.add('hidden'); this.hideCreatePreview(); diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 65fd54cec1f..83a6f91aea7 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -152,7 +152,7 @@ export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineC export const CTX_INLINE_CHAT_LAST_FEEDBACK = new RawContextKey<'unhelpful' | 'helpful' | ''>('inlineChatLastFeedbackKind', '', localize('inlineChatLastFeedbackKind', "The last kind of feedback that was provided")); export const CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING = new RawContextKey('inlineChatSupportIssueReporting', false, localize('inlineChatSupportIssueReporting', "Whether the interactive editor supports issue reporting")); export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineChatDocumentChanged', false, localize('inlineChatDocumentChanged', "Whether the document has changed concurrently")); -export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.editMode', EditMode.Live); +export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.mode', EditMode.Live); export const CTX_INLINE_CHAT_TOOLBAR_ICON_ENABLED = new RawContextKey('inlineChatToolbarIconEnabled', false, localize('inlineChatToolbarIconEnabled', "Whether the toolbar icon spawning inline chat is enabled.")); // --- (select) action identifier @@ -189,6 +189,7 @@ export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', { d export const enum EditMode { Live = 'live', + Live3 = 'live3', LivePreview = 'livePreview', Preview = 'preview' }