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