diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index 23073e494ba..5beab56e7f2 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -278,13 +278,20 @@ } .monaco-editor .inline-chat-slash-command { - color: var(--vscode-textLink-foreground) + opacity: 0; } .monaco-editor .inline-chat-slash-command-detail { opacity: 0.5; } +.monaco-editor .inline-chat-slash-command-content-widget { + padding: 0 0.4em; + border-radius: 3px; + background-color: var(--vscode-textCodeBlock-background); + color: var(--vscode-textLink-foreground); +} + /* diff zone */ .monaco-editor .inline-chat-diff-widget { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 57077181753..dc9490e8c4f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./inlineChat'; -import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import { localize } from 'vs/nls'; @@ -49,6 +49,7 @@ import { ExpansionState } from 'vs/workbench/contrib/inlineChat/browser/inlineCh import { IdleValue } from 'vs/base/common/async'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { IMenuWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar'; +import { KeyCode } from 'vs/base/common/keyCodes'; const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); @@ -180,6 +181,8 @@ export class InlineChatWidget { private _preferredExpansionState: ExpansionState | undefined; private _expansionState: ExpansionState = ExpansionState.NOT_CROPPED; + private _slashCommandContentWidget: SlashCommandContentWidget; + constructor( private readonly parentEditor: ICodeEditor, @IModelService private readonly _modelService: IModelService, @@ -278,6 +281,11 @@ export class InlineChatWidget { this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder)); togglePlaceholder(); + // slash command content widget + + this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor); + this._store.add(this._slashCommandContentWidget); + // toolbars const toolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, MENU_INLINE_CHAT_WIDGET, { @@ -654,6 +662,8 @@ export class InlineChatWidget { const decorations = this._inputEditor.createDecorationsCollection(); const updateSlashDecorations = () => { + this._slashCommandContentWidget.hide(); + const newDecorations: IModelDeltaDecoration[] = []; for (const command of commands) { const withSlash = `/${command.command}`; @@ -664,9 +674,16 @@ export class InlineChatWidget { options: { description: 'inline-chat-slash-command', inlineClassName: 'inline-chat-slash-command', + after: { + // Force some space between slash command and placeholder + content: ' ' + } } }); + this._slashCommandContentWidget.setCommandText(command.command); + this._slashCommandContentWidget.show(); + // inject detail when otherwise empty if (firstLine === `/${command.command} `) { newDecorations.push({ @@ -691,6 +708,57 @@ export class InlineChatWidget { } } +class SlashCommandContentWidget extends Disposable implements IContentWidget { + private _domNode = document.createElement('div'); + private _lastSlashCommandText: string | undefined; + + constructor(private _editor: ICodeEditor) { + super(); + + this._domNode.toggleAttribute('hidden', true); + this._domNode.classList.add('inline-chat-slash-command-content-widget'); + + // If backspace at a slash command boundary, remove the slash command + this._register(this._editor.onKeyUp((e) => { + 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('inline-chat-slash-command', [{ + range: new Range(1, 1, 1, selection.startColumn), + text: null + }]); + })); + } + + show() { + this._domNode.toggleAttribute('hidden', false); + this._editor.addContentWidget(this); + } + + hide() { + this._domNode.toggleAttribute('hidden', true); + this._editor.removeContentWidget(this); + } + + setCommandText(slashCommand: string) { + this._domNode.innerText = `${slashCommand} `; + this._lastSlashCommandText = slashCommand; + } + + getId() { return 'inline-chat-slash-command-content-widget'; } + getDomNode() { return this._domNode; } + getPosition() { return { position: { lineNumber: 1, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] }; } +} + export class InlineChatZoneWidget extends ZoneWidget { readonly widget: InlineChatWidget;