Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/fixtures/gen/inlayHintsController.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 { ModifierKeyEmitter } from 'vs/base/browser/dom';
7
import { isNonEmptyArray } from 'vs/base/common/arrays';
8
import { RunOnceScheduler } from 'vs/base/common/async';
9
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
10
import { onUnexpectedError } from 'vs/base/common/errors';
11
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
12
import { LRUCache } from 'vs/base/common/map';
13
import { IRange } from 'vs/base/common/range';
14
import { assertType } from 'vs/base/common/types';
15
import { URI } from 'vs/base/common/uri';
16
import { IActiveCodeEditor, ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
17
import { ClassNameReference, CssProperties, DynamicCssRules } from 'vs/editor/browser/editorDom';
18
import { StableEditorScrollState } from 'vs/editor/browser/stableEditorScroll';
19
import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions';
20
import { EditOperation } from 'vs/editor/common/core/editOperation';
21
import { Range } from 'vs/editor/common/core/range';
22
import { IEditorContribution } from 'vs/editor/common/editorCommon';
23
import * as languages from 'vs/editor/common/languages';
24
import { IModelDeltaDecoration, InjectedTextCursorStops, InjectedTextOptions, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model';
25
import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel';
26
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce';
27
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
28
import { ITextModelService } from 'vs/editor/common/services/resolverService';
29
import { ClickLinkGesture, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture';
30
import { InlayHintAnchor, InlayHintItem, InlayHintsFragments } from 'vs/editor/contrib/inlayHints/browser/inlayHints';
31
import { goToDefinitionWithLocation, showGoToContextMenu } from 'vs/editor/contrib/inlayHints/browser/inlayHintsLocations';
32
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
33
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
34
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
35
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
36
import * as colors from 'vs/platform/theme/common/colorRegistry';
37
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
38
39
// --- hint caching service (per session)
40
41
class InlayHintsCache {
42
43
declare readonly _serviceBrand: undefined;
44
45
private readonly _entries = new LRUCache<string, InlayHintItem[]>(50);
46
47
get(model: ITextModel): InlayHintItem[] | undefined {
48
const key = InlayHintsCache._key(model);
49
return this._entries.get(key);
50
}
51
52
set(model: ITextModel, value: InlayHintItem[]): void {
53
const key = InlayHintsCache._key(model);
54
this._entries.set(key, value);
55
}
56
57
private static _key(model: ITextModel): string {
58
return `${model.uri.toString()}/${model.getVersionId()}`;
59
}
60
}
61
62
interface IInlayHintsCache extends InlayHintsCache { }
63
const IInlayHintsCache = createDecorator<IInlayHintsCache>('IInlayHintsCache');
64
registerSingleton(IInlayHintsCache, InlayHintsCache, InstantiationType.Delayed);
65
66
// --- rendered label
67
68
export class RenderedInlayHintLabelPart {
69
constructor(readonly item: InlayHintItem, readonly index: number) { }
70
71
get part() {
72
const label = this.item.hint.label;
73
if (typeof label === 'string') {
74
return { label };
75
} else {
76
return label[this.index];
77
}
78
}
79
}
80
81
class ActiveInlayHintInfo {
82
constructor(readonly part: RenderedInlayHintLabelPart, readonly hasTriggerModifier: boolean) { }
83
}
84
85
type InlayHintDecorationRenderInfo = {
86
item: InlayHintItem;
87
decoration: IModelDeltaDecoration;
88
classNameRef: ClassNameReference;
89
};
90
91
const enum RenderMode {
92
Normal,
93
Invisible
94
}
95
96
// --- controller
97
98
export class InlayHintsController implements IEditorContribution {
99
100
static readonly ID: string = 'editor.contrib.InlayHints';
101
102
private static readonly _MAX_DECORATORS = 1500;
103
private static readonly _MAX_LABEL_LEN = 43;
104
105
static get(editor: ICodeEditor): InlayHintsController | undefined {
106
return editor.getContribution<InlayHintsController>(InlayHintsController.ID) ?? undefined;
107
}
108
109
private readonly _disposables = new DisposableStore();
110
private readonly _sessionDisposables = new DisposableStore();
111
private readonly _debounceInfo: IFeatureDebounceInformation;
112
private readonly _decorationsMetadata = new Map<string, InlayHintDecorationRenderInfo>();
113
private readonly _ruleFactory = new DynamicCssRules(this._editor);
114
115
private _activeRenderMode = RenderMode.Normal;
116
private _activeInlayHintPart?: ActiveInlayHintInfo;
117
118
constructor(
119
private readonly _editor: ICodeEditor,
120
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
121
@ILanguageFeatureDebounceService _featureDebounce: ILanguageFeatureDebounceService,
122
@IInlayHintsCache private readonly _inlayHintsCache: IInlayHintsCache,
123
@ICommandService private readonly _commandService: ICommandService,
124
@INotificationService private readonly _notificationService: INotificationService,
125
@IInstantiationService private readonly _instaService: IInstantiationService,
126
) {
127
this._debounceInfo = _featureDebounce.for(_languageFeaturesService.inlayHintsProvider, 'InlayHint', { min: 25 });
128
this._disposables.add(_languageFeaturesService.inlayHintsProvider.onDidChange(() => this._update()));
129
this._disposables.add(_editor.onDidChangeModel(() => this._update()));
130
this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update()));
131
this._disposables.add(_editor.onDidChangeConfiguration(e => {
132
if (e.hasChanged(EditorOption.inlayHints)) {
133
this._update();
134
}
135
}));
136
this._update();
137
138
}
139
140
dispose(): void {
141
this._sessionDisposables.dispose();
142
this._removeAllDecorations();
143
this._disposables.dispose();
144
}
145
146
private _update(): void {
147
this._sessionDisposables.clear();
148
this._removeAllDecorations();
149
150
const options = this._editor.getOption(EditorOption.inlayHints);
151
if (options.enabled === 'off') {
152
return;
153
}
154
155
const model = this._editor.getModel();
156
if (!model || !this._languageFeaturesService.inlayHintsProvider.has(model)) {
157
return;
158
}
159
160
if (options.enabled === 'on') {
161
// different "on" modes: always
162
this._activeRenderMode = RenderMode.Normal;
163
} else {
164
// different "on" modes: offUnlessPressed, or onUnlessPressed
165
let defaultMode: RenderMode;
166
let altMode: RenderMode;
167
if (options.enabled === 'onUnlessPressed') {
168
defaultMode = RenderMode.Normal;
169
altMode = RenderMode.Invisible;
170
} else {
171
defaultMode = RenderMode.Invisible;
172
altMode = RenderMode.Normal;
173
}
174
this._activeRenderMode = defaultMode;
175
176
this._sessionDisposables.add(ModifierKeyEmitter.getInstance().event(e => {
177
if (!this._editor.hasModel()) {
178
return;
179
}
180
const newRenderMode = e.altKey && e.ctrlKey && !(e.shiftKey || e.metaKey) ? altMode : defaultMode;
181
if (newRenderMode !== this._activeRenderMode) {
182
this._activeRenderMode = newRenderMode;
183
const model = this._editor.getModel();
184
const copies = this._copyInlayHintsWithCurrentAnchor(model);
185
this._updateHintsDecorators([model.getFullModelRange()], copies);
186
scheduler.schedule(0);
187
}
188
}));
189
}
190
191
// iff possible, quickly update from cache
192
const cached = this._inlayHintsCache.get(model);
193
if (cached) {
194
this._updateHintsDecorators([model.getFullModelRange()], cached);
195
}
196
this._sessionDisposables.add(toDisposable(() => {
197
// cache items when switching files etc
198
if (!model.isDisposed()) {
199
this._cacheHintsForFastRestore(model);
200
}
201
}));
202
203
let cts: CancellationTokenSource | undefined;
204
const watchedProviders = new Set<languages.InlayHintsProvider>();
205
206
const scheduler = new RunOnceScheduler(async () => {
207
const t1 = Date.now();
208
209
cts?.dispose(true);
210
cts = new CancellationTokenSource();
211
const listener = model.onWillDispose(() => cts?.cancel());
212
213
try {
214
const myToken = cts.token;
215
const inlayHints = await InlayHintsFragments.create(this._languageFeaturesService.inlayHintsProvider, model, this._getHintsRanges(), myToken);
216
scheduler.delay = this._debounceInfo.update(model, Date.now() - t1);
217
if (myToken.isCancellationRequested) {
218
inlayHints.dispose();
219
return;
220
}
221
222
// listen to provider changes
223
for (const provider of inlayHints.provider) {
224
if (typeof provider.onDidChangeInlayHints === 'function' && !watchedProviders.has(provider)) {
225
watchedProviders.add(provider);
226
this._sessionDisposables.add(provider.onDidChangeInlayHints(() => {
227
if (!scheduler.isScheduled()) { // ignore event when request is already scheduled
228
scheduler.schedule();
229
}
230
}));
231
}
232
}
233
234
this._sessionDisposables.add(inlayHints);
235
this._updateHintsDecorators(inlayHints.ranges, inlayHints.items);
236
this._cacheHintsForFastRestore(model);
237
238
} catch (err) {
239
onUnexpectedError(err);
240
241
} finally {
242
cts.dispose();
243
listener.dispose();
244
}
245
246
}, this._debounceInfo.get(model));
247
248
this._sessionDisposables.add(scheduler);
249
this._sessionDisposables.add(toDisposable(() => cts?.dispose(true)));
250
scheduler.schedule(0);
251
252
this._sessionDisposables.add(this._editor.onDidScrollChange((e) => {
253
// update when scroll position changes
254
// uses scrollTopChanged has weak heuristic to differenatiate between scrolling due to
255
// typing or due to "actual" scrolling
256
if (e.scrollTopChanged || !scheduler.isScheduled()) {
257
scheduler.schedule();
258
}
259
}));
260
this._sessionDisposables.add(this._editor.onDidChangeModelContent((e) => {
261
cts?.cancel();
262
263
// update less aggressive when typing
264
const delay = Math.max(scheduler.delay, 1250);
265
scheduler.schedule(delay);
266
}));
267
268
// mouse gestures
269
this._sessionDisposables.add(this._installDblClickGesture(() => scheduler.schedule(0)));
270
this._sessionDisposables.add(this._installLinkGesture());
271
this._sessionDisposables.add(this._installContextMenu());
272
}
273
274
private _installLinkGesture(): IDisposable {
275
276
const store = new DisposableStore();
277
const gesture = store.add(new ClickLinkGesture(this._editor));
278
279
// let removeHighlight = () => { };
280
281
const sessionStore = new DisposableStore();
282
store.add(sessionStore);
283
284
store.add(gesture.onMouseMoveOrRelevantKeyDown(e => {
285
const [mouseEvent] = e;
286
const labelPart = this._getInlayHintLabelPart(mouseEvent);
287
const model = this._editor.getModel();
288
289
if (!labelPart || !model) {
290
sessionStore.clear();
291
return;
292
}
293
294
// resolve the item
295
const cts = new CancellationTokenSource();
296
sessionStore.add(toDisposable(() => cts.dispose(true)));
297
labelPart.item.resolve(cts.token);
298
299
// render link => when the modifier is pressed and when there is a command or location
300
this._activeInlayHintPart = labelPart.part.command || labelPart.part.location
301
? new ActiveInlayHintInfo(labelPart, mouseEvent.hasTriggerModifier)
302
: undefined;
303
304
const lineNumber = model.validatePosition(labelPart.item.hint.position).lineNumber;
305
const range = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber));
306
const lineHints = this._getInlineHintsForRange(range);
307
this._updateHintsDecorators([range], lineHints);
308
sessionStore.add(toDisposable(() => {
309
this._activeInlayHintPart = undefined;
310
this._updateHintsDecorators([range], lineHints);
311
}));
312
}));
313
store.add(gesture.onCancel(() => sessionStore.clear()));
314
store.add(gesture.onExecute(async e => {
315
const label = this._getInlayHintLabelPart(e);
316
if (label) {
317
const part = label.part;
318
if (part.location) {
319
// location -> execute go to def
320
this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor as IActiveCodeEditor, part.location);
321
} else if (languages.Command.is(part.command)) {
322
// command -> execute it
323
await this._invokeCommand(part.command, label.item);
324
}
325
}
326
}));
327
return store;
328
}
329
330
private _getInlineHintsForRange(range: Range) {
331
const lineHints = new Set<InlayHintItem>();
332
for (const data of this._decorationsMetadata.values()) {
333
if (range.containsRange(data.item.anchor.range)) {
334
lineHints.add(data.item);
335
}
336
}
337
return Array.from(lineHints);
338
}
339
340
private _installDblClickGesture(updateInlayHints: Function): IDisposable {
341
return this._editor.onMouseUp(async e => {
342
if (e.event.detail !== 2) {
343
return;
344
}
345
const part = this._getInlayHintLabelPart(e);
346
if (!part) {
347
return;
348
}
349
e.event.preventDefault();
350
await part.item.resolve(CancellationToken.None);
351
if (isNonEmptyArray(part.item.hint.textEdits)) {
352
const edits = part.item.hint.textEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text));
353
this._editor.executeEdits('inlayHint.default', edits);
354
updateInlayHints();
355
}
356
});
357
}
358
359
private _installContextMenu(): IDisposable {
360
return this._editor.onContextMenu(async e => {
361
if (!(e.event.target instanceof HTMLElement)) {
362
return;
363
}
364
const part = this._getInlayHintLabelPart(e);
365
if (part) {
366
await this._instaService.invokeFunction(showGoToContextMenu, this._editor, e.event.target, part);
367
}
368
});
369
}
370
371
private _getInlayHintLabelPart(e: IEditorMouseEvent | ClickLinkMouseEvent): RenderedInlayHintLabelPart | undefined {
372
if (e.target.type !== MouseTargetType.CONTENT_TEXT) {
373
return undefined;
374
}
375
const options = e.target.detail.injectedText?.options;
376
if (options instanceof ModelDecorationInjectedTextOptions && options?.attachedData instanceof RenderedInlayHintLabelPart) {
377
return options.attachedData;
378
}
379
return undefined;
380
}
381
382
private async _invokeCommand(command: languages.Command, item: InlayHintItem) {
383
try {
384
await this._commandService.executeCommand(command.id, ...(command.arguments ?? []));
385
} catch (err) {
386
this._notificationService.notify({
387
severity: Severity.Error,
388
source: item.provider.displayName,
389
message: err
390
});
391
}
392
}
393
394
private _cacheHintsForFastRestore(model: ITextModel): void {
395
const hints = this._copyInlayHintsWithCurrentAnchor(model);
396
this._inlayHintsCache.set(model, hints);
397
}
398
399
// return inlay hints but with an anchor that reflects "updates"
400
// that happened after receiving them, e.g adding new lines before a hint
401
private _copyInlayHintsWithCurrentAnchor(model: ITextModel): InlayHintItem[] {
402
const items = new Map<InlayHintItem, InlayHintItem>();
403
for (const [id, obj] of this._decorationsMetadata) {
404
if (items.has(obj.item)) {
405
// an inlay item can be rendered as multiple decorations
406
// but they will all uses the same range
407
continue;
408
}
409
const range = model.getDecorationRange(id);
410
if (range) {
411
// update range with whatever the editor has tweaked it to
412
const anchor = new InlayHintAnchor(range, obj.item.anchor.direction);
413
const copy = obj.item.with({ anchor });
414
items.set(obj.item, copy);
415
}
416
}
417
return Array.from(items.values());
418
}
419
420
private _getHintsRanges(): Range[] {
421
const extra = 30;
422
const model = this._editor.getModel()!;
423
const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow();
424
const result: Range[] = [];
425
for (const range of visibleRanges.sort(Range.compareRangesUsingStarts)) {
426
const extendedRange = model.validateRange(new Range(range.startLineNumber - extra, range.startColumn, range.endLineNumber + extra, range.endColumn));
427
if (result.length === 0 || !Range.areIntersectingOrTouching(result[result.length - 1], extendedRange)) {
428
result.push(extendedRange);
429
} else {
430
result[result.length - 1] = Range.plusRange(result[result.length - 1], extendedRange);
431
}
432
}
433
return result;
434
}
435
436
private _updateHintsDecorators(ranges: readonly Range[], items: readonly InlayHintItem[]): void {
437
438
// utils to collect/create injected text decorations
439
const newDecorationsData: InlayHintDecorationRenderInfo[] = [];
440
const addInjectedText = (item: InlayHintItem, ref: ClassNameReference, content: string, cursorStops: InjectedTextCursorStops, attachedData?: RenderedInlayHintLabelPart): void => {
441
const opts: InjectedTextOptions = {
442
content,
443
inlineClassNameAffectsLetterSpacing: true,
444
inlineClassName: ref.className,
445
cursorStops,
446
attachedData
447
};
448
newDecorationsData.push({
449
item,
450
classNameRef: ref,
451
decoration: {
452
range: item.anchor.range,
453
options: {
454
// className: "rangeHighlight", // DEBUG highlight to see to what range a hint is attached
455
description: 'InlayHint',
456
showIfCollapsed: item.anchor.range.isEmpty(), // "original" range is empty
457
collapseOnReplaceEdit: !item.anchor.range.isEmpty(),
458
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
459
[item.anchor.direction]: this._activeRenderMode === RenderMode.Normal ? opts : undefined
460
}
461
}
462
});
463
};
464
465
const addInjectedWhitespace = (item: InlayHintItem, isLast: boolean): void => {
466
const marginRule = this._ruleFactory.createClassNameRef({
467
width: `${(fontSize / 3) | 0}px`,
468
display: 'inline-block'
469
});
470
addInjectedText(item, marginRule, '\u200a', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None);
471
};
472
473
474
//
475
const { fontSize, fontFamily, padding, isUniform } = this._getLayoutInfo();
476
const fontFamilyVar = '--code-editorInlayHintsFontFamily';
477
this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily);
478
479
480
type ILineInfo = { line: number; totalLen: number };
481
let currentLineInfo: ILineInfo = { line: 0, totalLen: 0 };
482
483
for (const item of items) {
484
485
if (currentLineInfo.line !== item.anchor.range.startLineNumber) {
486
currentLineInfo = { line: item.anchor.range.startLineNumber, totalLen: 0 };
487
}
488
489
if (currentLineInfo.totalLen > InlayHintsController._MAX_LABEL_LEN) {
490
continue;
491
}
492
493
// whitespace leading the actual label
494
if (item.hint.paddingLeft) {
495
addInjectedWhitespace(item, false);
496
}
497
498
// the label with its parts
499
const parts: languages.InlayHintLabelPart[] = typeof item.hint.label === 'string'
500
? [{ label: item.hint.label }]
501
: item.hint.label;
502
503
for (let i = 0; i < parts.length; i++) {
504
const part = parts[i];
505
506
const isFirst = i === 0;
507
const isLast = i === parts.length - 1;
508
509
const cssProperties: CssProperties = {
510
fontSize: `${fontSize}px`,
511
fontFamily: `var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}`,
512
verticalAlign: isUniform ? 'baseline' : 'middle',
513
unicodeBidi: 'isolate'
514
};
515
516
if (isNonEmptyArray(item.hint.textEdits)) {
517
cssProperties.cursor = 'default';
518
}
519
520
this._fillInColors(cssProperties, item.hint);
521
522
if ((part.command || part.location) && this._activeInlayHintPart?.part.item === item && this._activeInlayHintPart.part.index === i) {
523
// active link!
524
cssProperties.textDecoration = 'underline';
525
if (this._activeInlayHintPart.hasTriggerModifier) {
526
cssProperties.color = themeColorFromId(colors.editorActiveLinkForeground);
527
cssProperties.cursor = 'pointer';
528
}
529
}
530
531
if (padding) {
532
if (isFirst && isLast) {
533
// only element
534
cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px`;
535
cssProperties.borderRadius = `${(fontSize / 4) | 0}px`;
536
} else if (isFirst) {
537
// first element
538
cssProperties.padding = `1px 0 1px ${Math.max(1, fontSize / 4) | 0}px`;
539
cssProperties.borderRadius = `${(fontSize / 4) | 0}px 0 0 ${(fontSize / 4) | 0}px`;
540
} else if (isLast) {
541
// last element
542
cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px 1px 0`;
543
cssProperties.borderRadius = `0 ${(fontSize / 4) | 0}px ${(fontSize / 4) | 0}px 0`;
544
} else {
545
cssProperties.padding = `1px 0 1px 0`;
546
}
547
}
548
549
let textlabel = part.label;
550
currentLineInfo.totalLen += textlabel.length;
551
let tooLong = false;
552
const over = currentLineInfo.totalLen - InlayHintsController._MAX_LABEL_LEN;
553
if (over > 0) {
554
textlabel = textlabel.slice(0, -over) + '…';
555
tooLong = true;
556
}
557
558
addInjectedText(
559
item,
560
this._ruleFactory.createClassNameRef(cssProperties),
561
fixSpace(textlabel),
562
isLast && !item.hint.paddingRight ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None,
563
new RenderedInlayHintLabelPart(item, i)
564
);
565
566
if (tooLong) {
567
break;
568
}
569
}
570
571
// whitespace trailing the actual label
572
if (item.hint.paddingRight) {
573
addInjectedWhitespace(item, true);
574
}
575
576
if (newDecorationsData.length > InlayHintsController._MAX_DECORATORS) {
577
break;
578
}
579
}
580
581
// collect all decoration ids that are affected by the ranges
582
// and only update those decorations
583
const decorationIdsToReplace: string[] = [];
584
for (const [id, metadata] of this._decorationsMetadata) {
585
const range = this._editor.getModel()?.getDecorationRange(id);
586
if (range && ranges.some(r => r.containsRange(range))) {
587
decorationIdsToReplace.push(id);
588
metadata.classNameRef.dispose();
589
this._decorationsMetadata.delete(id);
590
}
591
}
592
593
const scrollState = StableEditorScrollState.capture(this._editor);
594
595
this._editor.changeDecorations(accessor => {
596
const newDecorationIds = accessor.deltaDecorations(decorationIdsToReplace, newDecorationsData.map(d => d.decoration));
597
for (let i = 0; i < newDecorationIds.length; i++) {
598
const data = newDecorationsData[i];
599
this._decorationsMetadata.set(newDecorationIds[i], data);
600
}
601
});
602
603
scrollState.restore(this._editor);
604
}
605
606
private _fillInColors(props: CssProperties, hint: languages.InlayHint): void {
607
if (hint.kind === languages.InlayHintKind.Parameter) {
608
props.backgroundColor = themeColorFromId(colors.editorInlayHintParameterBackground);
609
props.color = themeColorFromId(colors.editorInlayHintParameterForeground);
610
} else if (hint.kind === languages.InlayHintKind.Type) {
611
props.backgroundColor = themeColorFromId(colors.editorInlayHintTypeBackground);
612
props.color = themeColorFromId(colors.editorInlayHintTypeForeground);
613
} else {
614
props.backgroundColor = themeColorFromId(colors.editorInlayHintBackground);
615
props.color = themeColorFromId(colors.editorInlayHintForeground);
616
}
617
}
618
619
private _getLayoutInfo() {
620
const options = this._editor.getOption(EditorOption.inlayHints);
621
const padding = options.padding;
622
623
const editorFontSize = this._editor.getOption(EditorOption.fontSize);
624
const editorFontFamily = this._editor.getOption(EditorOption.fontFamily);
625
626
let fontSize = options.fontSize;
627
if (!fontSize || fontSize < 5 || fontSize > editorFontSize) {
628
fontSize = editorFontSize;
629
}
630
631
const fontFamily = options.fontFamily || editorFontFamily;
632
633
const isUniform = !padding
634
&& fontFamily === editorFontFamily
635
&& fontSize === editorFontSize;
636
637
return { fontSize, fontFamily, padding, isUniform };
638
}
639
640
private _removeAllDecorations(): void {
641
this._editor.removeDecorations(Array.from(this._decorationsMetadata.keys()));
642
for (const obj of this._decorationsMetadata.values()) {
643
obj.classNameRef.dispose();
644
}
645
this._decorationsMetadata.clear();
646
}
647
648
649
// --- accessibility
650
651
getInlayHintsForLine(line: number): InlayHintItem[] {
652
if (!this._editor.hasModel()) {
653
return [];
654
}
655
const set = new Set<languages.InlayHint>();
656
const result: InlayHintItem[] = [];
657
for (const deco of this._editor.getLineDecorations(line)) {
658
const data = this._decorationsMetadata.get(deco.id);
659
if (data && !set.has(data.item.hint)) {
660
set.add(data.item.hint);
661
result.push(data.item);
662
}
663
}
664
return result;
665
}
666
}
667
668
669
// Prevents the view from potentially visible whitespace
670
function fixSpace(str: string): string {
671
const noBreakWhitespace = '\xa0';
672
return str.replace(/[ \t]/g, noBreakWhitespace);
673
}
674
675
CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, ...args: [URI, IRange]): Promise<languages.InlayHint[]> => {
676
677
const [uri, range] = args;
678
assertType(URI.isUri(uri));
679
assertType(Range.isIRange(range));
680
681
const { inlayHintsProvider } = accessor.get(ILanguageFeaturesService);
682
const ref = await accessor.get(ITextModelService).createModelReference(uri);
683
try {
684
const model = await InlayHintsFragments.create(inlayHintsProvider, ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None);
685
const result = model.items.map(i => i.hint);
686
setTimeout(() => model.dispose(), 0); // dispose after sending to ext host
687
return result;
688
} finally {
689
ref.dispose();
690
}
691
});
692
693