Skip to content

Commit 930a745

Browse files
authored
Improve slash command rendering in inline chat (microsoft#187769)
* Render inline chat slash commands as blocks * Allow backspace to delete slash command block * Remove unnecessary template string literal
1 parent 8e7d6f8 commit 930a745

File tree

2 files changed

+78
-3
lines changed

2 files changed

+78
-3
lines changed

src/vs/workbench/contrib/inlineChat/browser/inlineChat.css

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,13 +278,20 @@
278278
}
279279

280280
.monaco-editor .inline-chat-slash-command {
281-
color: var(--vscode-textLink-foreground)
281+
opacity: 0;
282282
}
283283

284284
.monaco-editor .inline-chat-slash-command-detail {
285285
opacity: 0.5;
286286
}
287287

288+
.monaco-editor .inline-chat-slash-command-content-widget {
289+
padding: 0 0.4em;
290+
border-radius: 3px;
291+
background-color: var(--vscode-textCodeBlock-background);
292+
color: var(--vscode-textLink-foreground);
293+
}
294+
288295
/* diff zone */
289296

290297
.monaco-editor .inline-chat-diff-widget {

src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import 'vs/css!./inlineChat';
7-
import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
8-
import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
7+
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
8+
import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
99
import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions';
1010
import { IRange, Range } from 'vs/editor/common/core/range';
1111
import { localize } from 'vs/nls';
@@ -49,6 +49,7 @@ import { ExpansionState } from 'vs/workbench/contrib/inlineChat/browser/inlineCh
4949
import { IdleValue } from 'vs/base/common/async';
5050
import * as aria from 'vs/base/browser/ui/aria/aria';
5151
import { IMenuWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar';
52+
import { KeyCode } from 'vs/base/common/keyCodes';
5253

5354
const defaultAriaLabel = localize('aria-label', "Inline Chat Input");
5455

@@ -180,6 +181,8 @@ export class InlineChatWidget {
180181
private _preferredExpansionState: ExpansionState | undefined;
181182
private _expansionState: ExpansionState = ExpansionState.NOT_CROPPED;
182183

184+
private _slashCommandContentWidget: SlashCommandContentWidget;
185+
183186
constructor(
184187
private readonly parentEditor: ICodeEditor,
185188
@IModelService private readonly _modelService: IModelService,
@@ -278,6 +281,11 @@ export class InlineChatWidget {
278281
this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder));
279282
togglePlaceholder();
280283

284+
// slash command content widget
285+
286+
this._slashCommandContentWidget = new SlashCommandContentWidget(this._inputEditor);
287+
this._store.add(this._slashCommandContentWidget);
288+
281289
// toolbars
282290

283291
const toolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, MENU_INLINE_CHAT_WIDGET, {
@@ -654,6 +662,8 @@ export class InlineChatWidget {
654662
const decorations = this._inputEditor.createDecorationsCollection();
655663

656664
const updateSlashDecorations = () => {
665+
this._slashCommandContentWidget.hide();
666+
657667
const newDecorations: IModelDeltaDecoration[] = [];
658668
for (const command of commands) {
659669
const withSlash = `/${command.command}`;
@@ -664,9 +674,16 @@ export class InlineChatWidget {
664674
options: {
665675
description: 'inline-chat-slash-command',
666676
inlineClassName: 'inline-chat-slash-command',
677+
after: {
678+
// Force some space between slash command and placeholder
679+
content: ' '
680+
}
667681
}
668682
});
669683

684+
this._slashCommandContentWidget.setCommandText(command.command);
685+
this._slashCommandContentWidget.show();
686+
670687
// inject detail when otherwise empty
671688
if (firstLine === `/${command.command} `) {
672689
newDecorations.push({
@@ -691,6 +708,57 @@ export class InlineChatWidget {
691708
}
692709
}
693710

711+
class SlashCommandContentWidget extends Disposable implements IContentWidget {
712+
private _domNode = document.createElement('div');
713+
private _lastSlashCommandText: string | undefined;
714+
715+
constructor(private _editor: ICodeEditor) {
716+
super();
717+
718+
this._domNode.toggleAttribute('hidden', true);
719+
this._domNode.classList.add('inline-chat-slash-command-content-widget');
720+
721+
// If backspace at a slash command boundary, remove the slash command
722+
this._register(this._editor.onKeyUp((e) => {
723+
if (e.keyCode !== KeyCode.Backspace) {
724+
return;
725+
}
726+
727+
const firstLine = this._editor.getModel()?.getLineContent(1);
728+
const selection = this._editor.getSelection();
729+
const withSlash = `/${this._lastSlashCommandText}`;
730+
if (!firstLine?.startsWith(withSlash) || !selection?.isEmpty() || selection?.startLineNumber !== 1 || selection?.startColumn !== withSlash.length + 1) {
731+
return;
732+
}
733+
734+
// Allow to undo the backspace
735+
this._editor.executeEdits('inline-chat-slash-command', [{
736+
range: new Range(1, 1, 1, selection.startColumn),
737+
text: null
738+
}]);
739+
}));
740+
}
741+
742+
show() {
743+
this._domNode.toggleAttribute('hidden', false);
744+
this._editor.addContentWidget(this);
745+
}
746+
747+
hide() {
748+
this._domNode.toggleAttribute('hidden', true);
749+
this._editor.removeContentWidget(this);
750+
}
751+
752+
setCommandText(slashCommand: string) {
753+
this._domNode.innerText = `${slashCommand} `;
754+
this._lastSlashCommandText = slashCommand;
755+
}
756+
757+
getId() { return 'inline-chat-slash-command-content-widget'; }
758+
getDomNode() { return this._domNode; }
759+
getPosition() { return { position: { lineNumber: 1, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] }; }
760+
}
761+
694762
export class InlineChatZoneWidget extends ZoneWidget {
695763

696764
readonly widget: InlineChatWidget;

0 commit comments

Comments
 (0)