Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/fixtures/edit/6276.ts
13399 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 } from 'vs/base/browser/dom';
7
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
8
import { KeybindingLabel, unthemedKeybindingLabelOptions } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
9
import { Action, IAction, Separator } from 'vs/base/common/actions';
10
import { equals } from 'vs/base/common/arrays';
11
import { RunOnceScheduler } from 'vs/base/common/async';
12
import { Codicon } from 'vs/base/common/codicons';
13
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
14
import { OS } from 'vs/base/common/platform';
15
import { ThemeIcon } from 'vs/base/common/themables';
16
import 'vs/css!./inlineSuggestionHintsWidget';
17
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
18
import { EditorOption } from 'vs/editor/common/config/editorOptions';
19
import { Position } from 'vs/editor/common/core/position';
20
import { Command } from 'vs/editor/common/languages';
21
import { PositionAffinity } from 'vs/editor/common/model';
22
import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from 'vs/editor/contrib/inlineCompletions/browser/consts';
23
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
24
import { localize } from 'vs/nls';
25
import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
26
import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
27
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
28
import { ICommandService } from 'vs/platform/commands/common/commands';
29
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
30
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
31
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
32
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
33
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
34
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
35
36
export class InlineSuggestionHintsWidget extends Disposable {
37
private readonly widget = this._register(this.instantiationService.createInstance(InlineSuggestionHintsContentWidget, this.editor, true));
38
39
private sessionPosition: Position | undefined = undefined;
40
private isDisposed = false;
41
42
constructor(
43
private readonly editor: ICodeEditor,
44
private readonly model: InlineCompletionsModel,
45
@IInstantiationService private readonly instantiationService: IInstantiationService,
46
) {
47
super();
48
49
editor.addContentWidget(this.widget);
50
this._register(toDisposable(() => editor.removeContentWidget(this.widget)));
51
this._register(model.onDidChange(() => this.update()));
52
this._register(editor.onDidChangeConfiguration(() => this.update()));
53
this.update();
54
}
55
56
override dispose(): void {
57
this.isDisposed = true;
58
super.dispose();
59
}
60
61
private update(): void {
62
if (this.isDisposed) {
63
return;
64
}
65
66
const options = this.editor.getOption(EditorOption.inlineSuggest);
67
if (options.showToolbar !== 'always' || !this.model.ghostText) {
68
this.widget.update(null, 0, undefined, []);
69
this.sessionPosition = undefined;
70
return;
71
}
72
73
if (!this.model.completionSession.value) {
74
return;
75
}
76
77
if (!this.model.completionSession.value.hasBeenTriggeredExplicitly) {
78
this.model.completionSession.value.ensureUpdateWithExplicitContext();
79
}
80
81
const ghostText = this.model.ghostText;
82
83
const firstColumn = ghostText.parts[0].column;
84
if (this.sessionPosition && this.sessionPosition.lineNumber !== ghostText.lineNumber) {
85
this.sessionPosition = undefined;
86
}
87
88
const position = new Position(ghostText.lineNumber, Math.min(firstColumn, this.sessionPosition?.column ?? Number.MAX_SAFE_INTEGER));
89
this.sessionPosition = position;
90
91
this.widget.update(
92
this.sessionPosition,
93
this.model.completionSession.value.currentlySelectedIndex,
94
this.model.completionSession.value.hasBeenTriggeredExplicitly ? this.model.completionSession.value.getInlineCompletionsCountSync() : undefined,
95
this.model.completionSession.value.commands,
96
);
97
}
98
}
99
100
const inlineSuggestionHintsNextIcon = registerIcon('inline-suggestion-hints-next', Codicon.chevronRight, localize('parameterHintsNextIcon', 'Icon for show next parameter hint.'));
101
const inlineSuggestionHintsPreviousIcon = registerIcon('inline-suggestion-hints-previous', Codicon.chevronLeft, localize('parameterHintsPreviousIcon', 'Icon for show previous parameter hint.'));
102
103
export class InlineSuggestionHintsContentWidget extends Disposable implements IContentWidget {
104
private static _dropDownVisible = false;
105
public static get dropDownVisible() { return this._dropDownVisible; }
106
107
private static id = 0;
108
109
private readonly id = `InlineSuggestionHintsContentWidget${InlineSuggestionHintsContentWidget.id++}`;
110
public readonly allowEditorOverflow = true;
111
public readonly suppressMouseDown = false;
112
113
private readonly nodes = h('div.inlineSuggestionsHints', { className: this.withBorder ? '.withBorder' : '' }, [
114
h('div', { style: { display: 'flex' } }, [
115
h('div@actionBar', { className: 'custom-actions' }),
116
h('div@toolBar'),
117
])
118
]);
119
private position: Position | null = null;
120
121
private createCommandAction(commandId: string, label: string, iconClassName: string): Action {
122
const action = new Action(
123
commandId,
124
label,
125
iconClassName,
126
true,
127
() => this._commandService.executeCommand(commandId),
128
);
129
const kb = this.keybindingService.lookupKeybinding(commandId, this._contextKeyService);
130
let tooltip = label;
131
if (kb) {
132
tooltip = localize({ key: 'content', comment: ['A label', 'A keybinding'] }, '{0} ({1})', label, kb.getLabel());
133
}
134
action.tooltip = tooltip;
135
return action;
136
}
137
138
private readonly previousAction = this.createCommandAction(showPreviousInlineSuggestionActionId, localize('previous', 'Previous'), ThemeIcon.asClassName(inlineSuggestionHintsPreviousIcon));
139
private readonly availableSuggestionCountAction = new Action('inlineSuggestionHints.availableSuggestionCount', '', undefined, false);
140
private readonly nextAction = this.createCommandAction(showNextInlineSuggestionActionId, localize('next', 'Next'), ThemeIcon.asClassName(inlineSuggestionHintsNextIcon));
141
142
private readonly toolBar: CustomizedMenuWorkbenchToolBar;
143
144
// TODO@hediet: deprecate MenuId.InlineCompletionsActions
145
private readonly inlineCompletionsActionsMenus = this._register(this._menuService.createMenu(
146
MenuId.InlineCompletionsActions,
147
this._contextKeyService
148
));
149
150
private readonly clearAvailableSuggestionCountLabelDebounced = this._register(new RunOnceScheduler(() => {
151
this.availableSuggestionCountAction.label = '';
152
}, 100));
153
154
private readonly disableButtonsDebounced = this._register(new RunOnceScheduler(() => {
155
this.previousAction.enabled = this.nextAction.enabled = false;
156
}, 100));
157
158
private lastCurrentSuggestionIdx = -1;
159
private lastSuggestionCount = -1;
160
private lastCommands: Command[] = [];
161
162
constructor(
163
private readonly editor: ICodeEditor,
164
private readonly withBorder: boolean,
165
@ICommandService private readonly _commandService: ICommandService,
166
@IInstantiationService instantiationService: IInstantiationService,
167
@IKeybindingService private readonly keybindingService: IKeybindingService,
168
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
169
@IMenuService private readonly _menuService: IMenuService,
170
) {
171
super();
172
173
const actionBar = this._register(new ActionBar(this.nodes.actionBar));
174
175
actionBar.push(this.previousAction, { icon: true, label: false });
176
actionBar.push(this.availableSuggestionCountAction);
177
actionBar.push(this.nextAction, { icon: true, label: false });
178
179
this.toolBar = this._register(instantiationService.createInstance(CustomizedMenuWorkbenchToolBar, this.nodes.toolBar, MenuId.InlineSuggestionToolbar, {
180
menuOptions: { renderShortTitle: true },
181
toolbarOptions: { primaryGroup: g => g.startsWith('primary') },
182
actionViewItemProvider: (action, options) => {
183
return action instanceof MenuItemAction ? instantiationService.createInstance(StatusBarViewItem, action, undefined) : undefined;
184
},
185
telemetrySource: 'InlineSuggestionToolbar',
186
}));
187
188
this._register(this.toolBar.onDidChangeDropdownVisibility(e => {
189
InlineSuggestionHintsContentWidget._dropDownVisible = e;
190
}));
191
}
192
193
public update(position: Position | null, currentSuggestionIdx: number, suggestionCount: number | undefined, extraCommands: Command[]): void {
194
if (this.position === position
195
&& this.lastCurrentSuggestionIdx === currentSuggestionIdx
196
&& this.lastSuggestionCount === suggestionCount
197
&& equals(this.lastCommands, extraCommands)) {
198
// nothing to update
199
return;
200
}
201
202
this.position = position;
203
this.lastCurrentSuggestionIdx = currentSuggestionIdx;
204
this.lastSuggestionCount = suggestionCount ?? -1;
205
this.lastCommands = extraCommands;
206
207
if (suggestionCount !== undefined && suggestionCount > 1) {
208
this.disableButtonsDebounced.cancel();
209
this.previousAction.enabled = this.nextAction.enabled = true;
210
} else {
211
this.disableButtonsDebounced.schedule();
212
}
213
214
if (suggestionCount !== undefined) {
215
this.clearAvailableSuggestionCountLabelDebounced.cancel();
216
this.availableSuggestionCountAction.label = `${currentSuggestionIdx + 1}/${suggestionCount}`;
217
} else {
218
this.clearAvailableSuggestionCountLabelDebounced.schedule();
219
}
220
221
this.editor.layoutContentWidget(this);
222
223
const extraActions = extraCommands.map<IAction>(c => ({
224
class: undefined,
225
id: c.id,
226
enabled: true,
227
tooltip: c.tooltip || '',
228
label: c.title,
229
run: (event) => {
230
return this._commandService.executeCommand(c.id);
231
},
232
}));
233
234
for (const [_, group] of this.inlineCompletionsActionsMenus.getActions()) {
235
for (const action of group) {
236
if (action instanceof MenuItemAction) {
237
extraActions.push(action);
238
}
239
}
240
}
241
242
if (extraActions.length > 0) {
243
extraActions.unshift(new Separator());
244
}
245
246
this.toolBar.setAdditionalSecondaryActions(extraActions);
247
}
248
249
getId(): string { return this.id; }
250
251
getDomNode(): HTMLElement {
252
return this.nodes.root;
253
}
254
255
getPosition(): IContentWidgetPosition | null {
256
return {
257
position: this.position,
258
preference: [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW],
259
positionAffinity: PositionAffinity.LeftOfInjectedText,
260
};
261
}
262
}
263
264
class StatusBarViewItem extends MenuEntryActionViewItem {
265
protected override updateLabel() {
266
const kb = this._keybindingService.lookupKeybinding(this._action.id, this._contextKeyService);
267
if (!kb) {
268
return super.updateLabel();
269
}
270
if (this.label) {
271
const div = h('div.keybinding').root;
272
273
const k = new KeybindingLabel(div, OS, { disableTitle: true, ...unthemedKeybindingLabelOptions });
274
k.set(kb);
275
this.label.textContent = this._action.label;
276
this.label.appendChild(div);
277
this.label.classList.add('inlineSuggestionStatusBarItemLabel');
278
}
279
}
280
}
281
282
export class CustomizedMenuWorkbenchToolBar extends WorkbenchToolBar {
283
private readonly menu = this._store.add(this.menuService.createMenu(this.menuId, this.contextKeyService, { emitEventsForSubmenuChanges: true }));
284
private additionalActions: IAction[] = [];
285
286
constructor(
287
container: HTMLElement,
288
private readonly menuId: MenuId,
289
private readonly options2: IMenuWorkbenchToolBarOptions | undefined,
290
@IMenuService private readonly menuService: IMenuService,
291
@IContextKeyService private readonly contextKeyService: IContextKeyService,
292
@IContextMenuService contextMenuService: IContextMenuService,
293
@IKeybindingService keybindingService: IKeybindingService,
294
@ITelemetryService telemetryService: ITelemetryService,
295
) {
296
super(container, { resetMenu: menuId, ...options2 }, menuService, contextKeyService, contextMenuService, keybindingService, telemetryService);
297
298
this._store.add(this.menu.onDidChange(() => this.updateToolbar()));
299
this.updateToolbar();
300
}
301
302
private updateToolbar(): void {
303
const primary: IAction[] = [];
304
const secondary: IAction[] = [];
305
createAndFillInActionBarActions(
306
this.menu,
307
this.options2?.menuOptions,
308
{ primary, secondary },
309
this.options2?.toolbarOptions?.primaryGroup, this.options2?.toolbarOptions?.shouldInlineSubmenu, this.options2?.toolbarOptions?.useSeparatorsInPrimaryActions
310
);
311
312
secondary.push(...this.additionalActions);
313
this.setActions(primary, secondary);
314
}
315
316
setAdditionalSecondaryActions(actions: IAction[]): void {
317
if (equals(this.additionalActions, actions, (a, b) => a === b)) {
318
// don't update if the actions are the same
319
return;
320
}
321
322
this.additionalActions = actions;
323
this.updateToolbar();
324
}
325
}
326
327