Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/codelens/browser/codelensController.ts
5251 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
7
import { CancelablePromise, createCancelablePromise, disposableTimeout, RunOnceScheduler } from '../../../../base/common/async.js';
8
import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js';
9
import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
10
import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js';
11
import { IActiveCodeEditor, ICodeEditor, IViewZoneChangeAccessor, MouseTargetType } from '../../../browser/editorBrowser.js';
12
import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';
13
import { EditorOption } from '../../../common/config/editorOptions.js';
14
import { EDITOR_FONT_DEFAULTS } from '../../../common/config/fontInfo.js';
15
import { IEditorContribution } from '../../../common/editorCommon.js';
16
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
17
import { IModelDecorationsChangeAccessor } from '../../../common/model.js';
18
import { CodeLens, Command } from '../../../common/languages.js';
19
import { CodeLensItem, CodeLensModel, getCodeLensModel } from './codelens.js';
20
import { ICodeLensCache } from './codeLensCache.js';
21
import { CodeLensHelper, CodeLensWidget } from './codelensWidget.js';
22
import { localize, localize2 } from '../../../../nls.js';
23
import { ICommandService } from '../../../../platform/commands/common/commands.js';
24
import { INotificationService } from '../../../../platform/notification/common/notification.js';
25
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
26
import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';
27
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
28
29
export class CodeLensContribution implements IEditorContribution {
30
31
static readonly ID: string = 'css.editor.codeLens';
32
33
private readonly _disposables = new DisposableStore();
34
private readonly _localToDispose = new DisposableStore();
35
36
private readonly _lenses: CodeLensWidget[] = [];
37
38
private readonly _provideCodeLensDebounce: IFeatureDebounceInformation;
39
private readonly _resolveCodeLensesDebounce: IFeatureDebounceInformation;
40
private readonly _resolveCodeLensesScheduler: RunOnceScheduler;
41
42
private _getCodeLensModelPromise: CancelablePromise<CodeLensModel> | undefined;
43
private readonly _oldCodeLensModels = new DisposableStore();
44
private _currentCodeLensModel: CodeLensModel | undefined;
45
private _resolveCodeLensesPromise: CancelablePromise<void[]> | undefined;
46
47
constructor(
48
private readonly _editor: ICodeEditor,
49
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
50
@ILanguageFeatureDebounceService debounceService: ILanguageFeatureDebounceService,
51
@ICommandService private readonly _commandService: ICommandService,
52
@INotificationService private readonly _notificationService: INotificationService,
53
@ICodeLensCache private readonly _codeLensCache: ICodeLensCache
54
) {
55
this._provideCodeLensDebounce = debounceService.for(_languageFeaturesService.codeLensProvider, 'CodeLensProvide', { min: 250 });
56
this._resolveCodeLensesDebounce = debounceService.for(_languageFeaturesService.codeLensProvider, 'CodeLensResolve', { min: 250, salt: 'resolve' });
57
this._resolveCodeLensesScheduler = new RunOnceScheduler(() => this._resolveCodeLensesInViewport(), this._resolveCodeLensesDebounce.default());
58
59
this._disposables.add(this._editor.onDidChangeModel(() => this._onModelChange()));
60
this._disposables.add(this._editor.onDidChangeModelLanguage(() => this._onModelChange()));
61
this._disposables.add(this._editor.onDidChangeConfiguration((e) => {
62
if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.codeLensFontSize) || e.hasChanged(EditorOption.codeLensFontFamily)) {
63
this._updateLensStyle();
64
}
65
if (e.hasChanged(EditorOption.codeLens)) {
66
this._onModelChange();
67
}
68
}));
69
this._disposables.add(_languageFeaturesService.codeLensProvider.onDidChange(this._onModelChange, this));
70
this._onModelChange();
71
72
this._updateLensStyle();
73
}
74
75
dispose(): void {
76
this._localDispose();
77
this._localToDispose.dispose();
78
this._disposables.dispose();
79
this._resolveCodeLensesScheduler.dispose();
80
this._oldCodeLensModels.dispose();
81
this._currentCodeLensModel?.dispose();
82
}
83
84
private _getLayoutInfo() {
85
const lineHeightFactor = Math.max(1.3, this._editor.getOption(EditorOption.lineHeight) / this._editor.getOption(EditorOption.fontSize));
86
let fontSize = this._editor.getOption(EditorOption.codeLensFontSize);
87
if (!fontSize || fontSize < 5) {
88
fontSize = (this._editor.getOption(EditorOption.fontSize) * .9) | 0;
89
}
90
return {
91
fontSize,
92
codeLensHeight: (fontSize * lineHeightFactor) | 0,
93
};
94
}
95
96
private _updateLensStyle(): void {
97
98
const { codeLensHeight, fontSize } = this._getLayoutInfo();
99
const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily);
100
const editorFontInfo = this._editor.getOption(EditorOption.fontInfo);
101
102
const { style } = this._editor.getContainerDomNode();
103
104
style.setProperty('--vscode-editorCodeLens-lineHeight', `${codeLensHeight}px`);
105
style.setProperty('--vscode-editorCodeLens-fontSize', `${fontSize}px`);
106
style.setProperty('--vscode-editorCodeLens-fontFeatureSettings', editorFontInfo.fontFeatureSettings);
107
108
if (fontFamily) {
109
style.setProperty('--vscode-editorCodeLens-fontFamily', fontFamily);
110
style.setProperty('--vscode-editorCodeLens-fontFamilyDefault', EDITOR_FONT_DEFAULTS.fontFamily);
111
}
112
113
//
114
this._editor.changeViewZones(accessor => {
115
for (const lens of this._lenses) {
116
lens.updateHeight(codeLensHeight, accessor);
117
}
118
});
119
}
120
121
private _localDispose(): void {
122
this._getCodeLensModelPromise?.cancel();
123
this._getCodeLensModelPromise = undefined;
124
this._resolveCodeLensesPromise?.cancel();
125
this._resolveCodeLensesPromise = undefined;
126
this._localToDispose.clear();
127
this._oldCodeLensModels.clear();
128
this._currentCodeLensModel?.dispose();
129
}
130
131
private _onModelChange(): void {
132
133
this._localDispose();
134
135
const model = this._editor.getModel();
136
if (!model) {
137
return;
138
}
139
140
if (!this._editor.getOption(EditorOption.codeLens) || model.isTooLargeForTokenization()) {
141
return;
142
}
143
144
const cachedLenses = this._codeLensCache.get(model);
145
if (cachedLenses) {
146
this._renderCodeLensSymbols(cachedLenses);
147
}
148
149
if (!this._languageFeaturesService.codeLensProvider.has(model)) {
150
// no provider -> return but check with
151
// cached lenses. they expire after 30 seconds
152
if (cachedLenses) {
153
disposableTimeout(() => {
154
const cachedLensesNow = this._codeLensCache.get(model);
155
if (cachedLenses === cachedLensesNow) {
156
this._codeLensCache.delete(model);
157
this._onModelChange();
158
}
159
}, 30 * 1000, this._localToDispose);
160
}
161
return;
162
}
163
164
for (const provider of this._languageFeaturesService.codeLensProvider.all(model)) {
165
if (typeof provider.onDidChange === 'function') {
166
const registration = provider.onDidChange(() => scheduler.schedule());
167
this._localToDispose.add(registration);
168
}
169
}
170
171
const scheduler = new RunOnceScheduler(() => {
172
const t1 = Date.now();
173
174
this._getCodeLensModelPromise?.cancel();
175
this._getCodeLensModelPromise = createCancelablePromise(token => getCodeLensModel(this._languageFeaturesService.codeLensProvider, model, token));
176
177
this._getCodeLensModelPromise.then(result => {
178
if (this._currentCodeLensModel) {
179
this._oldCodeLensModels.add(this._currentCodeLensModel);
180
}
181
this._currentCodeLensModel = result;
182
183
// cache model to reduce flicker
184
this._codeLensCache.put(model, result);
185
186
// update moving average
187
const newDelay = this._provideCodeLensDebounce.update(model, Date.now() - t1);
188
scheduler.delay = newDelay;
189
190
// render lenses
191
this._renderCodeLensSymbols(result);
192
// dom.scheduleAtNextAnimationFrame(() => this._resolveCodeLensesInViewport());
193
this._resolveCodeLensesInViewportSoon();
194
}, onUnexpectedError);
195
196
}, this._provideCodeLensDebounce.get(model));
197
198
this._localToDispose.add(scheduler);
199
this._localToDispose.add(toDisposable(() => this._resolveCodeLensesScheduler.cancel()));
200
this._localToDispose.add(this._editor.onDidChangeModelContent(() => {
201
this._editor.changeDecorations(decorationsAccessor => {
202
this._editor.changeViewZones(viewZonesAccessor => {
203
const toDispose: CodeLensWidget[] = [];
204
let lastLensLineNumber: number = -1;
205
206
this._lenses.forEach((lens) => {
207
if (!lens.isValid() || lastLensLineNumber === lens.getLineNumber()) {
208
// invalid -> lens collapsed, attach range doesn't exist anymore
209
// line_number -> lenses should never be on the same line
210
toDispose.push(lens);
211
212
} else {
213
lens.update(viewZonesAccessor);
214
lastLensLineNumber = lens.getLineNumber();
215
}
216
});
217
218
const helper = new CodeLensHelper();
219
toDispose.forEach((l) => {
220
l.dispose(helper, viewZonesAccessor);
221
this._lenses.splice(this._lenses.indexOf(l), 1);
222
});
223
helper.commit(decorationsAccessor);
224
});
225
});
226
227
// Ask for all references again
228
scheduler.schedule();
229
230
// Cancel pending and active resolve requests
231
this._resolveCodeLensesScheduler.cancel();
232
this._resolveCodeLensesPromise?.cancel();
233
this._resolveCodeLensesPromise = undefined;
234
}));
235
this._localToDispose.add(this._editor.onDidFocusEditorText(() => {
236
scheduler.schedule();
237
}));
238
this._localToDispose.add(this._editor.onDidBlurEditorText(() => {
239
scheduler.cancel();
240
}));
241
this._localToDispose.add(this._editor.onDidScrollChange(e => {
242
if (e.scrollTopChanged && this._lenses.length > 0) {
243
this._resolveCodeLensesInViewportSoon();
244
}
245
}));
246
this._localToDispose.add(this._editor.onDidLayoutChange(() => {
247
this._resolveCodeLensesInViewportSoon();
248
}));
249
this._localToDispose.add(toDisposable(() => {
250
if (this._editor.getModel()) {
251
const scrollState = StableEditorScrollState.capture(this._editor);
252
this._editor.changeDecorations(decorationsAccessor => {
253
this._editor.changeViewZones(viewZonesAccessor => {
254
this._disposeAllLenses(decorationsAccessor, viewZonesAccessor);
255
});
256
});
257
scrollState.restore(this._editor);
258
} else {
259
// No accessors available
260
this._disposeAllLenses(undefined, undefined);
261
}
262
}));
263
this._localToDispose.add(this._editor.onMouseDown(e => {
264
if (e.target.type !== MouseTargetType.CONTENT_WIDGET) {
265
return;
266
}
267
let target = e.target.element;
268
if (target?.tagName === 'SPAN') {
269
target = target.parentElement;
270
}
271
if (target?.tagName === 'A') {
272
for (const lens of this._lenses) {
273
const command = lens.getCommand(target as HTMLLinkElement);
274
if (command) {
275
this._commandService.executeCommand(command.id, ...(command.arguments || [])).catch(err => this._notificationService.error(err));
276
break;
277
}
278
}
279
}
280
}));
281
scheduler.schedule();
282
}
283
284
private _disposeAllLenses(decChangeAccessor: IModelDecorationsChangeAccessor | undefined, viewZoneChangeAccessor: IViewZoneChangeAccessor | undefined): void {
285
const helper = new CodeLensHelper();
286
for (const lens of this._lenses) {
287
lens.dispose(helper, viewZoneChangeAccessor);
288
}
289
if (decChangeAccessor) {
290
helper.commit(decChangeAccessor);
291
}
292
this._lenses.length = 0;
293
}
294
295
private _renderCodeLensSymbols(symbols: CodeLensModel): void {
296
if (!this._editor.hasModel()) {
297
return;
298
}
299
300
const maxLineNumber = this._editor.getModel().getLineCount();
301
const groups: CodeLensItem[][] = [];
302
let lastGroup: CodeLensItem[] | undefined;
303
304
for (const symbol of symbols.lenses) {
305
const line = symbol.symbol.range.startLineNumber;
306
if (line < 1 || line > maxLineNumber) {
307
// invalid code lens
308
continue;
309
} else if (lastGroup && lastGroup[lastGroup.length - 1].symbol.range.startLineNumber === line) {
310
// on same line as previous
311
lastGroup.push(symbol);
312
} else {
313
// on later line as previous
314
lastGroup = [symbol];
315
groups.push(lastGroup);
316
}
317
}
318
319
if (!groups.length && !this._lenses.length) {
320
// Nothing to change
321
return;
322
}
323
324
const scrollState = StableEditorScrollState.capture(this._editor);
325
const layoutInfo = this._getLayoutInfo();
326
327
this._editor.changeDecorations(decorationsAccessor => {
328
this._editor.changeViewZones(viewZoneAccessor => {
329
330
const helper = new CodeLensHelper();
331
let codeLensIndex = 0;
332
let groupsIndex = 0;
333
334
while (groupsIndex < groups.length && codeLensIndex < this._lenses.length) {
335
336
const symbolsLineNumber = groups[groupsIndex][0].symbol.range.startLineNumber;
337
const codeLensLineNumber = this._lenses[codeLensIndex].getLineNumber();
338
339
if (codeLensLineNumber < symbolsLineNumber) {
340
this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor);
341
this._lenses.splice(codeLensIndex, 1);
342
} else if (codeLensLineNumber === symbolsLineNumber) {
343
this._lenses[codeLensIndex].updateCodeLensSymbols(groups[groupsIndex], helper);
344
groupsIndex++;
345
codeLensIndex++;
346
} else {
347
this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], <IActiveCodeEditor>this._editor, helper, viewZoneAccessor, layoutInfo.codeLensHeight, () => this._resolveCodeLensesInViewportSoon()));
348
codeLensIndex++;
349
groupsIndex++;
350
}
351
}
352
353
// Delete extra code lenses
354
while (codeLensIndex < this._lenses.length) {
355
this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor);
356
this._lenses.splice(codeLensIndex, 1);
357
}
358
359
// Create extra symbols
360
while (groupsIndex < groups.length) {
361
this._lenses.push(new CodeLensWidget(groups[groupsIndex], <IActiveCodeEditor>this._editor, helper, viewZoneAccessor, layoutInfo.codeLensHeight, () => this._resolveCodeLensesInViewportSoon()));
362
groupsIndex++;
363
}
364
365
helper.commit(decorationsAccessor);
366
});
367
});
368
369
scrollState.restore(this._editor);
370
}
371
372
private _resolveCodeLensesInViewportSoon(): void {
373
const model = this._editor.getModel();
374
if (model) {
375
this._resolveCodeLensesScheduler.schedule();
376
}
377
}
378
379
private _resolveCodeLensesInViewport(): void {
380
381
this._resolveCodeLensesPromise?.cancel();
382
this._resolveCodeLensesPromise = undefined;
383
384
const model = this._editor.getModel();
385
if (!model) {
386
return;
387
}
388
389
const toResolve: Array<ReadonlyArray<CodeLensItem>> = [];
390
const lenses: CodeLensWidget[] = [];
391
this._lenses.forEach((lens) => {
392
const request = lens.computeIfNecessary(model);
393
if (request) {
394
toResolve.push(request);
395
lenses.push(lens);
396
}
397
});
398
399
if (toResolve.length === 0) {
400
this._oldCodeLensModels.clear();
401
return;
402
}
403
404
const t1 = Date.now();
405
406
const resolvePromise = createCancelablePromise(token => {
407
408
const promises = toResolve.map((request, i) => {
409
410
const resolvedSymbols = new Array<CodeLens | undefined | null>(request.length);
411
const promises = request.map((request, i) => {
412
if (!request.symbol.command && typeof request.provider.resolveCodeLens === 'function') {
413
return Promise.resolve(request.provider.resolveCodeLens(model, request.symbol, token)).then(symbol => {
414
resolvedSymbols[i] = symbol;
415
}, onUnexpectedExternalError);
416
} else {
417
resolvedSymbols[i] = request.symbol;
418
return Promise.resolve(undefined);
419
}
420
});
421
422
return Promise.all(promises).then(() => {
423
if (!token.isCancellationRequested && !lenses[i].isDisposed()) {
424
lenses[i].updateCommands(resolvedSymbols);
425
}
426
});
427
});
428
429
return Promise.all(promises);
430
});
431
this._resolveCodeLensesPromise = resolvePromise;
432
433
this._resolveCodeLensesPromise.then(() => {
434
435
// update moving average
436
const newDelay = this._resolveCodeLensesDebounce.update(model, Date.now() - t1);
437
this._resolveCodeLensesScheduler.delay = newDelay;
438
439
if (this._currentCodeLensModel) { // update the cached state with new resolved items
440
this._codeLensCache.put(model, this._currentCodeLensModel);
441
}
442
this._oldCodeLensModels.clear(); // dispose old models once we have updated the UI with the current model
443
if (resolvePromise === this._resolveCodeLensesPromise) {
444
this._resolveCodeLensesPromise = undefined;
445
}
446
}, err => {
447
onUnexpectedError(err); // can also be cancellation!
448
if (resolvePromise === this._resolveCodeLensesPromise) {
449
this._resolveCodeLensesPromise = undefined;
450
}
451
});
452
}
453
454
async getModel(): Promise<CodeLensModel | undefined> {
455
await this._getCodeLensModelPromise;
456
await this._resolveCodeLensesPromise;
457
return !this._currentCodeLensModel?.isDisposed
458
? this._currentCodeLensModel
459
: undefined;
460
}
461
}
462
463
registerEditorContribution(CodeLensContribution.ID, CodeLensContribution, EditorContributionInstantiation.AfterFirstRender);
464
465
registerEditorAction(class ShowLensesInCurrentLine extends EditorAction {
466
467
constructor() {
468
super({
469
id: 'codelens.showLensesInCurrentLine',
470
precondition: EditorContextKeys.hasCodeLensProvider,
471
label: localize2('showLensOnLine', "Show CodeLens Commands for Current Line"),
472
});
473
}
474
475
async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
476
477
if (!editor.hasModel()) {
478
return;
479
}
480
481
const quickInputService = accessor.get(IQuickInputService);
482
const commandService = accessor.get(ICommandService);
483
const notificationService = accessor.get(INotificationService);
484
485
const lineNumber = editor.getSelection().positionLineNumber;
486
const codelensController = editor.getContribution<CodeLensContribution>(CodeLensContribution.ID);
487
if (!codelensController) {
488
return;
489
}
490
491
const model = await codelensController.getModel();
492
if (!model) {
493
// nothing
494
return;
495
}
496
497
const items: { label: string; command: Command }[] = [];
498
for (const lens of model.lenses) {
499
if (lens.symbol.command && lens.symbol.range.startLineNumber === lineNumber) {
500
items.push({
501
label: lens.symbol.command.title,
502
command: lens.symbol.command
503
});
504
}
505
}
506
507
if (items.length === 0) {
508
// We dont want an empty picker
509
return;
510
}
511
512
const item = await quickInputService.pick(items, {
513
canPickMany: false,
514
placeHolder: localize('placeHolder', "Select a command")
515
});
516
if (!item) {
517
// Nothing picked
518
return;
519
}
520
521
let command = item.command;
522
523
if (model.isDisposed) {
524
// try to find the same command again in-case the model has been re-created in the meantime
525
// this is a best attempt approach which shouldn't be needed because eager model re-creates
526
// shouldn't happen due to focus in/out anymore
527
const newModel = await codelensController.getModel();
528
const newLens = newModel?.lenses.find(lens => lens.symbol.range.startLineNumber === lineNumber && lens.symbol.command?.title === command.title);
529
if (!newLens || !newLens.symbol.command) {
530
return;
531
}
532
command = newLens.symbol.command;
533
}
534
535
try {
536
await commandService.executeCommand(command.id, ...(command.arguments || []));
537
} catch (err) {
538
notificationService.error(err);
539
}
540
}
541
});
542
543