Path: blob/main/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts
5334 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 { getActiveWindow } from '../../../../base/browser/dom.js';6import { BugIndicatingError } from '../../../../base/common/errors.js';7import { autorun, runOnChange } from '../../../../base/common/observable.js';8import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';9import { ILogService } from '../../../../platform/log/common/log.js';10import { EditorOption } from '../../../common/config/editorOptions.js';11import { Position } from '../../../common/core/position.js';12import { Range } from '../../../common/core/range.js';13import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';14import type { ViewContext } from '../../../common/viewModel/viewContext.js';15import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js';16import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js';17import { GPULifecycle } from '../../gpu/gpuDisposable.js';18import { quadVertices } from '../../gpu/gpuUtils.js';19import { ViewGpuContext } from '../../gpu/viewGpuContext.js';20import { FloatHorizontalRange, HorizontalPosition, HorizontalRange, IViewLines, LineVisibleRanges, RenderingContext, RestrictedRenderingContext, VisibleRanges } from '../../view/renderingContext.js';21import { ViewPart } from '../../view/viewPart.js';22import { ViewLineOptions } from '../viewLines/viewLineOptions.js';23import type * as viewEvents from '../../../common/viewEvents.js';24import { CursorColumns } from '../../../common/core/cursorColumns.js';25import { TextureAtlas } from '../../gpu/atlas/textureAtlas.js';26import { createContentSegmenter, type IContentSegmenter } from '../../gpu/contentSegmenter.js';27import { ViewportRenderStrategy } from '../../gpu/renderStrategy/viewportRenderStrategy.js';28import { FullFileRenderStrategy } from '../../gpu/renderStrategy/fullFileRenderStrategy.js';29import { MutableDisposable } from '../../../../base/common/lifecycle.js';30import type { ViewLineRenderingData } from '../../../common/viewModel.js';31import { GlyphRasterizer } from '../../gpu/raster/glyphRasterizer.js';3233const enum GlyphStorageBufferInfo {34FloatsPerEntry = 2 + 2 + 2,35BytesPerEntry = GlyphStorageBufferInfo.FloatsPerEntry * 4,36Offset_TexturePosition = 0,37Offset_TextureSize = 2,38Offset_OriginPosition = 4,39}4041/**42* The GPU implementation of the ViewLines part.43*/44export class ViewLinesGpu extends ViewPart implements IViewLines {4546private readonly canvas: HTMLCanvasElement;4748private _initViewportData?: ViewportData[];49private _lastViewportData?: ViewportData;50private _lastViewLineOptions?: ViewLineOptions;5152/**53* Tracks the maximum line width seen so far for horizontal scrollbar sizing.54* This is needed because GPU-rendered lines don't have DOM nodes to measure.55*/56private _maxLineWidth: number = 0;5758private _device!: GPUDevice;59private _renderPassDescriptor!: GPURenderPassDescriptor;60private _renderPassColorAttachment!: GPURenderPassColorAttachment;61private _bindGroup!: GPUBindGroup;62private _pipeline!: GPURenderPipeline;6364private _vertexBuffer!: GPUBuffer;6566private _glyphStorageBuffer!: GPUBuffer;67private _atlasGpuTexture!: GPUTexture;68private readonly _atlasGpuTextureVersions: number[] = [];6970private _initialized = false;7172private readonly _glyphRasterizer: MutableDisposable<GlyphRasterizer> = this._register(new MutableDisposable());73private readonly _renderStrategy: MutableDisposable<IGpuRenderStrategy> = this._register(new MutableDisposable());74private _rebuildBindGroup?: () => void;7576constructor(77context: ViewContext,78private readonly _viewGpuContext: ViewGpuContext,79@IInstantiationService private readonly _instantiationService: IInstantiationService,80@ILogService private readonly _logService: ILogService,81) {82super(context);8384this.canvas = this._viewGpuContext.canvas.domNode;8586// Re-render the following frame after canvas device pixel dimensions change, provided a87// new render does not occur.88this._register(autorun(reader => {89this._viewGpuContext.canvasDevicePixelDimensions.read(reader);90const lastViewportData = this._lastViewportData;91if (lastViewportData) {92setTimeout(() => {93if (lastViewportData === this._lastViewportData) {94this.renderText(lastViewportData);95}96});97}98}));99100this.initWebgpu();101}102103async initWebgpu() {104// #region General105106this._device = ViewGpuContext.deviceSync || await ViewGpuContext.device;107108if (this._store.isDisposed) {109return;110}111112const atlas = ViewGpuContext.atlas;113114// Rerender when the texture atlas deletes glyphs115this._register(atlas.onDidDeleteGlyphs(() => {116this._atlasGpuTextureVersions.length = 0;117this._atlasGpuTextureVersions[0] = 0;118this._atlasGpuTextureVersions[1] = 0;119this._renderStrategy.value!.reset();120}));121122const presentationFormat = navigator.gpu.getPreferredCanvasFormat();123this._viewGpuContext.ctx.configure({124device: this._device,125format: presentationFormat,126alphaMode: 'premultiplied',127});128129this._renderPassColorAttachment = {130view: null!, // Will be filled at render time131loadOp: 'load',132storeOp: 'store',133};134this._renderPassDescriptor = {135label: 'Monaco render pass',136colorAttachments: [this._renderPassColorAttachment],137};138139// #endregion General140141// #region Uniforms142143let layoutInfoUniformBuffer: GPUBuffer;144{145const enum Info {146FloatsPerEntry = 6,147BytesPerEntry = Info.FloatsPerEntry * 4,148Offset_CanvasWidth____ = 0,149Offset_CanvasHeight___ = 1,150Offset_ViewportOffsetX = 2,151Offset_ViewportOffsetY = 3,152Offset_ViewportWidth__ = 4,153Offset_ViewportHeight_ = 5,154}155const bufferValues = new Float32Array(Info.FloatsPerEntry);156const updateBufferValues = (canvasDevicePixelWidth: number = this.canvas.width, canvasDevicePixelHeight: number = this.canvas.height) => {157bufferValues[Info.Offset_CanvasWidth____] = canvasDevicePixelWidth;158bufferValues[Info.Offset_CanvasHeight___] = canvasDevicePixelHeight;159bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio);160bufferValues[Info.Offset_ViewportOffsetY] = 0;161bufferValues[Info.Offset_ViewportWidth__] = bufferValues[Info.Offset_CanvasWidth____] - bufferValues[Info.Offset_ViewportOffsetX];162bufferValues[Info.Offset_ViewportHeight_] = bufferValues[Info.Offset_CanvasHeight___] - bufferValues[Info.Offset_ViewportOffsetY];163return bufferValues;164};165layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, {166label: 'Monaco uniform buffer',167size: Info.BytesPerEntry,168usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,169}, () => updateBufferValues())).object;170this._register(runOnChange(this._viewGpuContext.canvasDevicePixelDimensions, ({ width, height }) => {171this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(width, height));172}));173this._register(runOnChange(this._viewGpuContext.contentLeft, () => {174this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues());175}));176}177178let atlasInfoUniformBuffer: GPUBuffer;179{180const enum Info {181FloatsPerEntry = 2,182BytesPerEntry = Info.FloatsPerEntry * 4,183Offset_Width_ = 0,184Offset_Height = 1,185}186atlasInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, {187label: 'Monaco atlas info uniform buffer',188size: Info.BytesPerEntry,189usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,190}, () => {191const values = new Float32Array(Info.FloatsPerEntry);192values[Info.Offset_Width_] = atlas.pageSize;193values[Info.Offset_Height] = atlas.pageSize;194return values;195})).object;196}197198// #endregion Uniforms199200// #region Storage buffers201202const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily);203const fontSize = this._context.configuration.options.get(EditorOption.fontSize);204this._glyphRasterizer.value = this._register(new GlyphRasterizer(fontSize, fontFamily, this._viewGpuContext.devicePixelRatio.get(), ViewGpuContext.decorationStyleCache));205this._register(runOnChange(this._viewGpuContext.devicePixelRatio, () => {206this._refreshGlyphRasterizer();207}));208209210this._renderStrategy.value = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer });211// this._renderStrategy.value = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device);212213this._glyphStorageBuffer = this._register(GPULifecycle.createBuffer(this._device, {214label: 'Monaco glyph storage buffer',215size: TextureAtlas.maximumPageCount * (TextureAtlasPage.maximumGlyphCount * GlyphStorageBufferInfo.BytesPerEntry),216usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,217})).object;218this._atlasGpuTextureVersions[0] = 0;219this._atlasGpuTextureVersions[1] = 0;220this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, {221label: 'Monaco atlas texture',222format: 'rgba8unorm',223size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: TextureAtlas.maximumPageCount },224dimension: '2d',225usage: GPUTextureUsage.TEXTURE_BINDING |226GPUTextureUsage.COPY_DST |227GPUTextureUsage.RENDER_ATTACHMENT,228})).object;229230this._updateAtlasStorageBufferAndTexture();231232// #endregion Storage buffers233234// #region Vertex buffer235236this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, {237label: 'Monaco vertex buffer',238size: quadVertices.byteLength,239usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,240}, quadVertices)).object;241242// #endregion Vertex buffer243244// #region Shader module245246const module = this._device.createShaderModule({247label: 'Monaco shader module',248code: this._renderStrategy.value.wgsl,249});250251// #endregion Shader module252253// #region Pipeline254255this._pipeline = this._device.createRenderPipeline({256label: 'Monaco render pipeline',257layout: 'auto',258vertex: {259module,260buffers: [261{262arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each263attributes: [264{ shaderLocation: 0, offset: 0, format: 'float32x2' }, // position265],266}267]268},269fragment: {270module,271targets: [272{273format: presentationFormat,274blend: {275color: {276srcFactor: 'src-alpha',277dstFactor: 'one-minus-src-alpha'278},279alpha: {280srcFactor: 'src-alpha',281dstFactor: 'one-minus-src-alpha'282},283},284}285],286},287});288289// #endregion Pipeline290291// #region Bind group292293this._rebuildBindGroup = () => {294this._bindGroup = this._device.createBindGroup({295label: 'Monaco bind group',296layout: this._pipeline.getBindGroupLayout(0),297entries: [298// TODO: Pass in generically as array?299{ binding: BindingId.GlyphInfo, resource: { buffer: this._glyphStorageBuffer } },300{301binding: BindingId.TextureSampler, resource: this._device.createSampler({302label: 'Monaco atlas sampler',303magFilter: 'nearest',304minFilter: 'nearest',305})306},307{ binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() },308{ binding: BindingId.LayoutInfoUniform, resource: { buffer: layoutInfoUniformBuffer } },309{ binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } },310...this._renderStrategy.value!.bindGroupEntries311],312});313};314this._rebuildBindGroup();315316// endregion Bind group317318this._initialized = true;319320// Render the initial viewport immediately after initialization321if (this._initViewportData) {322// HACK: Rendering multiple times in the same frame like this isn't ideal, but there323// isn't an easy way to merge viewport data324for (const viewportData of this._initViewportData) {325this.renderText(viewportData);326}327this._initViewportData = undefined;328}329}330331private _refreshRenderStrategy(viewportData: ViewportData) {332if (this._renderStrategy.value?.type === 'viewport') {333return;334}335if (viewportData.endLineNumber < FullFileRenderStrategy.maxSupportedLines && this._viewportMaxColumn(viewportData) < FullFileRenderStrategy.maxSupportedColumns) {336return;337}338this._logService.trace(`File is larger than ${FullFileRenderStrategy.maxSupportedLines} lines or ${FullFileRenderStrategy.maxSupportedColumns} columns, switching to viewport render strategy`);339const viewportRenderStrategy = this._instantiationService.createInstance(ViewportRenderStrategy, this._context, this._viewGpuContext, this._device, this._glyphRasterizer as { value: GlyphRasterizer });340this._renderStrategy.value = viewportRenderStrategy;341this._register(viewportRenderStrategy.onDidChangeBindGroupEntries(() => this._rebuildBindGroup?.()));342this._rebuildBindGroup?.();343}344345private _viewportMaxColumn(viewportData: ViewportData): number {346let maxColumn = 0;347let lineData: ViewLineRenderingData;348for (let i = viewportData.startLineNumber; i <= viewportData.endLineNumber; i++) {349lineData = viewportData.getViewLineRenderingData(i);350maxColumn = Math.max(maxColumn, lineData.maxColumn);351}352return maxColumn;353}354355private _updateAtlasStorageBufferAndTexture() {356for (const [layerIndex, page] of ViewGpuContext.atlas.pages.entries()) {357if (layerIndex >= TextureAtlas.maximumPageCount) {358console.log(`Attempt to upload atlas page [${layerIndex}], only ${TextureAtlas.maximumPageCount} are supported currently`);359continue;360}361362// Skip the update if it's already the latest version363if (page.version === this._atlasGpuTextureVersions[layerIndex]) {364continue;365}366367this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version);368369const entryCount = GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount;370const values = new Float32Array(entryCount);371let entryOffset = 0;372for (const glyph of page.glyphs) {373values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition] = glyph.x;374values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y;375values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize] = glyph.w;376values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize + 1] = glyph.h;377values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX;378values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY;379entryOffset += GlyphStorageBufferInfo.FloatsPerEntry;380}381if (entryOffset / GlyphStorageBufferInfo.FloatsPerEntry > TextureAtlasPage.maximumGlyphCount) {382throw new Error(`Attempting to write more glyphs (${entryOffset / GlyphStorageBufferInfo.FloatsPerEntry}) than the GPUBuffer can hold (${TextureAtlasPage.maximumGlyphCount})`);383}384this._device.queue.writeBuffer(385this._glyphStorageBuffer,386layerIndex * GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount * Float32Array.BYTES_PER_ELEMENT,387values,3880,389GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount390);391if (page.usedArea.right - page.usedArea.left > 0 && page.usedArea.bottom - page.usedArea.top > 0) {392this._device.queue.copyExternalImageToTexture(393{ source: page.source },394{395texture: this._atlasGpuTexture,396origin: {397x: page.usedArea.left,398y: page.usedArea.top,399z: layerIndex400}401},402{403width: page.usedArea.right - page.usedArea.left + 1,404height: page.usedArea.bottom - page.usedArea.top + 1405},406);407}408this._atlasGpuTextureVersions[layerIndex] = page.version;409}410}411412public prepareRender(ctx: RenderingContext): void {413throw new BugIndicatingError('Should not be called');414}415416public override render(ctx: RestrictedRenderingContext): void {417throw new BugIndicatingError('Should not be called');418}419420// #region Event handlers421422// Since ViewLinesGpu currently coordinates rendering to the canvas, it must listen to all423// changed events that any GPU part listens to. This is because any drawing to the canvas will424// clear it for that frame, so all parts must be rendered every time.425//426// Additionally, since this is intrinsically linked to ViewLines, it must also listen to events427// from that side. Luckily rendering is cheap, it's only when uploaded data changes does it428// start to cost.429430override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean {431this._refreshGlyphRasterizer();432this._maxLineWidth = 0;433return true;434}435override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; }436override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; }437override onFlushed(e: viewEvents.ViewFlushedEvent): boolean {438this._maxLineWidth = 0;439return true;440}441442override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { return true; }443override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean {444this._maxLineWidth = 0;445return true;446}447override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { return true; }448override onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean { return true; }449override onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean { return true; }450override onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { return true; }451override onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { return true; }452override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return true; }453454// #endregion455456private _refreshGlyphRasterizer() {457const glyphRasterizer = this._glyphRasterizer.value;458if (!glyphRasterizer) {459return;460}461const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily);462const fontSize = this._context.configuration.options.get(EditorOption.fontSize);463const devicePixelRatio = this._viewGpuContext.devicePixelRatio.get();464if (465glyphRasterizer.fontFamily !== fontFamily ||466glyphRasterizer.fontSize !== fontSize ||467glyphRasterizer.devicePixelRatio !== devicePixelRatio468) {469this._glyphRasterizer.value = new GlyphRasterizer(fontSize, fontFamily, devicePixelRatio, ViewGpuContext.decorationStyleCache);470}471}472473public renderText(viewportData: ViewportData): void {474if (this._initialized) {475this._refreshRenderStrategy(viewportData);476return this._renderText(viewportData);477} else {478this._initViewportData = this._initViewportData ?? [];479this._initViewportData.push(viewportData);480}481}482483private _renderText(viewportData: ViewportData): void {484this._viewGpuContext.rectangleRenderer.draw(viewportData);485486const options = new ViewLineOptions(this._context.configuration, this._context.theme.type);487488this._renderStrategy.value!.update(viewportData, options);489490this._updateAtlasStorageBufferAndTexture();491492const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' });493494this._renderPassColorAttachment.view = this._viewGpuContext.ctx.getCurrentTexture().createView({ label: 'Monaco canvas texture view' });495const pass = encoder.beginRenderPass(this._renderPassDescriptor);496pass.setPipeline(this._pipeline);497pass.setVertexBuffer(0, this._vertexBuffer);498499// Only draw the content area500const contentLeft = Math.ceil(this._viewGpuContext.contentLeft.get() * this._viewGpuContext.devicePixelRatio.get());501pass.setScissorRect(contentLeft, 0, this.canvas.width - contentLeft, this.canvas.height);502503pass.setBindGroup(0, this._bindGroup);504505this._renderStrategy.value!.draw(pass, viewportData);506507pass.end();508509const commandBuffer = encoder.finish();510511this._device.queue.submit([commandBuffer]);512513this._lastViewportData = viewportData;514this._lastViewLineOptions = options;515516// Update max line width for horizontal scrollbar517this._updateMaxLineWidth(viewportData, options);518}519520/**521* Update the max line width based on GPU-rendered lines.522* This is needed because GPU-rendered lines don't have DOM nodes to measure.523*/524private _updateMaxLineWidth(viewportData: ViewportData, viewLineOptions: ViewLineOptions): void {525const dpr = getActiveWindow().devicePixelRatio;526let localMaxLineWidth = 0;527528for (let lineNumber = viewportData.startLineNumber; lineNumber <= viewportData.endLineNumber; lineNumber++) {529if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, lineNumber)) {530continue;531}532533const lineData = viewportData.getViewLineRenderingData(lineNumber);534const lineWidth = this._computeLineWidth(lineData, viewLineOptions, dpr);535localMaxLineWidth = Math.max(localMaxLineWidth, lineWidth);536}537538// Only update if we found a larger width (use ceil to match DOM behavior)539const iLineWidth = Math.ceil(localMaxLineWidth);540if (iLineWidth > this._maxLineWidth) {541this._maxLineWidth = iLineWidth;542this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLineWidth);543}544}545546/**547* Compute the width of a line in CSS pixels.548*/549private _computeLineWidth(lineData: ViewLineRenderingData, viewLineOptions: ViewLineOptions, dpr: number): number {550const content = lineData.content;551let contentSegmenter: IContentSegmenter | undefined;552if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {553contentSegmenter = createContentSegmenter(lineData, viewLineOptions);554}555556let width = 0;557let tabXOffset = 0;558559for (let x = 0; x < content.length; x++) {560let chars: string;561if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) {562chars = content.charAt(x);563} else {564const segment = contentSegmenter!.getSegmentAtIndex(x);565if (segment === undefined) {566continue;567}568chars = segment;569}570571if (chars === '\t') {572const offsetBefore = x + tabXOffset;573tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);574width += viewLineOptions.spaceWidth * (tabXOffset - offsetBefore);575tabXOffset -= x + 1;576} else if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) {577width += viewLineOptions.spaceWidth;578} else {579width += this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / dpr;580}581}582583return width;584}585586linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null {587if (!this._lastViewportData) {588return null;589}590const originalEndLineNumber = _range.endLineNumber;591const range = Range.intersectRanges(_range, this._lastViewportData.visibleRange);592if (!range) {593return null;594}595596const rendStartLineNumber = this._lastViewportData.startLineNumber;597const rendEndLineNumber = this._lastViewportData.endLineNumber;598599const viewportData = this._lastViewportData;600const viewLineOptions = this._lastViewLineOptions;601602if (!viewportData || !viewLineOptions) {603return null;604}605606const visibleRanges: LineVisibleRanges[] = [];607608let nextLineModelLineNumber: number = 0;609if (includeNewLines) {610nextLineModelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(range.startLineNumber, 1)).lineNumber;611}612613for (let lineNumber = range.startLineNumber; lineNumber <= range.endLineNumber; lineNumber++) {614615if (lineNumber < rendStartLineNumber || lineNumber > rendEndLineNumber) {616continue;617}618const startColumn = lineNumber === range.startLineNumber ? range.startColumn : 1;619const continuesInNextLine = lineNumber !== originalEndLineNumber;620const endColumn = continuesInNextLine ? this._context.viewModel.getLineMaxColumn(lineNumber) : range.endColumn;621622const visibleRangesForLine = this._visibleRangesForLineRange(lineNumber, startColumn, endColumn);623624if (!visibleRangesForLine) {625continue;626}627628if (includeNewLines && lineNumber < originalEndLineNumber) {629const currentLineModelLineNumber = nextLineModelLineNumber;630nextLineModelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber + 1, 1)).lineNumber;631632if (currentLineModelLineNumber !== nextLineModelLineNumber) {633visibleRangesForLine.ranges[visibleRangesForLine.ranges.length - 1].width += viewLineOptions.spaceWidth;634}635}636637visibleRanges.push(new LineVisibleRanges(visibleRangesForLine.outsideRenderedLine, lineNumber, HorizontalRange.from(visibleRangesForLine.ranges), continuesInNextLine));638}639640if (visibleRanges.length === 0) {641return null;642}643644return visibleRanges;645}646647private _visibleRangesForLineRange(lineNumber: number, startColumn: number, endColumn: number): VisibleRanges | null {648if (this.shouldRender()) {649// Cannot read from the DOM because it is dirty650// i.e. the model & the dom are out of sync, so I'd be reading something stale651return null;652}653654const viewportData = this._lastViewportData;655const viewLineOptions = this._lastViewLineOptions;656657if (!viewportData || !viewLineOptions || lineNumber < viewportData.startLineNumber || lineNumber > viewportData.endLineNumber) {658return null;659}660661// Resolve tab widths for this line662const lineData = viewportData.getViewLineRenderingData(lineNumber);663const content = lineData.content;664665let contentSegmenter: IContentSegmenter | undefined;666if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {667contentSegmenter = createContentSegmenter(lineData, viewLineOptions);668}669670let chars: string | undefined = '';671672let resolvedStartColumn = 0;673let resolvedStartCssPixelOffset = 0;674for (let x = 0; x < startColumn - 1; x++) {675if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) {676chars = content.charAt(x);677} else {678chars = contentSegmenter!.getSegmentAtIndex(x);679if (chars === undefined) {680continue;681}682resolvedStartCssPixelOffset += (this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth;683}684if (chars === '\t') {685resolvedStartColumn = CursorColumns.nextRenderTabStop(resolvedStartColumn, lineData.tabSize);686} else {687resolvedStartColumn++;688}689}690let resolvedEndColumn = resolvedStartColumn;691let resolvedEndCssPixelOffset = 0;692for (let x = startColumn - 1; x < endColumn - 1; x++) {693if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) {694chars = content.charAt(x);695} else {696chars = contentSegmenter!.getSegmentAtIndex(x);697if (chars === undefined) {698continue;699}700resolvedEndCssPixelOffset += (this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / getActiveWindow().devicePixelRatio) - viewLineOptions.spaceWidth;701}702if (chars === '\t') {703resolvedEndColumn = CursorColumns.nextRenderTabStop(resolvedEndColumn, lineData.tabSize);704} else {705resolvedEndColumn++;706}707}708709// Visible horizontal range in _scaled_ pixels710const result = new VisibleRanges(false, [new FloatHorizontalRange(711resolvedStartColumn * viewLineOptions.spaceWidth + resolvedStartCssPixelOffset,712(resolvedEndColumn - resolvedStartColumn) * viewLineOptions.spaceWidth + resolvedEndCssPixelOffset)713]);714715return result;716}717718visibleRangeForPosition(position: Position): HorizontalPosition | null {719const visibleRanges = this._visibleRangesForLineRange(position.lineNumber, position.column, position.column);720if (!visibleRanges) {721return null;722}723return new HorizontalPosition(visibleRanges.outsideRenderedLine, visibleRanges.ranges[0].left);724}725726getLineWidth(lineNumber: number): number | undefined {727if (!this._lastViewportData || !this._lastViewLineOptions) {728return undefined;729}730if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {731return undefined;732}733734const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber);735const lineRange = this._visibleRangesForLineRange(lineNumber, 1, lineData.maxColumn);736const lastRange = lineRange?.ranges.at(-1);737if (lastRange) {738// Total line width is the left offset plus width of the last range739return lastRange.left + lastRange.width;740}741742return undefined;743}744745getPositionAtCoordinate(lineNumber: number, mouseContentHorizontalOffset: number): Position | undefined {746if (!this._lastViewportData || !this._lastViewLineOptions) {747return undefined;748}749if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {750return undefined;751}752const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber);753const content = lineData.content;754const dpr = getActiveWindow().devicePixelRatio;755const mouseContentHorizontalOffsetDevicePixels = mouseContentHorizontalOffset * dpr;756const spaceWidthDevicePixels = this._lastViewLineOptions.spaceWidth * dpr;757const contentSegmenter = createContentSegmenter(lineData, this._lastViewLineOptions);758759let widthSoFar = 0;760let charWidth = 0;761let tabXOffset = 0;762let column = 0;763for (let x = 0; x < content.length; x++) {764const chars = contentSegmenter.getSegmentAtIndex(x);765766// Part of an earlier segment767if (chars === undefined) {768column++;769continue;770}771772// Get the width of the character773if (chars === '\t') {774// Find the pixel offset between the current position and the next tab stop775const offsetBefore = x + tabXOffset;776tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);777charWidth = spaceWidthDevicePixels * (tabXOffset - offsetBefore);778// Convert back to offset excluding x and the current character779tabXOffset -= x + 1;780} else if (lineData.isBasicASCII && this._lastViewLineOptions.useMonospaceOptimizations) {781charWidth = spaceWidthDevicePixels;782} else {783charWidth = this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width;784}785786if (mouseContentHorizontalOffsetDevicePixels < widthSoFar + charWidth / 2) {787break;788}789790widthSoFar += charWidth;791column++;792}793794return new Position(lineNumber, column + 1);795}796}797798799