Path: blob/main/src/vs/editor/contrib/codelens/browser/codelensController.ts
4779 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import { CancelablePromise, createCancelablePromise, disposableTimeout, RunOnceScheduler } from '../../../../base/common/async.js';7import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js';8import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';9import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js';10import { IActiveCodeEditor, ICodeEditor, IViewZoneChangeAccessor, MouseTargetType } from '../../../browser/editorBrowser.js';11import { EditorAction, EditorContributionInstantiation, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';12import { EditorOption } from '../../../common/config/editorOptions.js';13import { EDITOR_FONT_DEFAULTS } from '../../../common/config/fontInfo.js';14import { IEditorContribution } from '../../../common/editorCommon.js';15import { EditorContextKeys } from '../../../common/editorContextKeys.js';16import { IModelDecorationsChangeAccessor } from '../../../common/model.js';17import { CodeLens, Command } from '../../../common/languages.js';18import { CodeLensItem, CodeLensModel, getCodeLensModel } from './codelens.js';19import { ICodeLensCache } from './codeLensCache.js';20import { CodeLensHelper, CodeLensWidget } from './codelensWidget.js';21import { localize, localize2 } from '../../../../nls.js';22import { ICommandService } from '../../../../platform/commands/common/commands.js';23import { INotificationService } from '../../../../platform/notification/common/notification.js';24import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';25import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js';26import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';2728export class CodeLensContribution implements IEditorContribution {2930static readonly ID: string = 'css.editor.codeLens';3132private readonly _disposables = new DisposableStore();33private readonly _localToDispose = new DisposableStore();3435private readonly _lenses: CodeLensWidget[] = [];3637private readonly _provideCodeLensDebounce: IFeatureDebounceInformation;38private readonly _resolveCodeLensesDebounce: IFeatureDebounceInformation;39private readonly _resolveCodeLensesScheduler: RunOnceScheduler;4041private _getCodeLensModelPromise: CancelablePromise<CodeLensModel> | undefined;42private readonly _oldCodeLensModels = new DisposableStore();43private _currentCodeLensModel: CodeLensModel | undefined;44private _resolveCodeLensesPromise: CancelablePromise<void[]> | undefined;4546constructor(47private readonly _editor: ICodeEditor,48@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,49@ILanguageFeatureDebounceService debounceService: ILanguageFeatureDebounceService,50@ICommandService private readonly _commandService: ICommandService,51@INotificationService private readonly _notificationService: INotificationService,52@ICodeLensCache private readonly _codeLensCache: ICodeLensCache53) {54this._provideCodeLensDebounce = debounceService.for(_languageFeaturesService.codeLensProvider, 'CodeLensProvide', { min: 250 });55this._resolveCodeLensesDebounce = debounceService.for(_languageFeaturesService.codeLensProvider, 'CodeLensResolve', { min: 250, salt: 'resolve' });56this._resolveCodeLensesScheduler = new RunOnceScheduler(() => this._resolveCodeLensesInViewport(), this._resolveCodeLensesDebounce.default());5758this._disposables.add(this._editor.onDidChangeModel(() => this._onModelChange()));59this._disposables.add(this._editor.onDidChangeModelLanguage(() => this._onModelChange()));60this._disposables.add(this._editor.onDidChangeConfiguration((e) => {61if (e.hasChanged(EditorOption.fontInfo) || e.hasChanged(EditorOption.codeLensFontSize) || e.hasChanged(EditorOption.codeLensFontFamily)) {62this._updateLensStyle();63}64if (e.hasChanged(EditorOption.codeLens)) {65this._onModelChange();66}67}));68this._disposables.add(_languageFeaturesService.codeLensProvider.onDidChange(this._onModelChange, this));69this._onModelChange();7071this._updateLensStyle();72}7374dispose(): void {75this._localDispose();76this._localToDispose.dispose();77this._disposables.dispose();78this._oldCodeLensModels.dispose();79this._currentCodeLensModel?.dispose();80}8182private _getLayoutInfo() {83const lineHeightFactor = Math.max(1.3, this._editor.getOption(EditorOption.lineHeight) / this._editor.getOption(EditorOption.fontSize));84let fontSize = this._editor.getOption(EditorOption.codeLensFontSize);85if (!fontSize || fontSize < 5) {86fontSize = (this._editor.getOption(EditorOption.fontSize) * .9) | 0;87}88return {89fontSize,90codeLensHeight: (fontSize * lineHeightFactor) | 0,91};92}9394private _updateLensStyle(): void {9596const { codeLensHeight, fontSize } = this._getLayoutInfo();97const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily);98const editorFontInfo = this._editor.getOption(EditorOption.fontInfo);99100const { style } = this._editor.getContainerDomNode();101102style.setProperty('--vscode-editorCodeLens-lineHeight', `${codeLensHeight}px`);103style.setProperty('--vscode-editorCodeLens-fontSize', `${fontSize}px`);104style.setProperty('--vscode-editorCodeLens-fontFeatureSettings', editorFontInfo.fontFeatureSettings);105106if (fontFamily) {107style.setProperty('--vscode-editorCodeLens-fontFamily', fontFamily);108style.setProperty('--vscode-editorCodeLens-fontFamilyDefault', EDITOR_FONT_DEFAULTS.fontFamily);109}110111//112this._editor.changeViewZones(accessor => {113for (const lens of this._lenses) {114lens.updateHeight(codeLensHeight, accessor);115}116});117}118119private _localDispose(): void {120this._getCodeLensModelPromise?.cancel();121this._getCodeLensModelPromise = undefined;122this._resolveCodeLensesPromise?.cancel();123this._resolveCodeLensesPromise = undefined;124this._localToDispose.clear();125this._oldCodeLensModels.clear();126this._currentCodeLensModel?.dispose();127}128129private _onModelChange(): void {130131this._localDispose();132133const model = this._editor.getModel();134if (!model) {135return;136}137138if (!this._editor.getOption(EditorOption.codeLens) || model.isTooLargeForTokenization()) {139return;140}141142const cachedLenses = this._codeLensCache.get(model);143if (cachedLenses) {144this._renderCodeLensSymbols(cachedLenses);145}146147if (!this._languageFeaturesService.codeLensProvider.has(model)) {148// no provider -> return but check with149// cached lenses. they expire after 30 seconds150if (cachedLenses) {151disposableTimeout(() => {152const cachedLensesNow = this._codeLensCache.get(model);153if (cachedLenses === cachedLensesNow) {154this._codeLensCache.delete(model);155this._onModelChange();156}157}, 30 * 1000, this._localToDispose);158}159return;160}161162for (const provider of this._languageFeaturesService.codeLensProvider.all(model)) {163if (typeof provider.onDidChange === 'function') {164const registration = provider.onDidChange(() => scheduler.schedule());165this._localToDispose.add(registration);166}167}168169const scheduler = new RunOnceScheduler(() => {170const t1 = Date.now();171172this._getCodeLensModelPromise?.cancel();173this._getCodeLensModelPromise = createCancelablePromise(token => getCodeLensModel(this._languageFeaturesService.codeLensProvider, model, token));174175this._getCodeLensModelPromise.then(result => {176if (this._currentCodeLensModel) {177this._oldCodeLensModels.add(this._currentCodeLensModel);178}179this._currentCodeLensModel = result;180181// cache model to reduce flicker182this._codeLensCache.put(model, result);183184// update moving average185const newDelay = this._provideCodeLensDebounce.update(model, Date.now() - t1);186scheduler.delay = newDelay;187188// render lenses189this._renderCodeLensSymbols(result);190// dom.scheduleAtNextAnimationFrame(() => this._resolveCodeLensesInViewport());191this._resolveCodeLensesInViewportSoon();192}, onUnexpectedError);193194}, this._provideCodeLensDebounce.get(model));195196this._localToDispose.add(scheduler);197this._localToDispose.add(toDisposable(() => this._resolveCodeLensesScheduler.cancel()));198this._localToDispose.add(this._editor.onDidChangeModelContent(() => {199this._editor.changeDecorations(decorationsAccessor => {200this._editor.changeViewZones(viewZonesAccessor => {201const toDispose: CodeLensWidget[] = [];202let lastLensLineNumber: number = -1;203204this._lenses.forEach((lens) => {205if (!lens.isValid() || lastLensLineNumber === lens.getLineNumber()) {206// invalid -> lens collapsed, attach range doesn't exist anymore207// line_number -> lenses should never be on the same line208toDispose.push(lens);209210} else {211lens.update(viewZonesAccessor);212lastLensLineNumber = lens.getLineNumber();213}214});215216const helper = new CodeLensHelper();217toDispose.forEach((l) => {218l.dispose(helper, viewZonesAccessor);219this._lenses.splice(this._lenses.indexOf(l), 1);220});221helper.commit(decorationsAccessor);222});223});224225// Ask for all references again226scheduler.schedule();227228// Cancel pending and active resolve requests229this._resolveCodeLensesScheduler.cancel();230this._resolveCodeLensesPromise?.cancel();231this._resolveCodeLensesPromise = undefined;232}));233this._localToDispose.add(this._editor.onDidFocusEditorText(() => {234scheduler.schedule();235}));236this._localToDispose.add(this._editor.onDidBlurEditorText(() => {237scheduler.cancel();238}));239this._localToDispose.add(this._editor.onDidScrollChange(e => {240if (e.scrollTopChanged && this._lenses.length > 0) {241this._resolveCodeLensesInViewportSoon();242}243}));244this._localToDispose.add(this._editor.onDidLayoutChange(() => {245this._resolveCodeLensesInViewportSoon();246}));247this._localToDispose.add(toDisposable(() => {248if (this._editor.getModel()) {249const scrollState = StableEditorScrollState.capture(this._editor);250this._editor.changeDecorations(decorationsAccessor => {251this._editor.changeViewZones(viewZonesAccessor => {252this._disposeAllLenses(decorationsAccessor, viewZonesAccessor);253});254});255scrollState.restore(this._editor);256} else {257// No accessors available258this._disposeAllLenses(undefined, undefined);259}260}));261this._localToDispose.add(this._editor.onMouseDown(e => {262if (e.target.type !== MouseTargetType.CONTENT_WIDGET) {263return;264}265let target = e.target.element;266if (target?.tagName === 'SPAN') {267target = target.parentElement;268}269if (target?.tagName === 'A') {270for (const lens of this._lenses) {271const command = lens.getCommand(target as HTMLLinkElement);272if (command) {273this._commandService.executeCommand(command.id, ...(command.arguments || [])).catch(err => this._notificationService.error(err));274break;275}276}277}278}));279scheduler.schedule();280}281282private _disposeAllLenses(decChangeAccessor: IModelDecorationsChangeAccessor | undefined, viewZoneChangeAccessor: IViewZoneChangeAccessor | undefined): void {283const helper = new CodeLensHelper();284for (const lens of this._lenses) {285lens.dispose(helper, viewZoneChangeAccessor);286}287if (decChangeAccessor) {288helper.commit(decChangeAccessor);289}290this._lenses.length = 0;291}292293private _renderCodeLensSymbols(symbols: CodeLensModel): void {294if (!this._editor.hasModel()) {295return;296}297298const maxLineNumber = this._editor.getModel().getLineCount();299const groups: CodeLensItem[][] = [];300let lastGroup: CodeLensItem[] | undefined;301302for (const symbol of symbols.lenses) {303const line = symbol.symbol.range.startLineNumber;304if (line < 1 || line > maxLineNumber) {305// invalid code lens306continue;307} else if (lastGroup && lastGroup[lastGroup.length - 1].symbol.range.startLineNumber === line) {308// on same line as previous309lastGroup.push(symbol);310} else {311// on later line as previous312lastGroup = [symbol];313groups.push(lastGroup);314}315}316317if (!groups.length && !this._lenses.length) {318// Nothing to change319return;320}321322const scrollState = StableEditorScrollState.capture(this._editor);323const layoutInfo = this._getLayoutInfo();324325this._editor.changeDecorations(decorationsAccessor => {326this._editor.changeViewZones(viewZoneAccessor => {327328const helper = new CodeLensHelper();329let codeLensIndex = 0;330let groupsIndex = 0;331332while (groupsIndex < groups.length && codeLensIndex < this._lenses.length) {333334const symbolsLineNumber = groups[groupsIndex][0].symbol.range.startLineNumber;335const codeLensLineNumber = this._lenses[codeLensIndex].getLineNumber();336337if (codeLensLineNumber < symbolsLineNumber) {338this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor);339this._lenses.splice(codeLensIndex, 1);340} else if (codeLensLineNumber === symbolsLineNumber) {341this._lenses[codeLensIndex].updateCodeLensSymbols(groups[groupsIndex], helper);342groupsIndex++;343codeLensIndex++;344} else {345this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], <IActiveCodeEditor>this._editor, helper, viewZoneAccessor, layoutInfo.codeLensHeight, () => this._resolveCodeLensesInViewportSoon()));346codeLensIndex++;347groupsIndex++;348}349}350351// Delete extra code lenses352while (codeLensIndex < this._lenses.length) {353this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor);354this._lenses.splice(codeLensIndex, 1);355}356357// Create extra symbols358while (groupsIndex < groups.length) {359this._lenses.push(new CodeLensWidget(groups[groupsIndex], <IActiveCodeEditor>this._editor, helper, viewZoneAccessor, layoutInfo.codeLensHeight, () => this._resolveCodeLensesInViewportSoon()));360groupsIndex++;361}362363helper.commit(decorationsAccessor);364});365});366367scrollState.restore(this._editor);368}369370private _resolveCodeLensesInViewportSoon(): void {371const model = this._editor.getModel();372if (model) {373this._resolveCodeLensesScheduler.schedule();374}375}376377private _resolveCodeLensesInViewport(): void {378379this._resolveCodeLensesPromise?.cancel();380this._resolveCodeLensesPromise = undefined;381382const model = this._editor.getModel();383if (!model) {384return;385}386387const toResolve: Array<ReadonlyArray<CodeLensItem>> = [];388const lenses: CodeLensWidget[] = [];389this._lenses.forEach((lens) => {390const request = lens.computeIfNecessary(model);391if (request) {392toResolve.push(request);393lenses.push(lens);394}395});396397if (toResolve.length === 0) {398this._oldCodeLensModels.clear();399return;400}401402const t1 = Date.now();403404const resolvePromise = createCancelablePromise(token => {405406const promises = toResolve.map((request, i) => {407408const resolvedSymbols = new Array<CodeLens | undefined | null>(request.length);409const promises = request.map((request, i) => {410if (!request.symbol.command && typeof request.provider.resolveCodeLens === 'function') {411return Promise.resolve(request.provider.resolveCodeLens(model, request.symbol, token)).then(symbol => {412resolvedSymbols[i] = symbol;413}, onUnexpectedExternalError);414} else {415resolvedSymbols[i] = request.symbol;416return Promise.resolve(undefined);417}418});419420return Promise.all(promises).then(() => {421if (!token.isCancellationRequested && !lenses[i].isDisposed()) {422lenses[i].updateCommands(resolvedSymbols);423}424});425});426427return Promise.all(promises);428});429this._resolveCodeLensesPromise = resolvePromise;430431this._resolveCodeLensesPromise.then(() => {432433// update moving average434const newDelay = this._resolveCodeLensesDebounce.update(model, Date.now() - t1);435this._resolveCodeLensesScheduler.delay = newDelay;436437if (this._currentCodeLensModel) { // update the cached state with new resolved items438this._codeLensCache.put(model, this._currentCodeLensModel);439}440this._oldCodeLensModels.clear(); // dispose old models once we have updated the UI with the current model441if (resolvePromise === this._resolveCodeLensesPromise) {442this._resolveCodeLensesPromise = undefined;443}444}, err => {445onUnexpectedError(err); // can also be cancellation!446if (resolvePromise === this._resolveCodeLensesPromise) {447this._resolveCodeLensesPromise = undefined;448}449});450}451452async getModel(): Promise<CodeLensModel | undefined> {453await this._getCodeLensModelPromise;454await this._resolveCodeLensesPromise;455return !this._currentCodeLensModel?.isDisposed456? this._currentCodeLensModel457: undefined;458}459}460461registerEditorContribution(CodeLensContribution.ID, CodeLensContribution, EditorContributionInstantiation.AfterFirstRender);462463registerEditorAction(class ShowLensesInCurrentLine extends EditorAction {464465constructor() {466super({467id: 'codelens.showLensesInCurrentLine',468precondition: EditorContextKeys.hasCodeLensProvider,469label: localize2('showLensOnLine', "Show CodeLens Commands for Current Line"),470});471}472473async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {474475if (!editor.hasModel()) {476return;477}478479const quickInputService = accessor.get(IQuickInputService);480const commandService = accessor.get(ICommandService);481const notificationService = accessor.get(INotificationService);482483const lineNumber = editor.getSelection().positionLineNumber;484const codelensController = editor.getContribution<CodeLensContribution>(CodeLensContribution.ID);485if (!codelensController) {486return;487}488489const model = await codelensController.getModel();490if (!model) {491// nothing492return;493}494495const items: { label: string; command: Command }[] = [];496for (const lens of model.lenses) {497if (lens.symbol.command && lens.symbol.range.startLineNumber === lineNumber) {498items.push({499label: lens.symbol.command.title,500command: lens.symbol.command501});502}503}504505if (items.length === 0) {506// We dont want an empty picker507return;508}509510const item = await quickInputService.pick(items, {511canPickMany: false,512placeHolder: localize('placeHolder', "Select a command")513});514if (!item) {515// Nothing picked516return;517}518519let command = item.command;520521if (model.isDisposed) {522// try to find the same command again in-case the model has been re-created in the meantime523// this is a best attempt approach which shouldn't be needed because eager model re-creates524// shouldn't happen due to focus in/out anymore525const newModel = await codelensController.getModel();526const newLens = newModel?.lenses.find(lens => lens.symbol.range.startLineNumber === lineNumber && lens.symbol.command?.title === command.title);527if (!newLens || !newLens.symbol.command) {528return;529}530command = newLens.symbol.command;531}532533try {534await commandService.executeCommand(command.id, ...(command.arguments || []));535} catch (err) {536notificationService.error(err);537}538}539});540541542