Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/codelens/browser/codelensWidget.ts
5251 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
import * as dom from '../../../../base/browser/dom.js';
7
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
8
import { Constants } from '../../../../base/common/uint.js';
9
import './codelensWidget.css';
10
import { ContentWidgetPositionPreference, IActiveCodeEditor, IContentWidget, IContentWidgetPosition, IViewZone, IViewZoneChangeAccessor } from '../../../browser/editorBrowser.js';
11
import { Range } from '../../../common/core/range.js';
12
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from '../../../common/model.js';
13
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
14
import { CodeLens, Command } from '../../../common/languages.js';
15
import { CodeLensItem } from './codelens.js';
16
17
class CodeLensViewZone implements IViewZone {
18
19
readonly suppressMouseDown: boolean;
20
readonly domNode: HTMLElement;
21
22
afterLineNumber: number;
23
/**
24
* We want that this view zone, which reserves space for a code lens appears
25
* as close as possible to the next line, so we use a very large value here.
26
*/
27
readonly afterColumn = Constants.MAX_SAFE_SMALL_INTEGER;
28
heightInPx: number;
29
30
private _lastHeight?: number;
31
private readonly _onHeight: () => void;
32
33
constructor(afterLineNumber: number, heightInPx: number, onHeight: () => void) {
34
this.afterLineNumber = afterLineNumber;
35
this.heightInPx = heightInPx;
36
37
this._onHeight = onHeight;
38
this.suppressMouseDown = true;
39
this.domNode = document.createElement('div');
40
}
41
42
onComputedHeight(height: number): void {
43
if (this._lastHeight === undefined) {
44
this._lastHeight = height;
45
} else if (this._lastHeight !== height) {
46
this._lastHeight = height;
47
this._onHeight();
48
}
49
}
50
51
isVisible(): boolean {
52
return this._lastHeight !== 0
53
&& this.domNode.hasAttribute('monaco-visible-view-zone');
54
}
55
}
56
57
class CodeLensContentWidget implements IContentWidget {
58
59
private static _idPool: number = 0;
60
61
// Editor.IContentWidget.allowEditorOverflow
62
readonly allowEditorOverflow: boolean = false;
63
readonly suppressMouseDown: boolean = true;
64
65
private readonly _id: string;
66
private readonly _domNode: HTMLElement;
67
private readonly _editor: IActiveCodeEditor;
68
private readonly _commands = new Map<string, Command>();
69
70
private _widgetPosition?: IContentWidgetPosition;
71
private _isEmpty: boolean = true;
72
73
constructor(
74
editor: IActiveCodeEditor,
75
line: number,
76
) {
77
this._editor = editor;
78
this._id = `codelens.widget-${(CodeLensContentWidget._idPool++)}`;
79
80
this.updatePosition(line);
81
82
this._domNode = document.createElement('span');
83
this._domNode.className = `codelens-decoration`;
84
}
85
86
withCommands(lenses: ReadonlyArray<CodeLens | undefined | null>, animate: boolean): void {
87
this._commands.clear();
88
89
const children: HTMLElement[] = [];
90
let hasSymbol = false;
91
for (let i = 0; i < lenses.length; i++) {
92
const lens = lenses[i];
93
if (!lens) {
94
continue;
95
}
96
hasSymbol = true;
97
if (lens.command) {
98
const title = renderLabelWithIcons(lens.command.title.trim());
99
if (lens.command.id) {
100
const id = `c${(CodeLensContentWidget._idPool++)}`;
101
children.push(dom.$('a', { id, title: lens.command.tooltip, role: 'button' }, ...title));
102
this._commands.set(id, lens.command);
103
} else {
104
children.push(dom.$('span', { title: lens.command.tooltip }, ...title));
105
}
106
if (i + 1 < lenses.length) {
107
children.push(dom.$('span', undefined, '\u00a0|\u00a0'));
108
}
109
}
110
}
111
112
if (!hasSymbol) {
113
// symbols but no commands
114
dom.reset(this._domNode, dom.$('span', undefined, 'no commands'));
115
116
} else {
117
// symbols and commands
118
dom.reset(this._domNode, ...children);
119
if (this._isEmpty && animate) {
120
this._domNode.classList.add('fadein');
121
}
122
this._isEmpty = false;
123
}
124
}
125
126
getCommand(link: HTMLLinkElement): Command | undefined {
127
return link.parentElement === this._domNode
128
? this._commands.get(link.id)
129
: undefined;
130
}
131
132
getId(): string {
133
return this._id;
134
}
135
136
getDomNode(): HTMLElement {
137
return this._domNode;
138
}
139
140
updatePosition(line: number): void {
141
const column = this._editor.getModel().getLineFirstNonWhitespaceColumn(line);
142
this._widgetPosition = {
143
position: { lineNumber: line, column: column },
144
preference: [ContentWidgetPositionPreference.ABOVE]
145
};
146
}
147
148
getPosition(): IContentWidgetPosition | null {
149
return this._widgetPosition || null;
150
}
151
}
152
153
export interface IDecorationIdCallback {
154
(decorationId: string): void;
155
}
156
157
export class CodeLensHelper {
158
159
private readonly _removeDecorations: string[];
160
private readonly _addDecorations: IModelDeltaDecoration[];
161
private readonly _addDecorationsCallbacks: IDecorationIdCallback[];
162
163
constructor() {
164
this._removeDecorations = [];
165
this._addDecorations = [];
166
this._addDecorationsCallbacks = [];
167
}
168
169
addDecoration(decoration: IModelDeltaDecoration, callback: IDecorationIdCallback): void {
170
this._addDecorations.push(decoration);
171
this._addDecorationsCallbacks.push(callback);
172
}
173
174
removeDecoration(decorationId: string): void {
175
this._removeDecorations.push(decorationId);
176
}
177
178
commit(changeAccessor: IModelDecorationsChangeAccessor): void {
179
const resultingDecorations = changeAccessor.deltaDecorations(this._removeDecorations, this._addDecorations);
180
for (let i = 0, len = resultingDecorations.length; i < len; i++) {
181
this._addDecorationsCallbacks[i](resultingDecorations[i]);
182
}
183
}
184
}
185
186
const codeLensDecorationOptions = ModelDecorationOptions.register({
187
collapseOnReplaceEdit: true,
188
description: 'codelens'
189
});
190
191
export class CodeLensWidget {
192
193
private readonly _editor: IActiveCodeEditor;
194
private readonly _viewZone: CodeLensViewZone;
195
private readonly _viewZoneId: string;
196
197
private _contentWidget?: CodeLensContentWidget;
198
private _decorationIds: string[];
199
private _data: readonly CodeLensItem[];
200
private _isDisposed: boolean = false;
201
202
constructor(
203
data: readonly CodeLensItem[],
204
editor: IActiveCodeEditor,
205
helper: CodeLensHelper,
206
viewZoneChangeAccessor: IViewZoneChangeAccessor,
207
heightInPx: number,
208
updateCallback: () => void
209
) {
210
this._editor = editor;
211
this._data = data;
212
213
// create combined range, track all ranges with decorations,
214
// check if there is already something to render
215
this._decorationIds = [];
216
let range: Range | undefined;
217
const lenses: CodeLens[] = [];
218
219
this._data.forEach((codeLensData, i) => {
220
221
if (codeLensData.symbol.command) {
222
lenses.push(codeLensData.symbol);
223
}
224
225
helper.addDecoration({
226
range: codeLensData.symbol.range,
227
options: codeLensDecorationOptions
228
}, id => this._decorationIds[i] = id);
229
230
// the range contains all lenses on this line
231
if (!range) {
232
range = Range.lift(codeLensData.symbol.range);
233
} else {
234
range = Range.plusRange(range, codeLensData.symbol.range);
235
}
236
});
237
238
this._viewZone = new CodeLensViewZone(range!.startLineNumber - 1, heightInPx, updateCallback);
239
this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone);
240
241
if (lenses.length > 0) {
242
this._createContentWidgetIfNecessary();
243
this._contentWidget!.withCommands(lenses, false);
244
}
245
}
246
247
private _createContentWidgetIfNecessary(): void {
248
if (!this._contentWidget) {
249
this._contentWidget = new CodeLensContentWidget(this._editor, this._viewZone.afterLineNumber + 1);
250
this._editor.addContentWidget(this._contentWidget);
251
} else {
252
this._editor.layoutContentWidget(this._contentWidget);
253
}
254
}
255
256
dispose(helper: CodeLensHelper, viewZoneChangeAccessor?: IViewZoneChangeAccessor): void {
257
this._decorationIds.forEach(helper.removeDecoration, helper);
258
this._decorationIds = [];
259
viewZoneChangeAccessor?.removeZone(this._viewZoneId);
260
if (this._contentWidget) {
261
this._editor.removeContentWidget(this._contentWidget);
262
this._contentWidget = undefined;
263
}
264
this._isDisposed = true;
265
}
266
267
isDisposed(): boolean {
268
return this._isDisposed;
269
}
270
271
isValid(): boolean {
272
return this._decorationIds.some((id, i) => {
273
const range = this._editor.getModel().getDecorationRange(id);
274
const symbol = this._data[i].symbol;
275
return !!(range && Range.isEmpty(symbol.range) === range.isEmpty());
276
});
277
}
278
279
updateCodeLensSymbols(data: readonly CodeLensItem[], helper: CodeLensHelper): void {
280
this._decorationIds.forEach(helper.removeDecoration, helper);
281
this._decorationIds = [];
282
this._data = data;
283
this._data.forEach((codeLensData, i) => {
284
helper.addDecoration({
285
range: codeLensData.symbol.range,
286
options: codeLensDecorationOptions
287
}, id => this._decorationIds[i] = id);
288
});
289
}
290
291
updateHeight(height: number, viewZoneChangeAccessor: IViewZoneChangeAccessor): void {
292
this._viewZone.heightInPx = height;
293
viewZoneChangeAccessor.layoutZone(this._viewZoneId);
294
if (this._contentWidget) {
295
this._editor.layoutContentWidget(this._contentWidget);
296
}
297
}
298
299
computeIfNecessary(model: ITextModel): readonly CodeLensItem[] | null {
300
if (!this._viewZone.isVisible()) {
301
return null;
302
}
303
304
// Read editor current state
305
for (let i = 0; i < this._decorationIds.length; i++) {
306
const range = model.getDecorationRange(this._decorationIds[i]);
307
if (range) {
308
this._data[i].symbol.range = range;
309
}
310
}
311
return this._data;
312
}
313
314
updateCommands(symbols: ReadonlyArray<CodeLens | undefined | null>): void {
315
this._createContentWidgetIfNecessary();
316
this._contentWidget!.withCommands(symbols, true);
317
318
for (let i = 0; i < this._data.length; i++) {
319
const resolved = symbols[i];
320
if (resolved) {
321
const { symbol } = this._data[i];
322
symbol.command = resolved.command || symbol.command;
323
}
324
}
325
}
326
327
getCommand(link: HTMLLinkElement): Command | undefined {
328
return this._contentWidget?.getCommand(link);
329
}
330
331
getLineNumber(): number {
332
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
333
if (range) {
334
return range.startLineNumber;
335
}
336
return -1;
337
}
338
339
update(viewZoneChangeAccessor: IViewZoneChangeAccessor): void {
340
if (this.isValid()) {
341
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
342
if (range) {
343
this._viewZone.afterLineNumber = range.startLineNumber - 1;
344
viewZoneChangeAccessor.layoutZone(this._viewZoneId);
345
346
if (this._contentWidget) {
347
this._contentWidget.updatePosition(range.startLineNumber);
348
this._editor.layoutContentWidget(this._contentWidget);
349
}
350
}
351
}
352
}
353
354
getItems(): readonly CodeLensItem[] {
355
return this._data;
356
}
357
}
358
359