Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts
4780 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 { h, n } from '../../../../../base/browser/dom.js';
7
import { renderMarkdown } from '../../../../../base/browser/markdownRenderer.js';
8
import { ActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';
9
import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
10
import { Action, IAction, Separator } from '../../../../../base/common/actions.js';
11
import { equals } from '../../../../../base/common/arrays.js';
12
import { RunOnceScheduler } from '../../../../../base/common/async.js';
13
import { Codicon } from '../../../../../base/common/codicons.js';
14
import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js';
15
import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js';
16
import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, observableFromEvent } from '../../../../../base/common/observable.js';
17
import { OS } from '../../../../../base/common/platform.js';
18
import { ThemeIcon } from '../../../../../base/common/themables.js';
19
import { localize } from '../../../../../nls.js';
20
import { MenuEntryActionViewItem, getActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
21
import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
22
import { IMenuService, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js';
23
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
24
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
25
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
26
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
27
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
28
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
29
import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js';
30
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../browser/editorBrowser.js';
31
import { EditorOption } from '../../../../common/config/editorOptions.js';
32
import { Position } from '../../../../common/core/position.js';
33
import { InlineCompletionCommand, InlineCompletionTriggerKind, InlineCompletionWarning } from '../../../../common/languages.js';
34
import { PositionAffinity } from '../../../../common/model.js';
35
import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from '../controller/commandIds.js';
36
import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js';
37
import './inlineCompletionsHintsWidget.css';
38
39
export class InlineCompletionsHintsWidget extends Disposable {
40
private readonly alwaysShowToolbar;
41
42
private sessionPosition: Position | undefined;
43
44
private readonly position;
45
46
constructor(
47
private readonly editor: ICodeEditor,
48
private readonly model: IObservable<InlineCompletionsModel | undefined>,
49
@IInstantiationService private readonly instantiationService: IInstantiationService,
50
) {
51
super();
52
this.alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always');
53
this.sessionPosition = undefined;
54
this.position = derived(this, reader => {
55
const ghostText = this.model.read(reader)?.primaryGhostText.read(reader);
56
57
if (!this.alwaysShowToolbar.read(reader) || !ghostText || ghostText.parts.length === 0) {
58
this.sessionPosition = undefined;
59
return null;
60
}
61
62
const firstColumn = ghostText.parts[0].column;
63
if (this.sessionPosition && this.sessionPosition.lineNumber !== ghostText.lineNumber) {
64
this.sessionPosition = undefined;
65
}
66
67
const position = new Position(ghostText.lineNumber, Math.min(firstColumn, this.sessionPosition?.column ?? Number.MAX_SAFE_INTEGER));
68
this.sessionPosition = position;
69
return position;
70
});
71
72
this._register(autorunWithStore((reader, store) => {
73
/** @description setup content widget */
74
const model = this.model.read(reader);
75
if (!model || !this.alwaysShowToolbar.read(reader)) {
76
return;
77
}
78
79
const contentWidgetValue = derived((reader) => {
80
const contentWidget = reader.store.add(this.instantiationService.createInstance(
81
InlineSuggestionHintsContentWidget.hot.read(reader),
82
this.editor,
83
true,
84
this.position,
85
model.selectedInlineCompletionIndex,
86
model.inlineCompletionsCount,
87
model.activeCommands,
88
model.warning,
89
() => { },
90
));
91
editor.addContentWidget(contentWidget);
92
reader.store.add(toDisposable(() => editor.removeContentWidget(contentWidget)));
93
94
reader.store.add(autorun(reader => {
95
/** @description request explicit */
96
const position = this.position.read(reader);
97
if (!position) {
98
return;
99
}
100
if (model.lastTriggerKind.read(reader) !== InlineCompletionTriggerKind.Explicit) {
101
model.triggerExplicitly();
102
}
103
}));
104
return contentWidget;
105
});
106
107
const hadPosition = derivedObservableWithCache(this, (reader, lastValue) => !!this.position.read(reader) || !!lastValue);
108
store.add(autorun(reader => {
109
if (hadPosition.read(reader)) {
110
contentWidgetValue.read(reader);
111
}
112
}));
113
}));
114
}
115
}
116
117
const inlineSuggestionHintsNextIcon = registerIcon('inline-suggestion-hints-next', Codicon.chevronRight, localize('parameterHintsNextIcon', 'Icon for show next parameter hint.'));
118
const inlineSuggestionHintsPreviousIcon = registerIcon('inline-suggestion-hints-previous', Codicon.chevronLeft, localize('parameterHintsPreviousIcon', 'Icon for show previous parameter hint.'));
119
120
export class InlineSuggestionHintsContentWidget extends Disposable implements IContentWidget {
121
public static readonly hot = createHotClass(this);
122
123
private static _dropDownVisible = false;
124
public static get dropDownVisible() { return this._dropDownVisible; }
125
126
private static id = 0;
127
128
private readonly id;
129
public readonly allowEditorOverflow;
130
public readonly suppressMouseDown;
131
132
private readonly _warningMessageContentNode;
133
134
private readonly _warningMessageNode;
135
136
private readonly nodes;
137
138
private createCommandAction(commandId: string, label: string, iconClassName: string): Action {
139
const action = new Action(
140
commandId,
141
label,
142
iconClassName,
143
true,
144
() => this._commandService.executeCommand(commandId),
145
);
146
action.tooltip = this.keybindingService.appendKeybinding(label, commandId, this._contextKeyService);
147
return action;
148
}
149
150
private readonly previousAction;
151
private readonly availableSuggestionCountAction;
152
private readonly nextAction;
153
154
private readonly toolBar: CustomizedMenuWorkbenchToolBar;
155
156
// TODO@hediet: deprecate MenuId.InlineCompletionsActions
157
private readonly inlineCompletionsActionsMenus;
158
159
private readonly clearAvailableSuggestionCountLabelDebounced;
160
161
private readonly disableButtonsDebounced;
162
163
constructor(
164
private readonly editor: ICodeEditor,
165
private readonly withBorder: boolean,
166
private readonly _position: IObservable<Position | null>,
167
private readonly _currentSuggestionIdx: IObservable<number>,
168
private readonly _suggestionCount: IObservable<number | undefined>,
169
private readonly _extraCommands: IObservable<InlineCompletionCommand[]>,
170
private readonly _warning: IObservable<InlineCompletionWarning | undefined>,
171
private readonly _relayout: () => void,
172
@ICommandService private readonly _commandService: ICommandService,
173
@IInstantiationService instantiationService: IInstantiationService,
174
@IKeybindingService private readonly keybindingService: IKeybindingService,
175
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
176
@IMenuService private readonly _menuService: IMenuService,
177
) {
178
super();
179
this.id = `InlineSuggestionHintsContentWidget${InlineSuggestionHintsContentWidget.id++}`;
180
this.allowEditorOverflow = true;
181
this.suppressMouseDown = false;
182
this._warningMessageContentNode = derived((reader) => {
183
const warning = this._warning.read(reader);
184
if (!warning) {
185
return undefined;
186
}
187
if (typeof warning.message === 'string') {
188
return warning.message;
189
}
190
const markdownElement = reader.store.add(renderMarkdown(warning.message));
191
return markdownElement.element;
192
});
193
this._warningMessageNode = n.div({
194
class: 'warningMessage',
195
style: {
196
maxWidth: 400,
197
margin: 4,
198
marginBottom: 4,
199
display: derived(reader => this._warning.read(reader) ? 'block' : 'none'),
200
}
201
}, [
202
this._warningMessageContentNode,
203
]).keepUpdated(this._store);
204
this.nodes = h('div.inlineSuggestionsHints', { className: this.withBorder ? 'monaco-hover monaco-hover-content' : '' }, [
205
this._warningMessageNode.element,
206
h('div@toolBar'),
207
]);
208
this.previousAction = this._register(this.createCommandAction(showPreviousInlineSuggestionActionId, localize('previous', 'Previous'), ThemeIcon.asClassName(inlineSuggestionHintsPreviousIcon)));
209
this.availableSuggestionCountAction = this._register(new Action('inlineSuggestionHints.availableSuggestionCount', '', undefined, false));
210
this.nextAction = this._register(this.createCommandAction(showNextInlineSuggestionActionId, localize('next', 'Next'), ThemeIcon.asClassName(inlineSuggestionHintsNextIcon)));
211
this.inlineCompletionsActionsMenus = this._register(this._menuService.createMenu(
212
MenuId.InlineCompletionsActions,
213
this._contextKeyService
214
));
215
this.clearAvailableSuggestionCountLabelDebounced = this._register(new RunOnceScheduler(() => {
216
this.availableSuggestionCountAction.label = '';
217
}, 100));
218
this.disableButtonsDebounced = this._register(new RunOnceScheduler(() => {
219
this.previousAction.enabled = this.nextAction.enabled = false;
220
}, 100));
221
222
this._register(autorun(reader => {
223
this._warningMessageContentNode.read(reader);
224
this._warningMessageNode.readEffect(reader);
225
// Only update after the warning message node has been rendered
226
this._relayout();
227
}));
228
229
this.toolBar = this._register(instantiationService.createInstance(CustomizedMenuWorkbenchToolBar, this.nodes.toolBar, MenuId.InlineSuggestionToolbar, {
230
menuOptions: { renderShortTitle: true },
231
toolbarOptions: { primaryGroup: g => g.startsWith('primary') },
232
actionViewItemProvider: (action, options) => {
233
if (action instanceof MenuItemAction) {
234
return instantiationService.createInstance(StatusBarViewItem, action, undefined);
235
}
236
if (action === this.availableSuggestionCountAction) {
237
const a = new ActionViewItemWithClassName(undefined, action, { label: true, icon: false });
238
a.setClass('availableSuggestionCount');
239
return a;
240
}
241
return undefined;
242
},
243
telemetrySource: 'InlineSuggestionToolbar',
244
}));
245
246
this.toolBar.setPrependedPrimaryActions([
247
this.previousAction,
248
this.availableSuggestionCountAction,
249
this.nextAction,
250
]);
251
252
this._register(this.toolBar.onDidChangeDropdownVisibility(e => {
253
InlineSuggestionHintsContentWidget._dropDownVisible = e;
254
}));
255
256
this._register(autorun(reader => {
257
/** @description update position */
258
this._position.read(reader);
259
this.editor.layoutContentWidget(this);
260
}));
261
262
this._register(autorun(reader => {
263
/** @description counts */
264
const suggestionCount = this._suggestionCount.read(reader);
265
const currentSuggestionIdx = this._currentSuggestionIdx.read(reader);
266
267
if (suggestionCount !== undefined) {
268
this.clearAvailableSuggestionCountLabelDebounced.cancel();
269
this.availableSuggestionCountAction.label = `${currentSuggestionIdx + 1}/${suggestionCount}`;
270
} else {
271
this.clearAvailableSuggestionCountLabelDebounced.schedule();
272
}
273
274
if (suggestionCount !== undefined && suggestionCount > 1) {
275
this.disableButtonsDebounced.cancel();
276
this.previousAction.enabled = this.nextAction.enabled = true;
277
} else {
278
this.disableButtonsDebounced.schedule();
279
}
280
}));
281
282
this._register(autorun(reader => {
283
/** @description extra commands */
284
const extraCommands = this._extraCommands.read(reader);
285
const extraActions = extraCommands.map<IAction>(c => ({
286
class: undefined,
287
id: c.command.id,
288
enabled: true,
289
tooltip: c.command.tooltip || '',
290
label: c.command.title,
291
run: (event) => {
292
return this._commandService.executeCommand(c.command.id);
293
},
294
}));
295
296
for (const [_, group] of this.inlineCompletionsActionsMenus.getActions()) {
297
for (const action of group) {
298
if (action instanceof MenuItemAction) {
299
extraActions.push(action);
300
}
301
}
302
}
303
304
if (extraActions.length > 0) {
305
extraActions.unshift(new Separator());
306
}
307
308
this.toolBar.setAdditionalSecondaryActions(extraActions);
309
}));
310
}
311
312
getId(): string { return this.id; }
313
314
getDomNode(): HTMLElement {
315
return this.nodes.root;
316
}
317
318
getPosition(): IContentWidgetPosition | null {
319
return {
320
position: this._position.get(),
321
preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW],
322
positionAffinity: PositionAffinity.LeftOfInjectedText,
323
};
324
}
325
}
326
327
class ActionViewItemWithClassName extends ActionViewItem {
328
private _className: string | undefined = undefined;
329
330
setClass(className: string | undefined): void {
331
this._className = className;
332
}
333
334
override render(container: HTMLElement): void {
335
super.render(container);
336
if (this._className) {
337
container.classList.add(this._className);
338
}
339
}
340
341
protected override updateTooltip(): void {
342
// NOOP, disable tooltip
343
}
344
}
345
346
class StatusBarViewItem extends MenuEntryActionViewItem {
347
protected override updateLabel() {
348
const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService, true);
349
if (!kb) {
350
return super.updateLabel();
351
}
352
if (this.label) {
353
const div = h('div.keybinding').root;
354
355
const k = this._register(new KeybindingLabel(div, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions }));
356
k.set(kb);
357
this.label.textContent = this._action.label;
358
this.label.appendChild(div);
359
this.label.classList.add('inlineSuggestionStatusBarItemLabel');
360
}
361
}
362
363
protected override updateTooltip(): void {
364
// NOOP, disable tooltip
365
}
366
}
367
368
export class CustomizedMenuWorkbenchToolBar extends WorkbenchToolBar {
369
private readonly menu;
370
private additionalActions: IAction[];
371
private prependedPrimaryActions: IAction[];
372
private additionalPrimaryActions: IAction[];
373
374
constructor(
375
container: HTMLElement,
376
private readonly menuId: MenuId,
377
private readonly options2: IMenuWorkbenchToolBarOptions | undefined,
378
@IMenuService private readonly menuService: IMenuService,
379
@IContextKeyService private readonly contextKeyService: IContextKeyService,
380
@IContextMenuService contextMenuService: IContextMenuService,
381
@IKeybindingService keybindingService: IKeybindingService,
382
@ICommandService commandService: ICommandService,
383
@ITelemetryService telemetryService: ITelemetryService,
384
) {
385
super(container, { resetMenu: menuId, ...options2 }, menuService, contextKeyService, contextMenuService, keybindingService, commandService, telemetryService);
386
this.menu = this._store.add(this.menuService.createMenu(this.menuId, this.contextKeyService, { emitEventsForSubmenuChanges: true }));
387
this.additionalActions = [];
388
this.prependedPrimaryActions = [];
389
this.additionalPrimaryActions = [];
390
391
this._store.add(this.menu.onDidChange(() => this.updateToolbar()));
392
this.updateToolbar();
393
}
394
395
private updateToolbar(): void {
396
const { primary, secondary } = getActionBarActions(
397
this.menu.getActions(this.options2?.menuOptions),
398
this.options2?.toolbarOptions?.primaryGroup, this.options2?.toolbarOptions?.shouldInlineSubmenu, this.options2?.toolbarOptions?.useSeparatorsInPrimaryActions
399
);
400
401
secondary.push(...this.additionalActions);
402
primary.unshift(...this.prependedPrimaryActions);
403
primary.push(...this.additionalPrimaryActions);
404
this.setActions(primary, secondary);
405
}
406
407
setPrependedPrimaryActions(actions: IAction[]): void {
408
if (equals(this.prependedPrimaryActions, actions, (a, b) => a === b)) {
409
return;
410
}
411
412
this.prependedPrimaryActions = actions;
413
this.updateToolbar();
414
}
415
416
setAdditionalPrimaryActions(actions: IAction[]): void {
417
if (equals(this.additionalPrimaryActions, actions, (a, b) => a === b)) {
418
return;
419
}
420
421
this.additionalPrimaryActions = actions;
422
this.updateToolbar();
423
}
424
425
setAdditionalSecondaryActions(actions: IAction[]): void {
426
if (equals(this.additionalActions, actions, (a, b) => a === b)) {
427
return;
428
}
429
430
this.additionalActions = actions;
431
this.updateToolbar();
432
}
433
}
434
435