Path: blob/main/src/vs/editor/contrib/codelens/browser/codelensController.ts
5251 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._resolveCodeLensesScheduler.dispose();79this._oldCodeLensModels.dispose();80this._currentCodeLensModel?.dispose();81}8283private _getLayoutInfo() {84const lineHeightFactor = Math.max(1.3, this._editor.getOption(EditorOption.lineHeight) / this._editor.getOption(EditorOption.fontSize));85let fontSize = this._editor.getOption(EditorOption.codeLensFontSize);86if (!fontSize || fontSize < 5) {87fontSize = (this._editor.getOption(EditorOption.fontSize) * .9) | 0;88}89return {90fontSize,91codeLensHeight: (fontSize * lineHeightFactor) | 0,92};93}9495private _updateLensStyle(): void {9697const { codeLensHeight, fontSize } = this._getLayoutInfo();98const fontFamily = this._editor.getOption(EditorOption.codeLensFontFamily);99const editorFontInfo = this._editor.getOption(EditorOption.fontInfo);100101const { style } = this._editor.getContainerDomNode();102103style.setProperty('--vscode-editorCodeLens-lineHeight', `${codeLensHeight}px`);104style.setProperty('--vscode-editorCodeLens-fontSize', `${fontSize}px`);105style.setProperty('--vscode-editorCodeLens-fontFeatureSettings', editorFontInfo.fontFeatureSettings);106107if (fontFamily) {108style.setProperty('--vscode-editorCodeLens-fontFamily', fontFamily);109style.setProperty('--vscode-editorCodeLens-fontFamilyDefault', EDITOR_FONT_DEFAULTS.fontFamily);110}111112//113this._editor.changeViewZones(accessor => {114for (const lens of this._lenses) {115lens.updateHeight(codeLensHeight, accessor);116}117});118}119120private _localDispose(): void {121this._getCodeLensModelPromise?.cancel();122this._getCodeLensModelPromise = undefined;123this._resolveCodeLensesPromise?.cancel();124this._resolveCodeLensesPromise = undefined;125this._localToDispose.clear();126this._oldCodeLensModels.clear();127this._currentCodeLensModel?.dispose();128}129130private _onModelChange(): void {131132this._localDispose();133134const model = this._editor.getModel();135if (!model) {136return;137}138139if (!this._editor.getOption(EditorOption.codeLens) || model.isTooLargeForTokenization()) {140return;141}142143const cachedLenses = this._codeLensCache.get(model);144if (cachedLenses) {145this._renderCodeLensSymbols(cachedLenses);146}147148if (!this._languageFeaturesService.codeLensProvider.has(model)) {149// no provider -> return but check with150// cached lenses. they expire after 30 seconds151if (cachedLenses) {152disposableTimeout(() => {153const cachedLensesNow = this._codeLensCache.get(model);154if (cachedLenses === cachedLensesNow) {155this._codeLensCache.delete(model);156this._onModelChange();157}158}, 30 * 1000, this._localToDispose);159}160return;161}162163for (const provider of this._languageFeaturesService.codeLensProvider.all(model)) {164if (typeof provider.onDidChange === 'function') {165const registration = provider.onDidChange(() => scheduler.schedule());166this._localToDispose.add(registration);167}168}169170const scheduler = new RunOnceScheduler(() => {171const t1 = Date.now();172173this._getCodeLensModelPromise?.cancel();174this._getCodeLensModelPromise = createCancelablePromise(token => getCodeLensModel(this._languageFeaturesService.codeLensProvider, model, token));175176this._getCodeLensModelPromise.then(result => {177if (this._currentCodeLensModel) {178this._oldCodeLensModels.add(this._currentCodeLensModel);179}180this._currentCodeLensModel = result;181182// cache model to reduce flicker183this._codeLensCache.put(model, result);184185// update moving average186const newDelay = this._provideCodeLensDebounce.update(model, Date.now() - t1);187scheduler.delay = newDelay;188189// render lenses190this._renderCodeLensSymbols(result);191// dom.scheduleAtNextAnimationFrame(() => this._resolveCodeLensesInViewport());192this._resolveCodeLensesInViewportSoon();193}, onUnexpectedError);194195}, this._provideCodeLensDebounce.get(model));196197this._localToDispose.add(scheduler);198this._localToDispose.add(toDisposable(() => this._resolveCodeLensesScheduler.cancel()));199this._localToDispose.add(this._editor.onDidChangeModelContent(() => {200this._editor.changeDecorations(decorationsAccessor => {201this._editor.changeViewZones(viewZonesAccessor => {202const toDispose: CodeLensWidget[] = [];203let lastLensLineNumber: number = -1;204205this._lenses.forEach((lens) => {206if (!lens.isValid() || lastLensLineNumber === lens.getLineNumber()) {207// invalid -> lens collapsed, attach range doesn't exist anymore208// line_number -> lenses should never be on the same line209toDispose.push(lens);210211} else {212lens.update(viewZonesAccessor);213lastLensLineNumber = lens.getLineNumber();214}215});216217const helper = new CodeLensHelper();218toDispose.forEach((l) => {219l.dispose(helper, viewZonesAccessor);220this._lenses.splice(this._lenses.indexOf(l), 1);221});222helper.commit(decorationsAccessor);223});224});225226// Ask for all references again227scheduler.schedule();228229// Cancel pending and active resolve requests230this._resolveCodeLensesScheduler.cancel();231this._resolveCodeLensesPromise?.cancel();232this._resolveCodeLensesPromise = undefined;233}));234this._localToDispose.add(this._editor.onDidFocusEditorText(() => {235scheduler.schedule();236}));237this._localToDispose.add(this._editor.onDidBlurEditorText(() => {238scheduler.cancel();239}));240this._localToDispose.add(this._editor.onDidScrollChange(e => {241if (e.scrollTopChanged && this._lenses.length > 0) {242this._resolveCodeLensesInViewportSoon();243}244}));245this._localToDispose.add(this._editor.onDidLayoutChange(() => {246this._resolveCodeLensesInViewportSoon();247}));248this._localToDispose.add(toDisposable(() => {249if (this._editor.getModel()) {250const scrollState = StableEditorScrollState.capture(this._editor);251this._editor.changeDecorations(decorationsAccessor => {252this._editor.changeViewZones(viewZonesAccessor => {253this._disposeAllLenses(decorationsAccessor, viewZonesAccessor);254});255});256scrollState.restore(this._editor);257} else {258// No accessors available259this._disposeAllLenses(undefined, undefined);260}261}));262this._localToDispose.add(this._editor.onMouseDown(e => {263if (e.target.type !== MouseTargetType.CONTENT_WIDGET) {264return;265}266let target = e.target.element;267if (target?.tagName === 'SPAN') {268target = target.parentElement;269}270if (target?.tagName === 'A') {271for (const lens of this._lenses) {272const command = lens.getCommand(target as HTMLLinkElement);273if (command) {274this._commandService.executeCommand(command.id, ...(command.arguments || [])).catch(err => this._notificationService.error(err));275break;276}277}278}279}));280scheduler.schedule();281}282283private _disposeAllLenses(decChangeAccessor: IModelDecorationsChangeAccessor | undefined, viewZoneChangeAccessor: IViewZoneChangeAccessor | undefined): void {284const helper = new CodeLensHelper();285for (const lens of this._lenses) {286lens.dispose(helper, viewZoneChangeAccessor);287}288if (decChangeAccessor) {289helper.commit(decChangeAccessor);290}291this._lenses.length = 0;292}293294private _renderCodeLensSymbols(symbols: CodeLensModel): void {295if (!this._editor.hasModel()) {296return;297}298299const maxLineNumber = this._editor.getModel().getLineCount();300const groups: CodeLensItem[][] = [];301let lastGroup: CodeLensItem[] | undefined;302303for (const symbol of symbols.lenses) {304const line = symbol.symbol.range.startLineNumber;305if (line < 1 || line > maxLineNumber) {306// invalid code lens307continue;308} else if (lastGroup && lastGroup[lastGroup.length - 1].symbol.range.startLineNumber === line) {309// on same line as previous310lastGroup.push(symbol);311} else {312// on later line as previous313lastGroup = [symbol];314groups.push(lastGroup);315}316}317318if (!groups.length && !this._lenses.length) {319// Nothing to change320return;321}322323const scrollState = StableEditorScrollState.capture(this._editor);324const layoutInfo = this._getLayoutInfo();325326this._editor.changeDecorations(decorationsAccessor => {327this._editor.changeViewZones(viewZoneAccessor => {328329const helper = new CodeLensHelper();330let codeLensIndex = 0;331let groupsIndex = 0;332333while (groupsIndex < groups.length && codeLensIndex < this._lenses.length) {334335const symbolsLineNumber = groups[groupsIndex][0].symbol.range.startLineNumber;336const codeLensLineNumber = this._lenses[codeLensIndex].getLineNumber();337338if (codeLensLineNumber < symbolsLineNumber) {339this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor);340this._lenses.splice(codeLensIndex, 1);341} else if (codeLensLineNumber === symbolsLineNumber) {342this._lenses[codeLensIndex].updateCodeLensSymbols(groups[groupsIndex], helper);343groupsIndex++;344codeLensIndex++;345} else {346this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], <IActiveCodeEditor>this._editor, helper, viewZoneAccessor, layoutInfo.codeLensHeight, () => this._resolveCodeLensesInViewportSoon()));347codeLensIndex++;348groupsIndex++;349}350}351352// Delete extra code lenses353while (codeLensIndex < this._lenses.length) {354this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor);355this._lenses.splice(codeLensIndex, 1);356}357358// Create extra symbols359while (groupsIndex < groups.length) {360this._lenses.push(new CodeLensWidget(groups[groupsIndex], <IActiveCodeEditor>this._editor, helper, viewZoneAccessor, layoutInfo.codeLensHeight, () => this._resolveCodeLensesInViewportSoon()));361groupsIndex++;362}363364helper.commit(decorationsAccessor);365});366});367368scrollState.restore(this._editor);369}370371private _resolveCodeLensesInViewportSoon(): void {372const model = this._editor.getModel();373if (model) {374this._resolveCodeLensesScheduler.schedule();375}376}377378private _resolveCodeLensesInViewport(): void {379380this._resolveCodeLensesPromise?.cancel();381this._resolveCodeLensesPromise = undefined;382383const model = this._editor.getModel();384if (!model) {385return;386}387388const toResolve: Array<ReadonlyArray<CodeLensItem>> = [];389const lenses: CodeLensWidget[] = [];390this._lenses.forEach((lens) => {391const request = lens.computeIfNecessary(model);392if (request) {393toResolve.push(request);394lenses.push(lens);395}396});397398if (toResolve.length === 0) {399this._oldCodeLensModels.clear();400return;401}402403const t1 = Date.now();404405const resolvePromise = createCancelablePromise(token => {406407const promises = toResolve.map((request, i) => {408409const resolvedSymbols = new Array<CodeLens | undefined | null>(request.length);410const promises = request.map((request, i) => {411if (!request.symbol.command && typeof request.provider.resolveCodeLens === 'function') {412return Promise.resolve(request.provider.resolveCodeLens(model, request.symbol, token)).then(symbol => {413resolvedSymbols[i] = symbol;414}, onUnexpectedExternalError);415} else {416resolvedSymbols[i] = request.symbol;417return Promise.resolve(undefined);418}419});420421return Promise.all(promises).then(() => {422if (!token.isCancellationRequested && !lenses[i].isDisposed()) {423lenses[i].updateCommands(resolvedSymbols);424}425});426});427428return Promise.all(promises);429});430this._resolveCodeLensesPromise = resolvePromise;431432this._resolveCodeLensesPromise.then(() => {433434// update moving average435const newDelay = this._resolveCodeLensesDebounce.update(model, Date.now() - t1);436this._resolveCodeLensesScheduler.delay = newDelay;437438if (this._currentCodeLensModel) { // update the cached state with new resolved items439this._codeLensCache.put(model, this._currentCodeLensModel);440}441this._oldCodeLensModels.clear(); // dispose old models once we have updated the UI with the current model442if (resolvePromise === this._resolveCodeLensesPromise) {443this._resolveCodeLensesPromise = undefined;444}445}, err => {446onUnexpectedError(err); // can also be cancellation!447if (resolvePromise === this._resolveCodeLensesPromise) {448this._resolveCodeLensesPromise = undefined;449}450});451}452453async getModel(): Promise<CodeLensModel | undefined> {454await this._getCodeLensModelPromise;455await this._resolveCodeLensesPromise;456return !this._currentCodeLensModel?.isDisposed457? this._currentCodeLensModel458: undefined;459}460}461462registerEditorContribution(CodeLensContribution.ID, CodeLensContribution, EditorContributionInstantiation.AfterFirstRender);463464registerEditorAction(class ShowLensesInCurrentLine extends EditorAction {465466constructor() {467super({468id: 'codelens.showLensesInCurrentLine',469precondition: EditorContextKeys.hasCodeLensProvider,470label: localize2('showLensOnLine', "Show CodeLens Commands for Current Line"),471});472}473474async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {475476if (!editor.hasModel()) {477return;478}479480const quickInputService = accessor.get(IQuickInputService);481const commandService = accessor.get(ICommandService);482const notificationService = accessor.get(INotificationService);483484const lineNumber = editor.getSelection().positionLineNumber;485const codelensController = editor.getContribution<CodeLensContribution>(CodeLensContribution.ID);486if (!codelensController) {487return;488}489490const model = await codelensController.getModel();491if (!model) {492// nothing493return;494}495496const items: { label: string; command: Command }[] = [];497for (const lens of model.lenses) {498if (lens.symbol.command && lens.symbol.range.startLineNumber === lineNumber) {499items.push({500label: lens.symbol.command.title,501command: lens.symbol.command502});503}504}505506if (items.length === 0) {507// We dont want an empty picker508return;509}510511const item = await quickInputService.pick(items, {512canPickMany: false,513placeHolder: localize('placeHolder', "Select a command")514});515if (!item) {516// Nothing picked517return;518}519520let command = item.command;521522if (model.isDisposed) {523// try to find the same command again in-case the model has been re-created in the meantime524// this is a best attempt approach which shouldn't be needed because eager model re-creates525// shouldn't happen due to focus in/out anymore526const newModel = await codelensController.getModel();527const newLens = newModel?.lenses.find(lens => lens.symbol.range.startLineNumber === lineNumber && lens.symbol.command?.title === command.title);528if (!newLens || !newLens.symbol.command) {529return;530}531command = newLens.symbol.command;532}533534try {535await commandService.executeCommand(command.id, ...(command.arguments || []));536} catch (err) {537notificationService.error(err);538}539}540});541542543