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