Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts
5283 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 { CancellationToken } from '../../../../base/common/cancellation.js';
7
import { CancellationError, onUnexpectedExternalError } from '../../../../base/common/errors.js';
8
import { DisposableStore } from '../../../../base/common/lifecycle.js';
9
import { IPosition, Position } from '../../../common/core/position.js';
10
import { Range } from '../../../common/core/range.js';
11
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
12
import { InlayHint, InlayHintList, InlayHintsProvider, Command } from '../../../common/languages.js';
13
import { ITextModel } from '../../../common/model.js';
14
import { createCommandUri } from '../../../../base/common/htmlContent.js';
15
16
export class InlayHintAnchor {
17
constructor(readonly range: Range, readonly direction: 'before' | 'after') { }
18
}
19
20
export class InlayHintItem {
21
22
private _isResolved: boolean = false;
23
private _currentResolve?: Promise<void>;
24
25
constructor(readonly hint: InlayHint, readonly anchor: InlayHintAnchor, readonly provider: InlayHintsProvider) { }
26
27
with(delta: { anchor: InlayHintAnchor }): InlayHintItem {
28
const result = new InlayHintItem(this.hint, delta.anchor, this.provider);
29
result._isResolved = this._isResolved;
30
result._currentResolve = this._currentResolve;
31
return result;
32
}
33
34
async resolve(token: CancellationToken): Promise<void> {
35
if (typeof this.provider.resolveInlayHint !== 'function') {
36
return;
37
}
38
if (this._currentResolve) {
39
// wait for an active resolve operation and try again
40
// when that's done.
41
await this._currentResolve;
42
if (token.isCancellationRequested) {
43
return;
44
}
45
return this.resolve(token);
46
}
47
if (!this._isResolved) {
48
this._currentResolve = this._doResolve(token)
49
.finally(() => this._currentResolve = undefined);
50
}
51
await this._currentResolve;
52
}
53
54
private async _doResolve(token: CancellationToken) {
55
try {
56
const newHint = await Promise.resolve(this.provider.resolveInlayHint!(this.hint, token));
57
this.hint.tooltip = newHint?.tooltip ?? this.hint.tooltip;
58
this.hint.label = newHint?.label ?? this.hint.label;
59
this.hint.textEdits = newHint?.textEdits ?? this.hint.textEdits;
60
this._isResolved = true;
61
} catch (err) {
62
onUnexpectedExternalError(err);
63
this._isResolved = false;
64
}
65
}
66
}
67
68
export class InlayHintsFragments {
69
70
private static _emptyInlayHintList: InlayHintList = Object.freeze({ dispose() { }, hints: [] });
71
72
static async create(registry: LanguageFeatureRegistry<InlayHintsProvider>, model: ITextModel, ranges: Range[], token: CancellationToken): Promise<InlayHintsFragments> {
73
74
const data: [InlayHintList, InlayHintsProvider][] = [];
75
76
const promises = registry.ordered(model).reverse().map(provider => ranges.map(async range => {
77
try {
78
const result = await provider.provideInlayHints(model, range, token);
79
if (result?.hints.length || provider.onDidChangeInlayHints) {
80
data.push([result ?? InlayHintsFragments._emptyInlayHintList, provider]);
81
}
82
} catch (err) {
83
onUnexpectedExternalError(err);
84
}
85
}));
86
87
await Promise.all(promises.flat());
88
89
if (token.isCancellationRequested || model.isDisposed()) {
90
throw new CancellationError();
91
}
92
93
return new InlayHintsFragments(ranges, data, model);
94
}
95
96
private readonly _disposables = new DisposableStore();
97
98
readonly items: readonly InlayHintItem[];
99
readonly ranges: readonly Range[];
100
readonly provider: Set<InlayHintsProvider>;
101
102
private constructor(ranges: Range[], data: [InlayHintList, InlayHintsProvider][], model: ITextModel) {
103
this.ranges = ranges;
104
this.provider = new Set();
105
const items: InlayHintItem[] = [];
106
for (const [list, provider] of data) {
107
this._disposables.add(list);
108
this.provider.add(provider);
109
110
for (const hint of list.hints) {
111
// compute the range to which the item should be attached to
112
const position = model.validatePosition(hint.position);
113
let direction: 'before' | 'after' = 'before';
114
115
const wordRange = InlayHintsFragments._getRangeAtPosition(model, position);
116
let range: Range;
117
118
if (wordRange.getStartPosition().isBefore(position)) {
119
range = Range.fromPositions(wordRange.getStartPosition(), position);
120
direction = 'after';
121
} else {
122
range = Range.fromPositions(position, wordRange.getEndPosition());
123
direction = 'before';
124
}
125
126
items.push(new InlayHintItem(hint, new InlayHintAnchor(range, direction), provider));
127
}
128
}
129
this.items = items.sort((a, b) => Position.compare(a.hint.position, b.hint.position));
130
}
131
132
dispose(): void {
133
this._disposables.dispose();
134
}
135
136
private static _getRangeAtPosition(model: ITextModel, position: IPosition): Range {
137
const line = position.lineNumber;
138
const word = model.getWordAtPosition(position);
139
if (word) {
140
// always prefer the word range
141
return new Range(line, word.startColumn, line, word.endColumn);
142
}
143
144
model.tokenization.tokenizeIfCheap(line);
145
const tokens = model.tokenization.getLineTokens(line);
146
const offset = position.column - 1;
147
const idx = tokens.findTokenIndexAtOffset(offset);
148
149
let start = tokens.getStartOffset(idx);
150
let end = tokens.getEndOffset(idx);
151
152
if (end - start === 1) {
153
// single character token, when at its end try leading/trailing token instead
154
if (start === offset && idx > 1) {
155
// leading token
156
start = tokens.getStartOffset(idx - 1);
157
end = tokens.getEndOffset(idx - 1);
158
} else if (end === offset && idx < tokens.getCount() - 1) {
159
// trailing token
160
start = tokens.getStartOffset(idx + 1);
161
end = tokens.getEndOffset(idx + 1);
162
}
163
}
164
165
return new Range(line, start + 1, line, end + 1);
166
}
167
}
168
169
export function asCommandLink(command: Command): string {
170
return createCommandUri(command.id, ...(command.arguments ?? [])).toString();
171
}
172
173