/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CancelablePromise, RunOnceScheduler, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import { toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import { ICodeEditor, MouseTargetType, IViewZoneChangeAccessor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { CodeLensProviderRegistry, CodeLens } from 'vs/editor/common/modes'; import { CodeLensModel, getCodeLensData, CodeLensItem } from 'vs/editor/contrib/codelens/codelens'; import { CodeLensWidget, CodeLensHelper } from 'vs/editor/contrib/codelens/codelensWidget'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICodeLensCache } from 'vs/editor/contrib/codelens/codeLensCache'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import * as dom from 'vs/base/browser/dom'; import { hash } from 'vs/base/common/hash'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; export class CodeLensContribution implements IEditorContribution { public static readonly ID: string = 'css.editor.codeLens'; private _isEnabled: boolean; private readonly _globalToDispose = new DisposableStore(); private readonly _localToDispose = new DisposableStore(); private readonly _styleElement: HTMLStyleElement; private readonly _styleClassName: string; private _lenses: CodeLensWidget[] = []; private _currentFindCodeLensSymbolsPromise: CancelablePromise | undefined; private _oldCodeLensModels = new DisposableStore(); private _currentCodeLensModel: CodeLensModel | undefined; private _modelChangeCounter: number = 0; private _currentResolveCodeLensSymbolsPromise: CancelablePromise | undefined; private _detectVisibleLenses: RunOnceScheduler | undefined; constructor( private readonly _editor: ICodeEditor, @ICommandService private readonly _commandService: ICommandService, @INotificationService private readonly _notificationService: INotificationService, @ICodeLensCache private readonly _codeLensCache: ICodeLensCache ) { this._isEnabled = this._editor.getOption(EditorOption.codeLens); this._globalToDispose.add(this._editor.onDidChangeModel(() => this._onModelChange())); this._globalToDispose.add(this._editor.onDidChangeModelLanguage(() => this._onModelChange())); this._globalToDispose.add(this._editor.onDidChangeConfiguration(() => { const prevIsEnabled = this._isEnabled; this._isEnabled = this._editor.getOption(EditorOption.codeLens); if (prevIsEnabled !== this._isEnabled) { this._onModelChange(); } })); this._globalToDispose.add(CodeLensProviderRegistry.onDidChange(this._onModelChange, this)); this._globalToDispose.add(this._editor.onDidChangeConfiguration(e => { if (e.hasChanged(EditorOption.fontInfo)) { this._updateLensStyle(); } })); this._onModelChange(); this._styleClassName = hash(this._editor.getId()).toString(16); this._styleElement = dom.createStyleSheet( dom.isInShadowDOM(this._editor.getContainerDomNode()) ? this._editor.getContainerDomNode() : undefined ); this._updateLensStyle(); } dispose(): void { this._localDispose(); this._globalToDispose.dispose(); this._oldCodeLensModels.dispose(); dispose(this._currentCodeLensModel); } private _updateLensStyle(): void { const options = this._editor.getOptions(); const fontInfo = options.get(EditorOption.fontInfo); const lineHeight = options.get(EditorOption.lineHeight); const height = Math.round(lineHeight * 1.1); const fontSize = Math.round(fontInfo.fontSize * 0.9); const newStyle = ` .monaco-editor .codelens-decoration.${this._styleClassName} { height: ${height}px; line-height: ${lineHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontInfo.fontSize * 0.45)}px;} .monaco-editor .codelens-decoration.${this._styleClassName} > a > .codicon { line-height: ${lineHeight}px; font-size: ${fontSize}px; } `; this._styleElement.innerHTML = newStyle; } private _localDispose(): void { if (this._currentFindCodeLensSymbolsPromise) { this._currentFindCodeLensSymbolsPromise.cancel(); this._currentFindCodeLensSymbolsPromise = undefined; this._modelChangeCounter++; } if (this._currentResolveCodeLensSymbolsPromise) { this._currentResolveCodeLensSymbolsPromise.cancel(); this._currentResolveCodeLensSymbolsPromise = undefined; } this._localToDispose.clear(); this._oldCodeLensModels.clear(); dispose(this._currentCodeLensModel); } private _onModelChange(): void { this._localDispose(); const model = this._editor.getModel(); if (!model) { return; } if (!this._isEnabled) { return; } const cachedLenses = this._codeLensCache.get(model); if (cachedLenses) { this._renderCodeLensSymbols(cachedLenses); } if (!CodeLensProviderRegistry.has(model)) { // no provider -> return but check with // cached lenses. they expire after 30 seconds if (cachedLenses) { this._localToDispose.add(disposableTimeout(() => { const cachedLensesNow = this._codeLensCache.get(model); if (cachedLenses === cachedLensesNow) { this._codeLensCache.delete(model); this._onModelChange(); } }, 30 * 1000)); } return; } for (const provider of CodeLensProviderRegistry.all(model)) { if (typeof provider.onDidChange === 'function') { let registration = provider.onDidChange(() => scheduler.schedule()); this._localToDispose.add(registration); } } const detectVisibleLenses = this._detectVisibleLenses = new RunOnceScheduler(() => this._onViewportChanged(), 250); const scheduler = new RunOnceScheduler(() => { const counterValue = ++this._modelChangeCounter; if (this._currentFindCodeLensSymbolsPromise) { this._currentFindCodeLensSymbolsPromise.cancel(); } this._currentFindCodeLensSymbolsPromise = createCancelablePromise(token => getCodeLensData(model, token)); this._currentFindCodeLensSymbolsPromise.then(result => { if (counterValue === this._modelChangeCounter) { // only the last one wins if (this._currentCodeLensModel) { this._oldCodeLensModels.add(this._currentCodeLensModel); } this._currentCodeLensModel = result; // cache model to reduce flicker this._codeLensCache.put(model, result); // render lenses this._renderCodeLensSymbols(result); detectVisibleLenses.schedule(); } }, onUnexpectedError); }, 250); this._localToDispose.add(scheduler); this._localToDispose.add(detectVisibleLenses); this._localToDispose.add(this._editor.onDidChangeModelContent(() => { this._editor.changeDecorations(decorationsAccessor => { this._editor.changeViewZones(viewZonesAccessor => { let toDispose: CodeLensWidget[] = []; let lastLensLineNumber: number = -1; this._lenses.forEach((lens) => { if (!lens.isValid() || lastLensLineNumber === lens.getLineNumber()) { // invalid -> lens collapsed, attach range doesn't exist anymore // line_number -> lenses should never be on the same line toDispose.push(lens); } else { lens.update(viewZonesAccessor); lastLensLineNumber = lens.getLineNumber(); } }); let helper = new CodeLensHelper(); toDispose.forEach((l) => { l.dispose(helper, viewZonesAccessor); this._lenses.splice(this._lenses.indexOf(l), 1); }); helper.commit(decorationsAccessor); }); }); // Compute new `visible` code lenses detectVisibleLenses.schedule(); // Ask for all references again scheduler.schedule(); })); this._localToDispose.add(this._editor.onDidScrollChange(e => { if (e.scrollTopChanged && this._lenses.length > 0) { detectVisibleLenses.schedule(); } })); this._localToDispose.add(this._editor.onDidLayoutChange(() => { detectVisibleLenses.schedule(); })); this._localToDispose.add(toDisposable(() => { if (this._editor.getModel()) { const scrollState = StableEditorScrollState.capture(this._editor); this._editor.changeDecorations(decorationsAccessor => { this._editor.changeViewZones(viewZonesAccessor => { this._disposeAllLenses(decorationsAccessor, viewZonesAccessor); }); }); scrollState.restore(this._editor); } else { // No accessors available this._disposeAllLenses(undefined, undefined); } })); this._localToDispose.add(this._editor.onMouseUp(e => { if (e.target.type !== MouseTargetType.CONTENT_WIDGET) { return; } let target = e.target.element; if (target?.tagName === 'SPAN') { target = target.parentElement; } if (target?.tagName === 'A') { for (const lens of this._lenses) { let command = lens.getCommand(target as HTMLLinkElement); if (command) { this._commandService.executeCommand(command.id, ...(command.arguments || [])).catch(err => this._notificationService.error(err)); break; } } } })); scheduler.schedule(); } private _disposeAllLenses(decChangeAccessor: IModelDecorationsChangeAccessor | undefined, viewZoneChangeAccessor: IViewZoneChangeAccessor | undefined): void { const helper = new CodeLensHelper(); for (const lens of this._lenses) { lens.dispose(helper, viewZoneChangeAccessor); } if (decChangeAccessor) { helper.commit(decChangeAccessor); } this._lenses = []; } private _renderCodeLensSymbols(symbols: CodeLensModel): void { if (!this._editor.hasModel()) { return; } let maxLineNumber = this._editor.getModel().getLineCount(); let groups: CodeLensItem[][] = []; let lastGroup: CodeLensItem[] | undefined; for (let symbol of symbols.lenses) { let line = symbol.symbol.range.startLineNumber; if (line < 1 || line > maxLineNumber) { // invalid code lens continue; } else if (lastGroup && lastGroup[lastGroup.length - 1].symbol.range.startLineNumber === line) { // on same line as previous lastGroup.push(symbol); } else { // on later line as previous lastGroup = [symbol]; groups.push(lastGroup); } } const scrollState = StableEditorScrollState.capture(this._editor); this._editor.changeDecorations(decorationsAccessor => { this._editor.changeViewZones(viewZoneAccessor => { const helper = new CodeLensHelper(); let codeLensIndex = 0; let groupsIndex = 0; while (groupsIndex < groups.length && codeLensIndex < this._lenses.length) { let symbolsLineNumber = groups[groupsIndex][0].symbol.range.startLineNumber; let codeLensLineNumber = this._lenses[codeLensIndex].getLineNumber(); if (codeLensLineNumber < symbolsLineNumber) { this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor); this._lenses.splice(codeLensIndex, 1); } else if (codeLensLineNumber === symbolsLineNumber) { this._lenses[codeLensIndex].updateCodeLensSymbols(groups[groupsIndex], helper); groupsIndex++; codeLensIndex++; } else { this._lenses.splice(codeLensIndex, 0, new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); codeLensIndex++; groupsIndex++; } } // Delete extra code lenses while (codeLensIndex < this._lenses.length) { this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor); this._lenses.splice(codeLensIndex, 1); } // Create extra symbols while (groupsIndex < groups.length) { this._lenses.push(new CodeLensWidget(groups[groupsIndex], this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._detectVisibleLenses && this._detectVisibleLenses.schedule())); groupsIndex++; } helper.commit(decorationsAccessor); }); }); scrollState.restore(this._editor); } private _onViewportChanged(): void { if (this._currentResolveCodeLensSymbolsPromise) { this._currentResolveCodeLensSymbolsPromise.cancel(); this._currentResolveCodeLensSymbolsPromise = undefined; } const model = this._editor.getModel(); if (!model) { return; } const toResolve: CodeLensItem[][] = []; const lenses: CodeLensWidget[] = []; this._lenses.forEach((lens) => { const request = lens.computeIfNecessary(model); if (request) { toResolve.push(request); lenses.push(lens); } }); if (toResolve.length === 0) { return; } const resolvePromise = createCancelablePromise(token => { const promises = toResolve.map((request, i) => { const resolvedSymbols = new Array(request.length); const promises = request.map((request, i) => { if (!request.symbol.command && typeof request.provider.resolveCodeLens === 'function') { return Promise.resolve(request.provider.resolveCodeLens(model, request.symbol, token)).then(symbol => { resolvedSymbols[i] = symbol; }, onUnexpectedExternalError); } else { resolvedSymbols[i] = request.symbol; return Promise.resolve(undefined); } }); return Promise.all(promises).then(() => { if (!token.isCancellationRequested && !lenses[i].isDisposed()) { lenses[i].updateCommands(resolvedSymbols); } }); }); return Promise.all(promises); }); this._currentResolveCodeLensSymbolsPromise = resolvePromise; this._currentResolveCodeLensSymbolsPromise.then(() => { if (this._currentCodeLensModel) { // update the cached state with new resolved items this._codeLensCache.put(model, this._currentCodeLensModel); } this._oldCodeLensModels.clear(); // dispose old models once we have updated the UI with the current model if (resolvePromise === this._currentResolveCodeLensSymbolsPromise) { this._currentResolveCodeLensSymbolsPromise = undefined; } }, err => { onUnexpectedError(err); // can also be cancellation! if (resolvePromise === this._currentResolveCodeLensSymbolsPromise) { this._currentResolveCodeLensSymbolsPromise = undefined; } }); } public getLenses(): CodeLensWidget[] { return this._lenses; } } export class ShowLensesInCurrentLineCommand extends EditorCommand { public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise { const quickInputService = accessor.get(IQuickInputService); const commandService = accessor.get(ICommandService); const notificationService = accessor.get(INotificationService); const lineNumber = editor.getSelection()?.positionLineNumber; const codelensController = editor.getContribution(CodeLensContribution.ID) as CodeLensContribution; const activeLensesWidgets = codelensController.getLenses().filter(lens => lens.getLineNumber() === lineNumber); const commandArguments: Map = new Map(); const picker = quickInputService.createQuickPick(); const items: (IQuickPickItem | IQuickPickSeparator)[] = []; activeLensesWidgets.forEach(widget => { widget.getItems().forEach(codelens => { const command = codelens.symbol.command; if (!command) { return; } items.push({ id: command.id, label: command.title }); commandArguments.set(command.id, command.arguments); }); }); picker.items = items; picker.canSelectMany = false; picker.onDidAccept(_ => { const selectedItems = picker.selectedItems; if (selectedItems.length === 1) { const id = selectedItems[0].id!; if (!id) { picker.hide(); return; } commandService.executeCommand(id, ...(commandArguments.get(id) || [])).catch(err => notificationService.error(err)); } picker.hide(); }); picker.show(); } } registerEditorContribution(CodeLensContribution.ID, CodeLensContribution); const showLensesInCurrentLineCommand = new ShowLensesInCurrentLineCommand({ id: 'codelens.showLensesInCurrentLine', precondition: undefined }); showLensesInCurrentLineCommand.register();