Path: blob/main/src/vs/editor/contrib/codelens/browser/codelensWidget.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*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../base/browser/dom.js';6import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';7import { Constants } from '../../../../base/common/uint.js';8import './codelensWidget.css';9import { ContentWidgetPositionPreference, IActiveCodeEditor, IContentWidget, IContentWidgetPosition, IViewZone, IViewZoneChangeAccessor } from '../../../browser/editorBrowser.js';10import { Range } from '../../../common/core/range.js';11import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from '../../../common/model.js';12import { ModelDecorationOptions } from '../../../common/model/textModel.js';13import { CodeLens, Command } from '../../../common/languages.js';14import { CodeLensItem } from './codelens.js';1516class CodeLensViewZone implements IViewZone {1718readonly suppressMouseDown: boolean;19readonly domNode: HTMLElement;2021afterLineNumber: number;22/**23* We want that this view zone, which reserves space for a code lens appears24* as close as possible to the next line, so we use a very large value here.25*/26readonly afterColumn = Constants.MAX_SAFE_SMALL_INTEGER;27heightInPx: number;2829private _lastHeight?: number;30private readonly _onHeight: () => void;3132constructor(afterLineNumber: number, heightInPx: number, onHeight: () => void) {33this.afterLineNumber = afterLineNumber;34this.heightInPx = heightInPx;3536this._onHeight = onHeight;37this.suppressMouseDown = true;38this.domNode = document.createElement('div');39}4041onComputedHeight(height: number): void {42if (this._lastHeight === undefined) {43this._lastHeight = height;44} else if (this._lastHeight !== height) {45this._lastHeight = height;46this._onHeight();47}48}4950isVisible(): boolean {51return this._lastHeight !== 052&& this.domNode.hasAttribute('monaco-visible-view-zone');53}54}5556class CodeLensContentWidget implements IContentWidget {5758private static _idPool: number = 0;5960// Editor.IContentWidget.allowEditorOverflow61readonly allowEditorOverflow: boolean = false;62readonly suppressMouseDown: boolean = true;6364private readonly _id: string;65private readonly _domNode: HTMLElement;66private readonly _editor: IActiveCodeEditor;67private readonly _commands = new Map<string, Command>();6869private _widgetPosition?: IContentWidgetPosition;70private _isEmpty: boolean = true;7172constructor(73editor: IActiveCodeEditor,74line: number,75) {76this._editor = editor;77this._id = `codelens.widget-${(CodeLensContentWidget._idPool++)}`;7879this.updatePosition(line);8081this._domNode = document.createElement('span');82this._domNode.className = `codelens-decoration`;83}8485withCommands(lenses: ReadonlyArray<CodeLens | undefined | null>, animate: boolean): void {86this._commands.clear();8788const children: HTMLElement[] = [];89let hasSymbol = false;90for (let i = 0; i < lenses.length; i++) {91const lens = lenses[i];92if (!lens) {93continue;94}95hasSymbol = true;96if (lens.command) {97const title = renderLabelWithIcons(lens.command.title.trim());98if (lens.command.id) {99const id = `c${(CodeLensContentWidget._idPool++)}`;100children.push(dom.$('a', { id, title: lens.command.tooltip, role: 'button' }, ...title));101this._commands.set(id, lens.command);102} else {103children.push(dom.$('span', { title: lens.command.tooltip }, ...title));104}105if (i + 1 < lenses.length) {106children.push(dom.$('span', undefined, '\u00a0|\u00a0'));107}108}109}110111if (!hasSymbol) {112// symbols but no commands113dom.reset(this._domNode, dom.$('span', undefined, 'no commands'));114115} else {116// symbols and commands117dom.reset(this._domNode, ...children);118if (this._isEmpty && animate) {119this._domNode.classList.add('fadein');120}121this._isEmpty = false;122}123}124125getCommand(link: HTMLLinkElement): Command | undefined {126return link.parentElement === this._domNode127? this._commands.get(link.id)128: undefined;129}130131getId(): string {132return this._id;133}134135getDomNode(): HTMLElement {136return this._domNode;137}138139updatePosition(line: number): void {140const column = this._editor.getModel().getLineFirstNonWhitespaceColumn(line);141this._widgetPosition = {142position: { lineNumber: line, column: column },143preference: [ContentWidgetPositionPreference.ABOVE]144};145}146147getPosition(): IContentWidgetPosition | null {148return this._widgetPosition || null;149}150}151152export interface IDecorationIdCallback {153(decorationId: string): void;154}155156export class CodeLensHelper {157158private readonly _removeDecorations: string[];159private readonly _addDecorations: IModelDeltaDecoration[];160private readonly _addDecorationsCallbacks: IDecorationIdCallback[];161162constructor() {163this._removeDecorations = [];164this._addDecorations = [];165this._addDecorationsCallbacks = [];166}167168addDecoration(decoration: IModelDeltaDecoration, callback: IDecorationIdCallback): void {169this._addDecorations.push(decoration);170this._addDecorationsCallbacks.push(callback);171}172173removeDecoration(decorationId: string): void {174this._removeDecorations.push(decorationId);175}176177commit(changeAccessor: IModelDecorationsChangeAccessor): void {178const resultingDecorations = changeAccessor.deltaDecorations(this._removeDecorations, this._addDecorations);179for (let i = 0, len = resultingDecorations.length; i < len; i++) {180this._addDecorationsCallbacks[i](resultingDecorations[i]);181}182}183}184185const codeLensDecorationOptions = ModelDecorationOptions.register({186collapseOnReplaceEdit: true,187description: 'codelens'188});189190export class CodeLensWidget {191192private readonly _editor: IActiveCodeEditor;193private readonly _viewZone: CodeLensViewZone;194private readonly _viewZoneId: string;195196private _contentWidget?: CodeLensContentWidget;197private _decorationIds: string[];198private _data: readonly CodeLensItem[];199private _isDisposed: boolean = false;200201constructor(202data: readonly CodeLensItem[],203editor: IActiveCodeEditor,204helper: CodeLensHelper,205viewZoneChangeAccessor: IViewZoneChangeAccessor,206heightInPx: number,207updateCallback: () => void208) {209this._editor = editor;210this._data = data;211212// create combined range, track all ranges with decorations,213// check if there is already something to render214this._decorationIds = [];215let range: Range | undefined;216const lenses: CodeLens[] = [];217218this._data.forEach((codeLensData, i) => {219220if (codeLensData.symbol.command) {221lenses.push(codeLensData.symbol);222}223224helper.addDecoration({225range: codeLensData.symbol.range,226options: codeLensDecorationOptions227}, id => this._decorationIds[i] = id);228229// the range contains all lenses on this line230if (!range) {231range = Range.lift(codeLensData.symbol.range);232} else {233range = Range.plusRange(range, codeLensData.symbol.range);234}235});236237this._viewZone = new CodeLensViewZone(range!.startLineNumber - 1, heightInPx, updateCallback);238this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone);239240if (lenses.length > 0) {241this._createContentWidgetIfNecessary();242this._contentWidget!.withCommands(lenses, false);243}244}245246private _createContentWidgetIfNecessary(): void {247if (!this._contentWidget) {248this._contentWidget = new CodeLensContentWidget(this._editor, this._viewZone.afterLineNumber + 1);249this._editor.addContentWidget(this._contentWidget);250} else {251this._editor.layoutContentWidget(this._contentWidget);252}253}254255dispose(helper: CodeLensHelper, viewZoneChangeAccessor?: IViewZoneChangeAccessor): void {256this._decorationIds.forEach(helper.removeDecoration, helper);257this._decorationIds = [];258viewZoneChangeAccessor?.removeZone(this._viewZoneId);259if (this._contentWidget) {260this._editor.removeContentWidget(this._contentWidget);261this._contentWidget = undefined;262}263this._isDisposed = true;264}265266isDisposed(): boolean {267return this._isDisposed;268}269270isValid(): boolean {271return this._decorationIds.some((id, i) => {272const range = this._editor.getModel().getDecorationRange(id);273const symbol = this._data[i].symbol;274return !!(range && Range.isEmpty(symbol.range) === range.isEmpty());275});276}277278updateCodeLensSymbols(data: readonly CodeLensItem[], helper: CodeLensHelper): void {279this._decorationIds.forEach(helper.removeDecoration, helper);280this._decorationIds = [];281this._data = data;282this._data.forEach((codeLensData, i) => {283helper.addDecoration({284range: codeLensData.symbol.range,285options: codeLensDecorationOptions286}, id => this._decorationIds[i] = id);287});288}289290updateHeight(height: number, viewZoneChangeAccessor: IViewZoneChangeAccessor): void {291this._viewZone.heightInPx = height;292viewZoneChangeAccessor.layoutZone(this._viewZoneId);293if (this._contentWidget) {294this._editor.layoutContentWidget(this._contentWidget);295}296}297298computeIfNecessary(model: ITextModel): readonly CodeLensItem[] | null {299if (!this._viewZone.isVisible()) {300return null;301}302303// Read editor current state304for (let i = 0; i < this._decorationIds.length; i++) {305const range = model.getDecorationRange(this._decorationIds[i]);306if (range) {307this._data[i].symbol.range = range;308}309}310return this._data;311}312313updateCommands(symbols: ReadonlyArray<CodeLens | undefined | null>): void {314this._createContentWidgetIfNecessary();315this._contentWidget!.withCommands(symbols, true);316317for (let i = 0; i < this._data.length; i++) {318const resolved = symbols[i];319if (resolved) {320const { symbol } = this._data[i];321symbol.command = resolved.command || symbol.command;322}323}324}325326getCommand(link: HTMLLinkElement): Command | undefined {327return this._contentWidget?.getCommand(link);328}329330getLineNumber(): number {331const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);332if (range) {333return range.startLineNumber;334}335return -1;336}337338update(viewZoneChangeAccessor: IViewZoneChangeAccessor): void {339if (this.isValid()) {340const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);341if (range) {342this._viewZone.afterLineNumber = range.startLineNumber - 1;343viewZoneChangeAccessor.layoutZone(this._viewZoneId);344345if (this._contentWidget) {346this._contentWidget.updatePosition(range.startLineNumber);347this._editor.layoutContentWidget(this._contentWidget);348}349}350}351}352353getItems(): readonly CodeLensItem[] {354return this._data;355}356}357358359