diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 05f37e2f578..83b20b66e15 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -98,6 +98,7 @@ export interface IChatWidgetViewOptions { inputSideToolbar?: MenuId; telemetrySource?: string; }; + editorOverflowWidgetsDomNode?: HTMLElement; } export interface IChatViewViewContext { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 5ae86608afc..d21e7736394 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -47,11 +47,23 @@ import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; const $ = dom.$; const INPUT_EDITOR_MAX_HEIGHT = 250; +interface IChatInputPartOptions { + renderFollowups: boolean; + renderStyle?: 'default' | 'compact'; + menus: { + executeToolbar: MenuId; + inputSideToolbar?: MenuId; + telemetrySource?: string; + }; + editorOverflowWidgetsDomNode?: HTMLElement; +} + export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { static readonly INPUT_SCHEME = 'chatSessionInput'; private static _counter = 0; @@ -120,11 +132,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge constructor( // private readonly editorOptions: ChatEditorOptions, // TODO this should be used private readonly location: ChatAgentLocation, - private readonly options: { - renderFollowups: boolean; - renderStyle?: 'default' | 'compact'; - menus: { executeToolbar: MenuId; inputSideToolbar?: MenuId; telemetrySource?: string }; - }, + private readonly options: IChatInputPartOptions, @IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService, @IModelService private readonly modelService: IModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -273,7 +281,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement; - const options = getSimpleEditorOptions(this.configurationService); + const options: IEditorConstructionOptions = getSimpleEditorOptions(this.configurationService); + options.overflowWidgetsDomNode = this.options.editorOverflowWidgetsDomNode; options.readOnly = false; options.ariaLabel = this._getAriaLabel(); options.fontFamily = DEFAULT_FONT_FAMILY; diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.css b/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.css deleted file mode 100644 index d2568dbbfed..00000000000 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.css +++ /dev/null @@ -1,11 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.chat-slash-command-content-widget { - background-color: var(--vscode-chat-slashCommandBackground); - color: var(--vscode-chat-slashCommandForeground); - border-radius: 3px; - padding: 1px; -} diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts deleted file mode 100644 index 39aa0aafad3..00000000000 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget.ts +++ /dev/null @@ -1,96 +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 'vs/css!./chatSlashCommandContentWidget'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { Range } from 'vs/editor/common/core/range'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget } from 'vs/editor/browser/editorBrowser'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { localize } from 'vs/nls'; -import * as aria from 'vs/base/browser/ui/aria/aria'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; - -export class SlashCommandContentWidget extends Disposable implements IContentWidget { - private _domNode = document.createElement('div'); - private _lastSlashCommandText: string | undefined; - private _isVisible = false; - - constructor(private _editor: ICodeEditor) { - super(); - - this._domNode.toggleAttribute('hidden', true); - this._domNode.classList.add('chat-slash-command-content-widget'); - - // If backspace at a slash command boundary, remove the slash command - this._register(this._editor.onKeyDown((e) => this._handleKeyDown(e))); - } - - override dispose() { - this.hide(); - super.dispose(); - } - - show() { - if (!this._isVisible) { - this._isVisible = true; - this._domNode.toggleAttribute('hidden', false); - this._editor.addContentWidget(this); - } - } - - hide() { - if (this._isVisible) { - this._isVisible = false; - this._domNode.toggleAttribute('hidden', true); - this._editor.removeContentWidget(this); - } - } - - setCommandText(slashCommand: string) { - this._domNode.innerText = `/${slashCommand} `; - this._lastSlashCommandText = slashCommand; - } - - getId() { - return 'chat-slash-command-content-widget'; - } - - getDomNode() { - return this._domNode; - } - - getPosition() { - return { position: { lineNumber: 1, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] }; - } - - beforeRender(): null { - const lineHeight = this._editor.getOption(EditorOption.lineHeight); - this._domNode.style.lineHeight = `${lineHeight - 2 /*padding*/}px`; - return null; - } - - private _handleKeyDown(e: IKeyboardEvent) { - if (e.keyCode !== KeyCode.Backspace) { - return; - } - - const firstLine = this._editor.getModel()?.getLineContent(1); - const selection = this._editor.getSelection(); - const withSlash = `/${this._lastSlashCommandText} `; - if (!firstLine?.startsWith(withSlash) || !selection?.isEmpty() || selection?.startLineNumber !== 1 || selection?.startColumn !== withSlash.length + 1) { - return; - } - - // Allow to undo the backspace - this._editor.executeEdits('chat-slash-command', [{ - range: new Range(1, 1, 1, selection.startColumn), - text: null - }]); - - // Announce the deletion - aria.alert(localize('exited slash command mode', 'Exited {0} mode', this._lastSlashCommandText)); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 0e0b353dbb3..b0dfc6fb437 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -501,7 +501,8 @@ export class ChatWidget extends Disposable implements IChatWidget { { renderFollowups: options?.renderFollowups ?? true, renderStyle: options?.renderStyle, - menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus } + menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, + editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, } )); this.inputPart.render(container, '', this); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts index 30611a8c49c..c8f1720ab48 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget.ts @@ -18,6 +18,7 @@ import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessi import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents'; import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry'; +import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; export class InlineChatContentWidget implements IContentWidget { @@ -36,6 +37,8 @@ export class InlineChatContentWidget implements IContentWidget { private _visible: boolean = false; private _focusNext: boolean = false; + + private readonly _defaultChatModel: ChatModel; private readonly _widget: ChatWidget; constructor( @@ -43,11 +46,14 @@ export class InlineChatContentWidget implements IContentWidget { @IInstantiationService instaService: IInstantiationService, ) { + this._defaultChatModel = this._store.add(instaService.createInstance(ChatModel, `inlineChatDefaultModel/editorContentWidgetPlaceholder`, undefined)); + this._widget = instaService.createInstance( ChatWidget, ChatAgentLocation.Editor, { resource: true }, { + editorOverflowWidgetsDomNode: _editor.getOverflowWidgetsDomNode(), renderStyle: 'compact', renderInputOnTop: true, supportsFileReferences: false, @@ -66,6 +72,7 @@ export class InlineChatContentWidget implements IContentWidget { this._store.add(this._widget); this._store.add(this._widget.onDidChangeHeight(() => _editor.layoutContentWidget(this))); this._widget.render(this._inputContainer); + this._widget.setModel(this._defaultChatModel, {}); this._domNode.tabIndex = -1; this._domNode.className = 'inline-chat-content-widget interactive-session'; @@ -78,7 +85,9 @@ export class InlineChatContentWidget implements IContentWidget { const tracker = dom.trackFocus(this._domNode); this._store.add(tracker.onDidBlur(() => { - if (this._visible) { + if (this._visible + // && !"ON" + ) { this._onDidBlur.fire(); } })); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 3965a6f254a..2985931da40 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -19,7 +19,7 @@ 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 { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { CompletionItemKind, CompletionList, TextEdit } from 'vs/editor/common/languages'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; @@ -41,7 +41,7 @@ import { InlineChatZoneWidget } from './inlineChatZoneWidget'; import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { StashedSession } from './inlineChatSession'; -import { IValidEditOperation } from 'vs/editor/common/model'; +import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model'; import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { tail } from 'vs/base/common/arrays'; @@ -432,7 +432,11 @@ export class InlineChatController implements IEditorContribution { } })); - // TODO@jrieken + // Update context key + this._ctxSupportIssueReporting.set(this._session.provider.supportIssueReporting ?? false); + + // #region DEBT + // DEBT@jrieken // REMOVE when agents are adopted this._sessionStore.add(this._languageFeatureService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'inline chat commands', @@ -455,14 +459,63 @@ export class InlineChatController implements IEditorContribution { kind: CompletionItemKind.Text, insertText: withSlash, range: Range.fromPositions(new Position(1, 1), position), + command: command.executeImmediately ? { id: 'workbench.action.chat.acceptInput', title: withSlash } : undefined }); } return result; } })); - // Update context key - this._ctxSupportIssueReporting.set(this._session.provider.supportIssueReporting ?? false); + + const updateSlashDecorations = (collection: IEditorDecorationsCollection, model: ITextModel) => { + + const newDecorations: IModelDeltaDecoration[] = []; + for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.command.length - a.command.length)) { + const withSlash = `/${command.command}`; + const firstLine = model.getLineContent(1); + if (firstLine.startsWith(withSlash)) { + newDecorations.push({ + range: new Range(1, 1, 1, withSlash.length + 1), + options: { + description: 'inline-chat-slash-command', + inlineClassName: 'inline-chat-slash-command', + after: { + // Force some space between slash command and placeholder + content: ' ' + } + } + }); + + // inject detail when otherwise empty + if (firstLine.trim() === `/${command.command}`) { + newDecorations.push({ + range: new Range(1, withSlash.length, 1, withSlash.length), + options: { + description: 'inline-chat-slash-command-detail', + after: { + content: `${command.detail}`, + inlineClassName: 'inline-chat-slash-command-detail' + } + } + }); + } + break; + } + } + collection.set(newDecorations); + }; + const inputInputEditor = this._input.value.chatWidget.inputEditor; + const zoneInputEditor = this._zone.value.widget.chatWidget.inputEditor; + const inputDecorations = inputInputEditor.createDecorationsCollection(); + const zoneDecorations = zoneInputEditor.createDecorationsCollection(); + this._sessionStore.add(inputInputEditor.onDidChangeModelContent(() => updateSlashDecorations(inputDecorations, inputInputEditor.getModel()!))); + this._sessionStore.add(zoneInputEditor.onDidChangeModelContent(() => updateSlashDecorations(zoneDecorations, zoneInputEditor.getModel()!))); + this._sessionStore.add(toDisposable(() => { + inputDecorations.clear(); + zoneDecorations.clear(); + })); + + //#endregion ------- DEBT if (!this._session.lastExchange) { return State.WAIT_FOR_INPUT; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 77bb2fc8aab..9781046762c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -93,6 +93,8 @@ export interface IInlineChatWidgetConstructionOptions { * globally. */ editableCodeBlocks?: boolean; + + editorOverflowWidgetsDomNode?: HTMLElement; } export interface IInlineChatMessage { @@ -395,9 +397,18 @@ export class InlineChatWidget { this._chatWidget.setInput(value); } + selectAll(includeSlashCommand: boolean = true) { - // TODO@jrieken includeSlashCommand - this._chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); + // DEBT@jrieken + // REMOVE when agents are adopted + let startColumn = 1; + if (!includeSlashCommand) { + const match = /^(\/\w+)\s*/.exec(this._chatWidget.inputEditor.getModel()!.getLineContent(1)); + if (match) { + startColumn = match[1].length + 1; + } + } + this._chatWidget.inputEditor.setSelection(new Selection(1, startColumn, Number.MAX_SAFE_INTEGER, 1)); } set placeholder(value: string) { @@ -612,7 +623,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @ITextModelService textModelResolverService: ITextModelService, @IChatService chatService: IChatService, ) { - super(ChatAgentLocation.Editor, options, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService); + super(ChatAgentLocation.Editor, { ...options, editorOverflowWidgetsDomNode: _parentEditor.getOverflowWidgetsDomNode() }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService); // preview editors this._previewDiffEditor = new Lazy(() => this._store.add(instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, { diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index 39604e43260..20da82a55f9 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -266,7 +266,10 @@ } .monaco-workbench .inline-chat-slash-command { - opacity: 0; + background-color: var(--vscode-chat-slashCommandBackground); + color: var(--vscode-chat-slashCommandForeground); + border-radius: 4px; + padding: 1px; } .monaco-workbench .inline-chat-slash-command-detail { diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css index f3d1368290d..fc5b00bb6df 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatContentWidget.css @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .monaco-workbench .inline-chat-content-widget { + z-index: 50; padding: 6px 6px 2px 6px; border-radius: 4px; background-color: var(--vscode-inlineChat-background);