Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordanceWidget.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/inlineChatEditorAffordance.css';
7
import { IDimension } from '../../../../base/browser/dom.js';
8
import * as dom from '../../../../base/browser/dom.js';
9
import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
10
import { Emitter, Event } from '../../../../base/common/event.js';
11
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js';
12
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
13
import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js';
14
import { computeIndentLevel } from '../../../../editor/common/model/utils.js';
15
import { autorun, IObservable } from '../../../../base/common/observable.js';
16
import { MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js';
17
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
18
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
19
import { quickFixCommandId } from '../../../../editor/contrib/codeAction/browser/codeAction.js';
20
import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js';
21
import { IAction } from '../../../../base/common/actions.js';
22
import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
23
import { ThemeIcon } from '../../../../base/common/themables.js';
24
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
25
import { INotificationService } from '../../../../platform/notification/common/notification.js';
26
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
27
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
28
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
29
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
30
import { Codicon } from '../../../../base/common/codicons.js';
31
import { ACTION_START, ACTION_ASK_IN_CHAT } from '../common/inlineChat.js';
32
import { ICommandService } from '../../../../platform/commands/common/commands.js';
33
34
class QuickFixActionViewItem extends MenuEntryActionViewItem {
35
36
readonly #lightBulbStore = this._store.add(new MutableDisposable<DisposableStore>());
37
#currentTitle: string | undefined;
38
readonly #editor: ICodeEditor;
39
40
constructor(
41
action: MenuItemAction,
42
editor: ICodeEditor,
43
@IKeybindingService keybindingService: IKeybindingService,
44
@INotificationService notificationService: INotificationService,
45
@IContextKeyService contextKeyService: IContextKeyService,
46
@IThemeService themeService: IThemeService,
47
@IContextMenuService contextMenuService: IContextMenuService,
48
@IAccessibilityService accessibilityService: IAccessibilityService,
49
@ICommandService commandService: ICommandService
50
) {
51
const wrappedAction = new class extends MenuItemAction {
52
constructor() {
53
super(action.item, action.alt?.item, {}, action.hideActions, action.menuKeybinding, contextKeyService, commandService);
54
}
55
56
elementGetter: () => HTMLElement | undefined = () => undefined;
57
58
override async run(...args: unknown[]): Promise<void> {
59
const controller = CodeActionController.get(editor);
60
const info = controller?.lightBulbState.get();
61
const element = this.elementGetter();
62
if (controller && info && element) {
63
const { bottom, left } = element.getBoundingClientRect();
64
await controller.showCodeActions(info.trigger, info.actions, { x: left, y: bottom });
65
}
66
}
67
};
68
69
super(wrappedAction, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService);
70
71
this.#editor = editor;
72
wrappedAction.elementGetter = () => this.element;
73
}
74
75
override render(container: HTMLElement): void {
76
super.render(container);
77
this.#updateFromLightBulb();
78
}
79
80
protected override getTooltip(): string {
81
return this.#currentTitle ?? super.getTooltip();
82
}
83
84
#updateFromLightBulb(): void {
85
const controller = CodeActionController.get(this.#editor);
86
if (!controller) {
87
return;
88
}
89
90
const store = new DisposableStore();
91
this.#lightBulbStore.value = store;
92
93
store.add(autorun(reader => {
94
const info = controller.lightBulbState.read(reader);
95
if (this.label) {
96
// Update icon
97
const icon = info?.icon ?? Codicon.lightBulb;
98
const iconClasses = ThemeIcon.asClassNameArray(icon);
99
this.label.className = '';
100
this.label.classList.add('codicon', 'action-label', ...iconClasses);
101
}
102
103
// Update tooltip
104
this.#currentTitle = info?.title;
105
this.updateTooltip();
106
}));
107
}
108
}
109
110
class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem {
111
112
readonly #kbLabel: string | undefined;
113
114
constructor(
115
action: MenuItemAction,
116
@IKeybindingService keybindingService: IKeybindingService,
117
@INotificationService notificationService: INotificationService,
118
@IContextKeyService contextKeyService: IContextKeyService,
119
@IThemeService themeService: IThemeService,
120
@IContextMenuService contextMenuService: IContextMenuService,
121
@IAccessibilityService accessibilityService: IAccessibilityService
122
) {
123
super(action, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService);
124
this.options.label = true;
125
this.options.icon = false;
126
this.#kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined;
127
}
128
129
protected override updateLabel(): void {
130
if (this.label) {
131
dom.reset(this.label,
132
this.action.label,
133
...(this.#kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this.#kbLabel)] : [])
134
);
135
}
136
}
137
}
138
139
/**
140
* Content widget that shows a small sparkle icon at the cursor position.
141
* When clicked, it shows the overlay widget for inline chat.
142
*/
143
export class InlineChatAffordanceWidget extends Disposable implements IContentWidget {
144
145
static #idPool = 0;
146
147
readonly #id = `inline-chat-content-widget-${InlineChatAffordanceWidget.#idPool++}`;
148
readonly #domNode: HTMLElement;
149
#position: IContentWidgetPosition | null = null;
150
#isVisible = false;
151
152
readonly #onDidRunAction = this._store.add(new Emitter<string>());
153
readonly onDidRunAction: Event<string> = this.#onDidRunAction.event;
154
155
readonly allowEditorOverflow = true;
156
readonly suppressMouseDown = false;
157
158
readonly #editor: ICodeEditor;
159
160
constructor(
161
editor: ICodeEditor,
162
selection: IObservable<Selection | undefined>,
163
@IInstantiationService instantiationService: IInstantiationService,
164
) {
165
super();
166
167
this.#editor = editor;
168
169
// Create the widget DOM
170
this.#domNode = dom.$('.inline-chat-content-widget');
171
172
// Create toolbar with the inline chat start action
173
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#domNode, MenuId.InlineChatEditorAffordance, {
174
telemetrySource: 'inlineChatEditorAffordance',
175
hiddenItemStrategy: HiddenItemStrategy.Ignore,
176
menuOptions: { renderShortTitle: true },
177
toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true },
178
actionViewItemProvider: (action: IAction) => {
179
if (action instanceof MenuItemAction && action.id === quickFixCommandId) {
180
return instantiationService.createInstance(QuickFixActionViewItem, action, this.#editor);
181
}
182
if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT || action.id === 'inlineChat.fixDiagnostics')) {
183
return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action);
184
}
185
return undefined;
186
}
187
}));
188
this._store.add(toolbar.actionRunner.onDidRun((e) => {
189
this.#onDidRunAction.fire(e.action.id);
190
this.#hide();
191
}));
192
193
this._store.add(autorun(r => {
194
const sel = selection.read(r);
195
if (sel) {
196
this.#show(sel);
197
} else {
198
this.#hide();
199
}
200
}));
201
202
this._store.add(this.#editor.onDidScrollChange(() => {
203
const sel = selection.get();
204
if (!sel) {
205
return;
206
}
207
const isInViewport = this.#isPositionInViewport();
208
if (isInViewport && !this.#isVisible) {
209
this.#show(sel);
210
} else if (!isInViewport && this.#isVisible) {
211
this.#hide();
212
}
213
}));
214
}
215
216
#show(selection: Selection): void {
217
218
if (selection.isEmpty()) {
219
this.#showAtLineStart(selection.getPosition().lineNumber);
220
} else {
221
this.#showAtSelection(selection);
222
}
223
224
if (this.#isVisible) {
225
this.#editor.layoutContentWidget(this);
226
} else {
227
this.#editor.addContentWidget(this);
228
this.#isVisible = true;
229
}
230
}
231
232
#showAtSelection(selection: Selection): void {
233
const cursorPosition = selection.getPosition();
234
const direction = selection.getDirection();
235
236
const preference = direction === SelectionDirection.RTL
237
? ContentWidgetPositionPreference.ABOVE
238
: ContentWidgetPositionPreference.BELOW;
239
240
this.#position = {
241
position: cursorPosition,
242
preference: [preference],
243
};
244
}
245
246
#showAtLineStart(lineNumber: number): void {
247
const model = this.#editor.getModel();
248
if (!model) {
249
return;
250
}
251
252
const tabSize = model.getOptions().tabSize;
253
const fontInfo = this.#editor.getOptions().get(EditorOption.fontInfo);
254
const lineContent = model.getLineContent(lineNumber);
255
const indent = computeIndentLevel(lineContent, tabSize);
256
const lineHasSpace = indent < 0 ? true : fontInfo.spaceWidth * indent > 22;
257
258
let effectiveLineNumber = lineNumber;
259
260
if (!lineHasSpace) {
261
const isLineEmptyOrIndented = (ln: number): boolean => {
262
const content = model.getLineContent(ln);
263
return /^\s*$|^\s+/.test(content);
264
};
265
266
const lineCount = model.getLineCount();
267
if (lineNumber > 1 && isLineEmptyOrIndented(lineNumber - 1)) {
268
effectiveLineNumber = lineNumber - 1;
269
} else if (lineNumber < lineCount && isLineEmptyOrIndented(lineNumber + 1)) {
270
effectiveLineNumber = lineNumber + 1;
271
}
272
}
273
274
const effectiveColumnNumber = /^\S\s*$/.test(model.getLineContent(effectiveLineNumber)) ? 2 : 1;
275
276
this.#position = {
277
position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber },
278
preference: [ContentWidgetPositionPreference.EXACT],
279
};
280
}
281
282
#isPositionInViewport(): boolean {
283
const widgetPosition = this.#position?.position;
284
if (!widgetPosition) {
285
return false;
286
}
287
288
// Check vertical visibility
289
const visibleRanges = this.#editor.getVisibleRanges();
290
const isLineVisible = visibleRanges.some(range =>
291
widgetPosition.lineNumber >= range.startLineNumber && widgetPosition.lineNumber <= range.endLineNumber
292
);
293
if (!isLineVisible) {
294
return false;
295
}
296
297
// Check horizontal visibility
298
const scrolledPos = this.#editor.getScrolledVisiblePosition(widgetPosition);
299
if (!scrolledPos) {
300
return false;
301
}
302
const layoutInfo = this.#editor.getOptions().get(EditorOption.layoutInfo);
303
return scrolledPos.left >= 0 && scrolledPos.left <= layoutInfo.width;
304
}
305
306
#hide(): void {
307
if (this.#isVisible) {
308
this.#isVisible = false;
309
this.#editor.removeContentWidget(this);
310
}
311
}
312
313
getId(): string {
314
return this.#id;
315
}
316
317
getDomNode(): HTMLElement {
318
return this.#domNode;
319
}
320
321
getPosition(): IContentWidgetPosition | null {
322
return this.#position;
323
}
324
325
beforeRender(): IDimension | null {
326
const position = this.#editor.getPosition();
327
const lineHeight = position ? this.#editor.getLineHeightForPosition(position) : this.#editor.getOption(EditorOption.lineHeight);
328
329
this.#domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`);
330
331
return null;
332
}
333
334
override dispose(): void {
335
if (this.#isVisible) {
336
this.#editor.removeContentWidget(this);
337
}
338
super.dispose();
339
}
340
}
341
342