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