Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/selections/selections.ts
3296 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 './selections.css';
7
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
8
import { Range } from '../../../common/core/range.js';
9
import { HorizontalRange, LineVisibleRanges, RenderingContext } from '../../view/renderingContext.js';
10
import { ViewContext } from '../../../common/viewModel/viewContext.js';
11
import * as viewEvents from '../../../common/viewEvents.js';
12
import { editorSelectionForeground } from '../../../../platform/theme/common/colorRegistry.js';
13
import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
14
import { EditorOption } from '../../../common/config/editorOptions.js';
15
16
const enum CornerStyle {
17
EXTERN,
18
INTERN,
19
FLAT
20
}
21
22
interface IVisibleRangeEndPointStyle {
23
top: CornerStyle;
24
bottom: CornerStyle;
25
}
26
27
class HorizontalRangeWithStyle {
28
public left: number;
29
public width: number;
30
public startStyle: IVisibleRangeEndPointStyle | null;
31
public endStyle: IVisibleRangeEndPointStyle | null;
32
33
constructor(other: HorizontalRange) {
34
this.left = other.left;
35
this.width = other.width;
36
this.startStyle = null;
37
this.endStyle = null;
38
}
39
}
40
41
class LineVisibleRangesWithStyle {
42
public lineNumber: number;
43
public ranges: HorizontalRangeWithStyle[];
44
45
constructor(lineNumber: number, ranges: HorizontalRangeWithStyle[]) {
46
this.lineNumber = lineNumber;
47
this.ranges = ranges;
48
}
49
}
50
51
function toStyledRange(item: HorizontalRange): HorizontalRangeWithStyle {
52
return new HorizontalRangeWithStyle(item);
53
}
54
55
function toStyled(item: LineVisibleRanges): LineVisibleRangesWithStyle {
56
return new LineVisibleRangesWithStyle(item.lineNumber, item.ranges.map(toStyledRange));
57
}
58
59
/**
60
* This view part displays selected text to the user. Every line has its own selection overlay.
61
*/
62
export class SelectionsOverlay extends DynamicViewOverlay {
63
64
private static readonly SELECTION_CLASS_NAME = 'selected-text';
65
private static readonly SELECTION_TOP_LEFT = 'top-left-radius';
66
private static readonly SELECTION_BOTTOM_LEFT = 'bottom-left-radius';
67
private static readonly SELECTION_TOP_RIGHT = 'top-right-radius';
68
private static readonly SELECTION_BOTTOM_RIGHT = 'bottom-right-radius';
69
private static readonly EDITOR_BACKGROUND_CLASS_NAME = 'monaco-editor-background';
70
71
private static readonly ROUNDED_PIECE_WIDTH = 10;
72
73
private readonly _context: ViewContext;
74
private _roundedSelection: boolean;
75
private _typicalHalfwidthCharacterWidth: number;
76
private _selections: Range[];
77
private _renderResult: string[] | null;
78
79
constructor(context: ViewContext) {
80
super();
81
this._context = context;
82
const options = this._context.configuration.options;
83
this._roundedSelection = options.get(EditorOption.roundedSelection);
84
this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;
85
this._selections = [];
86
this._renderResult = null;
87
this._context.addEventHandler(this);
88
}
89
90
public override dispose(): void {
91
this._context.removeEventHandler(this);
92
this._renderResult = null;
93
super.dispose();
94
}
95
96
// --- begin event handlers
97
98
public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
99
const options = this._context.configuration.options;
100
this._roundedSelection = options.get(EditorOption.roundedSelection);
101
this._typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;
102
return true;
103
}
104
public override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean {
105
this._selections = e.selections.slice(0);
106
return true;
107
}
108
public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
109
// true for inline decorations that can end up relayouting text
110
return true;//e.inlineDecorationsChanged;
111
}
112
public override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {
113
return true;
114
}
115
public override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean {
116
return true;
117
}
118
public override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {
119
return true;
120
}
121
public override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean {
122
return true;
123
}
124
public override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean {
125
return e.scrollTopChanged;
126
}
127
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
128
return true;
129
}
130
131
// --- end event handlers
132
133
private _visibleRangesHaveGaps(linesVisibleRanges: LineVisibleRangesWithStyle[]): boolean {
134
135
for (let i = 0, len = linesVisibleRanges.length; i < len; i++) {
136
const lineVisibleRanges = linesVisibleRanges[i];
137
138
if (lineVisibleRanges.ranges.length > 1) {
139
// There are two ranges on the same line
140
return true;
141
}
142
}
143
144
return false;
145
}
146
147
private _enrichVisibleRangesWithStyle(viewport: Range, linesVisibleRanges: LineVisibleRangesWithStyle[], previousFrame: LineVisibleRangesWithStyle[] | null): void {
148
const epsilon = this._typicalHalfwidthCharacterWidth / 4;
149
let previousFrameTop: HorizontalRangeWithStyle | null = null;
150
let previousFrameBottom: HorizontalRangeWithStyle | null = null;
151
152
if (previousFrame && previousFrame.length > 0 && linesVisibleRanges.length > 0) {
153
154
const topLineNumber = linesVisibleRanges[0].lineNumber;
155
if (topLineNumber === viewport.startLineNumber) {
156
for (let i = 0; !previousFrameTop && i < previousFrame.length; i++) {
157
if (previousFrame[i].lineNumber === topLineNumber) {
158
previousFrameTop = previousFrame[i].ranges[0];
159
}
160
}
161
}
162
163
const bottomLineNumber = linesVisibleRanges[linesVisibleRanges.length - 1].lineNumber;
164
if (bottomLineNumber === viewport.endLineNumber) {
165
for (let i = previousFrame.length - 1; !previousFrameBottom && i >= 0; i--) {
166
if (previousFrame[i].lineNumber === bottomLineNumber) {
167
previousFrameBottom = previousFrame[i].ranges[0];
168
}
169
}
170
}
171
172
if (previousFrameTop && !previousFrameTop.startStyle) {
173
previousFrameTop = null;
174
}
175
if (previousFrameBottom && !previousFrameBottom.startStyle) {
176
previousFrameBottom = null;
177
}
178
}
179
180
for (let i = 0, len = linesVisibleRanges.length; i < len; i++) {
181
// We know for a fact that there is precisely one range on each line
182
const curLineRange = linesVisibleRanges[i].ranges[0];
183
const curLeft = curLineRange.left;
184
const curRight = curLineRange.left + curLineRange.width;
185
186
const startStyle = {
187
top: CornerStyle.EXTERN,
188
bottom: CornerStyle.EXTERN
189
};
190
191
const endStyle = {
192
top: CornerStyle.EXTERN,
193
bottom: CornerStyle.EXTERN
194
};
195
196
if (i > 0) {
197
// Look above
198
const prevLeft = linesVisibleRanges[i - 1].ranges[0].left;
199
const prevRight = linesVisibleRanges[i - 1].ranges[0].left + linesVisibleRanges[i - 1].ranges[0].width;
200
201
if (abs(curLeft - prevLeft) < epsilon) {
202
startStyle.top = CornerStyle.FLAT;
203
} else if (curLeft > prevLeft) {
204
startStyle.top = CornerStyle.INTERN;
205
}
206
207
if (abs(curRight - prevRight) < epsilon) {
208
endStyle.top = CornerStyle.FLAT;
209
} else if (prevLeft < curRight && curRight < prevRight) {
210
endStyle.top = CornerStyle.INTERN;
211
}
212
} else if (previousFrameTop) {
213
// Accept some hiccups near the viewport edges to save on repaints
214
startStyle.top = previousFrameTop.startStyle!.top;
215
endStyle.top = previousFrameTop.endStyle!.top;
216
}
217
218
if (i + 1 < len) {
219
// Look below
220
const nextLeft = linesVisibleRanges[i + 1].ranges[0].left;
221
const nextRight = linesVisibleRanges[i + 1].ranges[0].left + linesVisibleRanges[i + 1].ranges[0].width;
222
223
if (abs(curLeft - nextLeft) < epsilon) {
224
startStyle.bottom = CornerStyle.FLAT;
225
} else if (nextLeft < curLeft && curLeft < nextRight) {
226
startStyle.bottom = CornerStyle.INTERN;
227
}
228
229
if (abs(curRight - nextRight) < epsilon) {
230
endStyle.bottom = CornerStyle.FLAT;
231
} else if (curRight < nextRight) {
232
endStyle.bottom = CornerStyle.INTERN;
233
}
234
} else if (previousFrameBottom) {
235
// Accept some hiccups near the viewport edges to save on repaints
236
startStyle.bottom = previousFrameBottom.startStyle!.bottom;
237
endStyle.bottom = previousFrameBottom.endStyle!.bottom;
238
}
239
240
curLineRange.startStyle = startStyle;
241
curLineRange.endStyle = endStyle;
242
}
243
}
244
245
private _getVisibleRangesWithStyle(selection: Range, ctx: RenderingContext, previousFrame: LineVisibleRangesWithStyle[] | null): LineVisibleRangesWithStyle[] {
246
const _linesVisibleRanges = ctx.linesVisibleRangesForRange(selection, true) || [];
247
const linesVisibleRanges = _linesVisibleRanges.map(toStyled);
248
const visibleRangesHaveGaps = this._visibleRangesHaveGaps(linesVisibleRanges);
249
250
if (!visibleRangesHaveGaps && this._roundedSelection) {
251
this._enrichVisibleRangesWithStyle(ctx.visibleRange, linesVisibleRanges, previousFrame);
252
}
253
254
// The visible ranges are sorted TOP-BOTTOM and LEFT-RIGHT
255
return linesVisibleRanges;
256
}
257
258
private _createSelectionPiece(top: number, bottom: number, className: string, left: number, width: number): string {
259
return (
260
'<div class="cslr '
261
+ className
262
+ '" style="'
263
+ 'top:' + top.toString() + 'px;'
264
+ 'bottom:' + bottom.toString() + 'px;'
265
+ 'left:' + left.toString() + 'px;'
266
+ 'width:' + width.toString() + 'px;'
267
+ '"></div>'
268
);
269
}
270
271
private _actualRenderOneSelection(output2: [string, string][], visibleStartLineNumber: number, hasMultipleSelections: boolean, visibleRanges: LineVisibleRangesWithStyle[]): void {
272
if (visibleRanges.length === 0) {
273
return;
274
}
275
276
const visibleRangesHaveStyle = !!visibleRanges[0].ranges[0].startStyle;
277
278
const firstLineNumber = visibleRanges[0].lineNumber;
279
const lastLineNumber = visibleRanges[visibleRanges.length - 1].lineNumber;
280
281
for (let i = 0, len = visibleRanges.length; i < len; i++) {
282
const lineVisibleRanges = visibleRanges[i];
283
const lineNumber = lineVisibleRanges.lineNumber;
284
const lineIndex = lineNumber - visibleStartLineNumber;
285
286
const top = hasMultipleSelections ? (lineNumber === firstLineNumber ? 1 : 0) : 0;
287
const bottom = hasMultipleSelections ? (lineNumber !== firstLineNumber && lineNumber === lastLineNumber ? 1 : 0) : 0;
288
289
let innerCornerOutput = '';
290
let restOfSelectionOutput = '';
291
292
for (let j = 0, lenJ = lineVisibleRanges.ranges.length; j < lenJ; j++) {
293
const visibleRange = lineVisibleRanges.ranges[j];
294
295
if (visibleRangesHaveStyle) {
296
const startStyle = visibleRange.startStyle!;
297
const endStyle = visibleRange.endStyle!;
298
if (startStyle.top === CornerStyle.INTERN || startStyle.bottom === CornerStyle.INTERN) {
299
// Reverse rounded corner to the left
300
301
// First comes the selection (blue layer)
302
innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
303
304
// Second comes the background (white layer) with inverse border radius
305
let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME;
306
if (startStyle.top === CornerStyle.INTERN) {
307
className += ' ' + SelectionsOverlay.SELECTION_TOP_RIGHT;
308
}
309
if (startStyle.bottom === CornerStyle.INTERN) {
310
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT;
311
}
312
innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left - SelectionsOverlay.ROUNDED_PIECE_WIDTH, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
313
}
314
if (endStyle.top === CornerStyle.INTERN || endStyle.bottom === CornerStyle.INTERN) {
315
// Reverse rounded corner to the right
316
317
// First comes the selection (blue layer)
318
innerCornerOutput += this._createSelectionPiece(top, bottom, SelectionsOverlay.SELECTION_CLASS_NAME, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
319
320
// Second comes the background (white layer) with inverse border radius
321
let className = SelectionsOverlay.EDITOR_BACKGROUND_CLASS_NAME;
322
if (endStyle.top === CornerStyle.INTERN) {
323
className += ' ' + SelectionsOverlay.SELECTION_TOP_LEFT;
324
}
325
if (endStyle.bottom === CornerStyle.INTERN) {
326
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT;
327
}
328
innerCornerOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left + visibleRange.width, SelectionsOverlay.ROUNDED_PIECE_WIDTH);
329
}
330
}
331
332
let className = SelectionsOverlay.SELECTION_CLASS_NAME;
333
if (visibleRangesHaveStyle) {
334
const startStyle = visibleRange.startStyle!;
335
const endStyle = visibleRange.endStyle!;
336
if (startStyle.top === CornerStyle.EXTERN) {
337
className += ' ' + SelectionsOverlay.SELECTION_TOP_LEFT;
338
}
339
if (startStyle.bottom === CornerStyle.EXTERN) {
340
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_LEFT;
341
}
342
if (endStyle.top === CornerStyle.EXTERN) {
343
className += ' ' + SelectionsOverlay.SELECTION_TOP_RIGHT;
344
}
345
if (endStyle.bottom === CornerStyle.EXTERN) {
346
className += ' ' + SelectionsOverlay.SELECTION_BOTTOM_RIGHT;
347
}
348
}
349
restOfSelectionOutput += this._createSelectionPiece(top, bottom, className, visibleRange.left, visibleRange.width);
350
}
351
352
output2[lineIndex][0] += innerCornerOutput;
353
output2[lineIndex][1] += restOfSelectionOutput;
354
}
355
}
356
357
private _previousFrameVisibleRangesWithStyle: (LineVisibleRangesWithStyle[] | null)[] = [];
358
public prepareRender(ctx: RenderingContext): void {
359
360
// Build HTML for inner corners separate from HTML for the rest of selections,
361
// as the inner corner HTML can interfere with that of other selections.
362
// In final render, make sure to place the inner corner HTML before the rest of selection HTML. See issue #77777.
363
const output: [string, string][] = [];
364
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
365
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
366
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
367
const lineIndex = lineNumber - visibleStartLineNumber;
368
output[lineIndex] = ['', ''];
369
}
370
371
const thisFrameVisibleRangesWithStyle: (LineVisibleRangesWithStyle[] | null)[] = [];
372
for (let i = 0, len = this._selections.length; i < len; i++) {
373
const selection = this._selections[i];
374
if (selection.isEmpty()) {
375
thisFrameVisibleRangesWithStyle[i] = null;
376
continue;
377
}
378
379
const visibleRangesWithStyle = this._getVisibleRangesWithStyle(selection, ctx, this._previousFrameVisibleRangesWithStyle[i]);
380
thisFrameVisibleRangesWithStyle[i] = visibleRangesWithStyle;
381
this._actualRenderOneSelection(output, visibleStartLineNumber, this._selections.length > 1, visibleRangesWithStyle);
382
}
383
384
this._previousFrameVisibleRangesWithStyle = thisFrameVisibleRangesWithStyle;
385
this._renderResult = output.map(([internalCorners, restOfSelection]) => internalCorners + restOfSelection);
386
}
387
388
public render(startLineNumber: number, lineNumber: number): string {
389
if (!this._renderResult) {
390
return '';
391
}
392
const lineIndex = lineNumber - startLineNumber;
393
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
394
return '';
395
}
396
return this._renderResult[lineIndex];
397
}
398
}
399
400
registerThemingParticipant((theme, collector) => {
401
const editorSelectionForegroundColor = theme.getColor(editorSelectionForeground);
402
if (editorSelectionForegroundColor && !editorSelectionForegroundColor.isTransparent()) {
403
collector.addRule(`.monaco-editor .view-line span.inline-selected-text { color: ${editorSelectionForegroundColor}; }`);
404
}
405
});
406
407
function abs(n: number): number {
408
return n < 0 ? -n : n;
409
}
410
411