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