Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.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 { getActiveWindow } from '../../../../base/browser/dom.js';
7
import { BugIndicatingError } from '../../../../base/common/errors.js';
8
import { autorun, runOnChange } from '../../../../base/common/observable.js';
9
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
10
import { ILogService } from '../../../../platform/log/common/log.js';
11
import { EditorOption } from '../../../common/config/editorOptions.js';
12
import { Position } from '../../../common/core/position.js';
13
import { Range } from '../../../common/core/range.js';
14
import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';
15
import type { ViewContext } from '../../../common/viewModel/viewContext.js';
16
import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js';
17
import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js';
18
import { GPULifecycle } from '../../gpu/gpuDisposable.js';
19
import { quadVertices } from '../../gpu/gpuUtils.js';
20
import { ViewGpuContext } from '../../gpu/viewGpuContext.js';
21
import { FloatHorizontalRange, HorizontalPosition, HorizontalRange, IViewLines, LineVisibleRanges, RenderingContext, RestrictedRenderingContext, VisibleRanges } from '../../view/renderingContext.js';
22
import { ViewPart } from '../../view/viewPart.js';
23
import { ViewLineOptions } from '../viewLines/viewLineOptions.js';
24
import type * as viewEvents from '../../../common/viewEvents.js';
25
import { CursorColumns } from '../../../common/core/cursorColumns.js';
26
import { TextureAtlas } from '../../gpu/atlas/textureAtlas.js';
27
import { createContentSegmenter, type IContentSegmenter } from '../../gpu/contentSegmenter.js';
28
import { ViewportRenderStrategy } from '../../gpu/renderStrategy/viewportRenderStrategy.js';
29
import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js';
30
import { MutableDisposable } from '../../../../base/common/lifecycle.js';
31
import type { ViewLineRenderingData } from '../../../common/viewModel.js';
32
import { GlyphRasterizer } from '../../gpu/raster/glyphRasterizer.js';
33
34
const enum GlyphStorageBufferInfo {
35
FloatsPerEntry = 2 + 2 + 2,
36
BytesPerEntry = GlyphStorageBufferInfo.FloatsPerEntry * 4,
37
Offset_TexturePosition = 0,
38
Offset_TextureSize = 2,
39
Offset_OriginPosition = 4,
40
}
41
42
/**
43
* The GPU implementation of the ViewLines part.
44
*/
45
export class ViewLinesGpu extends ViewPart implements IViewLines {
46
47
private readonly canvas: HTMLCanvasElement;
48
49
private _initViewportData?: ViewportData[];
50
private _lastViewportData?: ViewportData;
51
private _lastViewLineOptions?: ViewLineOptions;
52
53
private _device!: GPUDevice;
54
private _renderPassDescriptor!: GPURenderPassDescriptor;
55
private _renderPassColorAttachment!: GPURenderPassColorAttachment;
56
private _bindGroup!: GPUBindGroup;
57
private _pipeline!: GPURenderPipeline;
58
59
private _vertexBuffer!: GPUBuffer;
60
61
private _glyphStorageBuffer!: GPUBuffer;
62
private _atlasGpuTexture!: GPUTexture;
63
private readonly _atlasGpuTextureVersions: number[] = [];
64
65
private _initialized = false;
66
67
private readonly _glyphRasterizer: MutableDisposable<GlyphRasterizer> = this._register(new MutableDisposable());
68
private readonly _renderStrategy: MutableDisposable<IGpuRenderStrategy> = this._register(new MutableDisposable());
69
private _rebuildBindGroup?: () => void;
70
71
constructor(
72
context: ViewContext,
73
private readonly _viewGpuContext: ViewGpuContext,
74
@IInstantiationService private readonly _instantiationService: IInstantiationService,
75
@ILogService private readonly _logService: ILogService,
76
) {
77
super(context);
78
79
this.canvas = this._viewGpuContext.canvas.domNode;
80
81
// Re-render the following frame after canvas device pixel dimensions change, provided a
82
// new render does not occur.
83
this._register(autorun(reader => {
84
this._viewGpuContext.canvasDevicePixelDimensions.read(reader);
85
const lastViewportData = this._lastViewportData;
86
if (lastViewportData) {
87
setTimeout(() => {
88
if (lastViewportData === this._lastViewportData) {
89
this.renderText(lastViewportData);
90
}
91
});
92
}
93
}));
94
95
this.initWebgpu();
96
}
97
98
async initWebgpu() {
99
// #region General
100
101
this._device = ViewGpuContext.deviceSync || await ViewGpuContext.device;
102
103
if (this._store.isDisposed) {
104
return;
105
}
106
107
const atlas = ViewGpuContext.atlas;
108
109
// Rerender when the texture atlas deletes glyphs
110
this._register(atlas.onDidDeleteGlyphs(() => {
111
this._atlasGpuTextureVersions.length = 0;
112
this._atlasGpuTextureVersions[0] = 0;
113
this._atlasGpuTextureVersions[1] = 0;
114
this._renderStrategy.value!.reset();
115
}));
116
117
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
118
this._viewGpuContext.ctx.configure({
119
device: this._device,
120
format: presentationFormat,
121
alphaMode: 'premultiplied',
122
});
123
124
this._renderPassColorAttachment = {
125
view: null!, // Will be filled at render time
126
loadOp: 'load',
127
storeOp: 'store',
128
};
129
this._renderPassDescriptor = {
130
label: 'Monaco render pass',
131
colorAttachments: [this._renderPassColorAttachment],
132
};
133
134
// #endregion General
135
136
// #region Uniforms
137
138
let layoutInfoUniformBuffer: GPUBuffer;
139
{
140
const enum Info {
141
FloatsPerEntry = 6,
142
BytesPerEntry = Info.FloatsPerEntry * 4,
143
Offset_CanvasWidth____ = 0,
144
Offset_CanvasHeight___ = 1,
145
Offset_ViewportOffsetX = 2,
146
Offset_ViewportOffsetY = 3,
147
Offset_ViewportWidth__ = 4,
148
Offset_ViewportHeight_ = 5,
149
}
150
const bufferValues = new Float32Array(Info.FloatsPerEntry);
151
const updateBufferValues = (canvasDevicePixelWidth: number = this.canvas.width, canvasDevicePixelHeight: number = this.canvas.height) => {
152
bufferValues[Info.Offset_CanvasWidth____] = canvasDevicePixelWidth;
153
bufferValues[Info.Offset_CanvasHeight___] = canvasDevicePixelHeight;
154
bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio);
155
bufferValues[Info.Offset_ViewportOffsetY] = 0;
156
bufferValues[Info.Offset_ViewportWidth__] = bufferValues[Info.Offset_CanvasWidth____] - bufferValues[Info.Offset_ViewportOffsetX];
157
bufferValues[Info.Offset_ViewportHeight_] = bufferValues[Info.Offset_CanvasHeight___] - bufferValues[Info.Offset_ViewportOffsetY];
158
return bufferValues;
159
};
160
layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, {
161
label: 'Monaco uniform buffer',
162
size: Info.BytesPerEntry,
163
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
164
}, () => updateBufferValues())).object;
165
this._register(runOnChange(this._viewGpuContext.canvasDevicePixelDimensions, ({ width, height }) => {
166
this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(width, height));
167
}));
168
this._register(runOnChange(this._viewGpuContext.contentLeft, () => {
169
this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues());
170
}));
171
}
172
173
let atlasInfoUniformBuffer: GPUBuffer;
174
{
175
const enum Info {
176
FloatsPerEntry = 2,
177
BytesPerEntry = Info.FloatsPerEntry * 4,
178
Offset_Width_ = 0,
179
Offset_Height = 1,
180
}
181
atlasInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, {
182
label: 'Monaco atlas info uniform buffer',
183
size: Info.BytesPerEntry,
184
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
185
}, () => {
186
const values = new Float32Array(Info.FloatsPerEntry);
187
values[Info.Offset_Width_] = atlas.pageSize;
188
values[Info.Offset_Height] = atlas.pageSize;
189
return values;
190
})).object;
191
}
192
193
// #endregion Uniforms
194
195
// #region Storage buffers
196
197
const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily);
198
const fontSize = this._context.configuration.options.get(EditorOption.fontSize);
199
this._glyphRasterizer.value = this._register(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get(), ViewGpuContext.decorationStyleCache));
200
this._register(runOnChange(this._viewGpuContext.devicePixelRatio, () => {
201
this._refreshGlyphRasterizer();
202
}));
203
204
205
this._renderStrategy.value = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer });
206
// this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device);
207
208
this._glyphStorageBuffer = this._register(GPULifecycle.createBuffer(this._device, {
209
label: 'Monaco glyph storage buffer',
210
size: TextureAtlas.maximumPageCount * (TextureAtlasPage.maximumGlyphCount * GlyphStorageBufferInfo.BytesPerEntry),
211
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
212
})).object;
213
this._atlasGpuTextureVersions[0] = 0;
214
this._atlasGpuTextureVersions[1] = 0;
215
this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, {
216
label: 'Monaco atlas texture',
217
format: 'rgba8unorm',
218
size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: TextureAtlas.maximumPageCount },
219
dimension: '2d',
220
usage: GPUTextureUsage.TEXTURE_BINDING |
221
GPUTextureUsage.COPY_DST |
222
GPUTextureUsage.RENDER_ATTACHMENT,
223
})).object;
224
225
this._updateAtlasStorageBufferAndTexture();
226
227
// #endregion Storage buffers
228
229
// #region Vertex buffer
230
231
this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, {
232
label: 'Monaco vertex buffer',
233
size: quadVertices.byteLength,
234
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
235
}, quadVertices)).object;
236
237
// #endregion Vertex buffer
238
239
// #region Shader module
240
241
const module = this._device.createShaderModule({
242
label: 'Monaco shader module',
243
code: this._renderStrategy.value!.wgsl,
244
});
245
246
// #endregion Shader module
247
248
// #region Pipeline
249
250
this._pipeline = this._device.createRenderPipeline({
251
label: 'Monaco render pipeline',
252
layout: 'auto',
253
vertex: {
254
module,
255
buffers: [
256
{
257
arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each
258
attributes: [
259
{ shaderLocation: 0, offset: 0, format: 'float32x2' }, // position
260
],
261
}
262
]
263
},
264
fragment: {
265
module,
266
targets: [
267
{
268
format: presentationFormat,
269
blend: {
270
color: {
271
srcFactor: 'src-alpha',
272
dstFactor: 'one-minus-src-alpha'
273
},
274
alpha: {
275
srcFactor: 'src-alpha',
276
dstFactor: 'one-minus-src-alpha'
277
},
278
},
279
}
280
],
281
},
282
});
283
284
// #endregion Pipeline
285
286
// #region Bind group
287
288
this._rebuildBindGroup = () => {
289
this._bindGroup = this._device.createBindGroup({
290
label: 'Monaco bind group',
291
layout: this._pipeline.getBindGroupLayout(0),
292
entries: [
293
// TODO: Pass in generically as array?
294
{ binding: BindingId.GlyphInfo, resource: { buffer: this._glyphStorageBuffer } },
295
{
296
binding: BindingId.TextureSampler, resource: this._device.createSampler({
297
label: 'Monaco atlas sampler',
298
magFilter: 'nearest',
299
minFilter: 'nearest',
300
})
301
},
302
{ binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() },
303
{ binding: BindingId.LayoutInfoUniform, resource: { buffer: layoutInfoUniformBuffer } },
304
{ binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } },
305
...this._renderStrategy.value!.bindGroupEntries
306
],
307
});
308
};
309
this._rebuildBindGroup();
310
311
// endregion Bind group
312
313
this._initialized = true;
314
315
// Render the initial viewport immediately after initialization
316
if (this._initViewportData) {
317
// HACK: Rendering multiple times in the same frame like this isn't ideal, but there
318
// isn't an easy way to merge viewport data
319
for (const viewportData of this._initViewportData) {
320
this.renderText(viewportData);
321
}
322
this._initViewportData = undefined;
323
}
324
}
325
326
private _refreshRenderStrategy(viewportData: ViewportData) {
327
if (this._renderStrategy.value?.type === 'viewport') {
328
return;
329
}
330
if (viewportData.endLineNumber < FullFileRenderStrategy.maxSupportedLines && this._viewportMaxColumn(viewportData) < FullFileRenderStrategy.maxSupportedColumns) {
331
return;
332
}
333
this._logService.trace(`File is larger than ${FullFileRenderStrategy.maxSupportedLines} lines or ${FullFileRenderStrategy.maxSupportedColumns} columns, switching to viewport render strategy`);
334
const viewportRenderStrategy = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer });
335
this._renderStrategy.value = viewportRenderStrategy;
336
this._register(viewportRenderStrategy.onDidChangeBindGroupEntries(() => this._rebuildBindGroup?.()));
337
this._rebuildBindGroup?.();
338
}
339
340
private _viewportMaxColumn(viewportData: ViewportData): number {
341
let maxColumn = 0;
342
let lineData: ViewLineRenderingData;
343
for (let i = viewportData.startLineNumber; i <= viewportData.endLineNumber; i++) {
344
lineData = viewportData.getViewLineRenderingData(i);
345
maxColumn = Math.max(maxColumn, lineData.maxColumn);
346
}
347
return maxColumn;
348
}
349
350
private _updateAtlasStorageBufferAndTexture() {
351
for (const [layerIndex, page] of ViewGpuContext.atlas.pages.entries()) {
352
if (layerIndex >= TextureAtlas.maximumPageCount) {
353
console.log(`Attempt to upload atlas page [${layerIndex}], only ${TextureAtlas.maximumPageCount} are supported currently`);
354
continue;
355
}
356
357
// Skip the update if it's already the latest version
358
if (page.version === this._atlasGpuTextureVersions[layerIndex]) {
359
continue;
360
}
361
362
this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version);
363
364
const entryCount = GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount;
365
const values = new Float32Array(entryCount);
366
let entryOffset = 0;
367
for (const glyph of page.glyphs) {
368
values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition] = glyph.x;
369
values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y;
370
values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize] = glyph.w;
371
values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize + 1] = glyph.h;
372
values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX;
373
values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY;
374
entryOffset += GlyphStorageBufferInfo.FloatsPerEntry;
375
}
376
if (entryOffset / GlyphStorageBufferInfo.FloatsPerEntry > TextureAtlasPage.maximumGlyphCount) {
377
throw new Error(`Attempting to write more glyphs (${entryOffset / GlyphStorageBufferInfo.FloatsPerEntry}) than the GPUBuffer can hold (${TextureAtlasPage.maximumGlyphCount})`);
378
}
379
this._device.queue.writeBuffer(
380
this._glyphStorageBuffer,
381
layerIndex * GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount * Float32Array.BYTES_PER_ELEMENT,
382
values,
383
0,
384
GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount
385
);
386
if (page.usedArea.right - page.usedArea.left > 0 && page.usedArea.bottom - page.usedArea.top > 0) {
387
this._device.queue.copyExternalImageToTexture(
388
{ source: page.source },
389
{
390
texture: this._atlasGpuTexture,
391
origin: {
392
x: page.usedArea.left,
393
y: page.usedArea.top,
394
z: layerIndex
395
}
396
},
397
{
398
width: page.usedArea.right - page.usedArea.left + 1,
399
height: page.usedArea.bottom - page.usedArea.top + 1
400
},
401
);
402
}
403
this._atlasGpuTextureVersions[layerIndex] = page.version;
404
}
405
}
406
407
public prepareRender(ctx: RenderingContext): void {
408
throw new BugIndicatingError('Should not be called');
409
}
410
411
public override render(ctx: RestrictedRenderingContext): void {
412
throw new BugIndicatingError('Should not be called');
413
}
414
415
// #region Event handlers
416
417
// Since ViewLinesGpu currently coordinates rendering to the canvas, it must listen to all
418
// changed events that any GPU part listens to. This is because any drawing to the canvas will
419
// clear it for that frame, so all parts must be rendered every time.
420
//
421
// Additionally, since this is intrinsically linked to ViewLines, it must also listen to events
422
// from that side. Luckily rendering is cheap, it's only when uploaded data changes does it
423
// start to cost.
424
425
override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {
426
this._refreshGlyphRasterizer();
427
return true;
428
}
429
override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; }
430
override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; }
431
override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; }
432
433
override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { return true; }
434
override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { return true; }
435
override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { return true; }
436
override onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean { return true; }
437
override onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean { return true; }
438
override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { return true; }
439
override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { return true; }
440
override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return true; }
441
442
// #endregion
443
444
private _refreshGlyphRasterizer() {
445
const glyphRasterizer = this._glyphRasterizer.value;
446
if (!glyphRasterizer) {
447
return;
448
}
449
const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily);
450
const fontSize = this._context.configuration.options.get(EditorOption.fontSize);
451
const devicePixelRatio = this._viewGpuContext.devicePixelRatio.get();
452
if (
453
glyphRasterizer.fontFamily !== fontFamily ||
454
glyphRasterizer.fontSize !== fontSize ||
455
glyphRasterizer.devicePixelRatio !== devicePixelRatio
456
) {
457
this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio, ViewGpuContext.decorationStyleCache);
458
}
459
}
460
461
public renderText(viewportData: ViewportData): void {
462
if (this._initialized) {
463
this._refreshRenderStrategy(viewportData);
464
return this._renderText(viewportData);
465
} else {
466
this._initViewportData = this._initViewportData ?? [];
467
this._initViewportData.push(viewportData);
468
}
469
}
470
471
private _renderText(viewportData: ViewportData): void {
472
this._viewGpuContext.rectangleRenderer.draw(viewportData);
473
474
const options = new ViewLineOptions(this._context.configuration, this._context.theme.type);
475
476
this._renderStrategy.value!.update(viewportData, options);
477
478
this._updateAtlasStorageBufferAndTexture();
479
480
const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' });
481
482
this._renderPassColorAttachment.view = this._viewGpuContext.ctx.getCurrentTexture().createView({ label: 'Monaco canvas texture view' });
483
const pass = encoder.beginRenderPass(this._renderPassDescriptor);
484
pass.setPipeline(this._pipeline);
485
pass.setVertexBuffer(0, this._vertexBuffer);
486
487
// Only draw the content area
488
const contentLeft = Math.ceil(this._viewGpuContext.contentLeft.get() * this._viewGpuContext.devicePixelRatio.get());
489
pass.setScissorRect(contentLeft, 0, this.canvas.width - contentLeft, this.canvas.height);
490
491
pass.setBindGroup(0, this._bindGroup);
492
493
this._renderStrategy.value!.draw(pass, viewportData);
494
495
pass.end();
496
497
const commandBuffer = encoder.finish();
498
499
this._device.queue.submit([commandBuffer]);
500
501
this._lastViewportData = viewportData;
502
this._lastViewLineOptions = options;
503
}
504
505
linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null {
506
if (!this._lastViewportData) {
507
return null;
508
}
509
const originalEndLineNumber = _range.endLineNumber;
510
const range = Range.intersectRanges(_range, this._lastViewportData.visibleRange);
511
if (!range) {
512
return null;
513
}
514
515
const rendStartLineNumber = this._lastViewportData.startLineNumber;
516
const rendEndLineNumber = this._lastViewportData.endLineNumber;
517
518
const viewportData = this._lastViewportData;
519
const viewLineOptions = this._lastViewLineOptions;
520
521
if (!viewportData || !viewLineOptions) {
522
return null;
523
}
524
525
const visibleRanges: LineVisibleRanges[] = [];
526
527
let nextLineModelLineNumber: number = 0;
528
if (includeNewLines) {
529
nextLineModelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;
530
}
531
532
for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {
533
534
if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {
535
continue;
536
}
537
const startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1;
538
const continuesInNextLine = lineNumber !== originalEndLineNumber;
539
const endColumn = continuesInNextLine ? this._context.viewModel.getLineMaxColumn(lineNumber) : range.endColumn;
540
541
const visibleRangesForLine = this._visibleRangesForLineRange(lineNumber, startColumn, endColumn);
542
543
if (!visibleRangesForLine) {
544
continue;
545
}
546
547
if (includeNewLines && lineNumber < originalEndLineNumber) {
548
const currentLineModelLineNumber = nextLineModelLineNumber;
549
nextLineModelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber + 1, 1)).lineNumber;
550
551
if (currentLineModelLineNumber !== nextLineModelLineNumber) {
552
visibleRangesForLine.ranges[visibleRangesForLine.ranges.length - 1].width += viewLineOptions.spaceWidth;
553
}
554
}
555
556
visibleRanges.push(new LineVisibleRanges(visibleRangesForLine.outsideRenderedLine, lineNumber, HorizontalRange.from(visibleRangesForLine.ranges), continuesInNextLine));
557
}
558
559
if (visibleRanges.length === 0) {
560
return null;
561
}
562
563
return visibleRanges;
564
}
565
566
private _visibleRangesForLineRange(lineNumber: number, startColumn: number, endColumn: number): VisibleRanges | null {
567
if (this.shouldRender()) {
568
// Cannot read from the DOM because it is dirty
569
// i.e. the model & the dom are out of sync, so I'd be reading something stale
570
return null;
571
}
572
573
const viewportData = this._lastViewportData;
574
const viewLineOptions = this._lastViewLineOptions;
575
576
if (!viewportData || !viewLineOptions || lineNumber < viewportData.startLineNumber || lineNumber > viewportData.endLineNumber) {
577
return null;
578
}
579
580
// Resolve tab widths for this line
581
const lineData = viewportData.getViewLineRenderingData(lineNumber);
582
const content = lineData.content;
583
584
let contentSegmenter: IContentSegmenter | undefined;
585
if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {
586
contentSegmenter = createContentSegmenter(lineData, viewLineOptions);
587
}
588
589
let chars: string | undefined = '';
590
591
let resolvedStartColumn = 0;
592
let resolvedStartCssPixelOffset = 0;
593
for (let x = 0; x < startColumn - 1; x++) {
594
if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) {
595
chars = content.charAt(x);
596
} else {
597
chars = contentSegmenter!.getSegmentAtIndex(x);
598
if (chars === undefined) {
599
continue;
600
}
601
resolvedStartCssPixelOffset += (this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth;
602
}
603
if (chars === '\t') {
604
resolvedStartColumn = CursorColumns.nextRenderTabStop(resolvedStartColumn, lineData.tabSize);
605
} else {
606
resolvedStartColumn++;
607
}
608
}
609
let resolvedEndColumn = resolvedStartColumn;
610
let resolvedEndCssPixelOffset = 0;
611
for (let x = startColumn - 1; x < endColumn - 1; x++) {
612
if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) {
613
chars = content.charAt(x);
614
} else {
615
chars = contentSegmenter!.getSegmentAtIndex(x);
616
if (chars === undefined) {
617
continue;
618
}
619
resolvedEndCssPixelOffset += (this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth;
620
}
621
if (chars === '\t') {
622
resolvedEndColumn = CursorColumns.nextRenderTabStop(resolvedEndColumn, lineData.tabSize);
623
} else {
624
resolvedEndColumn++;
625
}
626
}
627
628
// Visible horizontal range in _scaled_ pixels
629
const result = new VisibleRanges(false, [new FloatHorizontalRange(
630
resolvedStartColumn * viewLineOptions.spaceWidth + resolvedStartCssPixelOffset,
631
(resolvedEndColumn - resolvedStartColumn) * viewLineOptions.spaceWidth + resolvedEndCssPixelOffset)
632
]);
633
634
return result;
635
}
636
637
visibleRangeForPosition(position: Position): HorizontalPosition | null {
638
const visibleRanges = this._visibleRangesForLineRange(position.lineNumber, position.column, position.column);
639
if (!visibleRanges) {
640
return null;
641
}
642
return new HorizontalPosition(visibleRanges.outsideRenderedLine, visibleRanges.ranges[0].left);
643
}
644
645
getLineWidth(lineNumber: number): number | undefined {
646
if (!this._lastViewportData || !this._lastViewLineOptions) {
647
return undefined;
648
}
649
if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
650
return undefined;
651
}
652
653
const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber);
654
const lineRange = this._visibleRangesForLineRange(lineNumber, 1, lineData.maxColumn);
655
const lastRange = lineRange?.ranges.at(-1);
656
if (lastRange) {
657
return lastRange.width;
658
}
659
660
return undefined;
661
}
662
663
getPositionAtCoordinate(lineNumber: number, mouseContentHorizontalOffset: number): Position | undefined {
664
if (!this._lastViewportData || !this._lastViewLineOptions) {
665
return undefined;
666
}
667
if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
668
return undefined;
669
}
670
const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber);
671
const content = lineData.content;
672
const dpr = getActiveWindow().devicePixelRatio;
673
const mouseContentHorizontalOffsetDevicePixels = mouseContentHorizontalOffset * dpr;
674
const spaceWidthDevicePixels = this._lastViewLineOptions.spaceWidth * dpr;
675
const contentSegmenter = createContentSegmenter(lineData, this._lastViewLineOptions);
676
677
let widthSoFar = 0;
678
let charWidth = 0;
679
let tabXOffset = 0;
680
let column = 0;
681
for (let x = 0; x < content.length; x++) {
682
const chars = contentSegmenter.getSegmentAtIndex(x);
683
684
// Part of an earlier segment
685
if (chars === undefined) {
686
column++;
687
continue;
688
}
689
690
// Get the width of the character
691
if (chars === '\t') {
692
// Find the pixel offset between the current position and the next tab stop
693
const offsetBefore = x + tabXOffset;
694
tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);
695
charWidth = spaceWidthDevicePixels * (tabXOffset - offsetBefore);
696
// Convert back to offset excluding x and the current character
697
tabXOffset -= x + 1;
698
} else if (lineData.isBasicASCII && this._lastViewLineOptions.useMonospaceOptimizations) {
699
charWidth = spaceWidthDevicePixels;
700
} else {
701
charWidth = this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width;
702
}
703
704
if (mouseContentHorizontalOffsetDevicePixels < widthSoFar + charWidth / 2) {
705
break;
706
}
707
708
widthSoFar += charWidth;
709
column++;
710
}
711
712
return new Position(lineNumber, column + 1);
713
}
714
}
715
716