From 0977e3f8a87da8dc835d095e4d3fcabf0e234909 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 18 Mar 2024 09:00:20 -0700 Subject: [PATCH 001/286] Hack rendering to canvas in monaco --- src/vs/editor/browser/view/viewLayer.ts | 69 ++++++++++++++++++- src/vs/editor/browser/view/viewOverlays.ts | 2 +- .../browser/viewParts/lines/viewLines.ts | 9 +-- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index bbbb0dd9d73..6e16f0da6a5 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getActiveWindow } from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; import { BugIndicatingError } from 'vs/base/common/errors'; @@ -255,9 +256,17 @@ export class VisibleLinesCollection { public readonly domNode: FastDomNode; private readonly _linesCollection: RenderedLinesCollection; + private readonly _canvas: HTMLCanvasElement; + constructor(host: IVisibleLinesHost) { this._host = host; this.domNode = this._createDomNode(); + + this._canvas = document.createElement('canvas'); + this._canvas.style.height = '100%'; + this._canvas.style.width = '100%'; + this.domNode.domNode.appendChild(this._canvas); + this._linesCollection = new RenderedLinesCollection(() => this._host.createVisibleLine()); } @@ -345,11 +354,18 @@ export class VisibleLinesCollection { return this._linesCollection.getLine(lineNumber); } - public renderLines(viewportData: ViewportData): void { - + public renderLines(viewportData: ViewportData, viewOverlays?: boolean): void { const inp = this._linesCollection._get(); - const renderer = new ViewLayerRenderer(this.domNode.domNode, this._host, viewportData); + let renderer; + if (viewOverlays) { + renderer = new ViewLayerRenderer(this.domNode.domNode, this._host, viewportData); + } else { + this._canvas.width = this.domNode.domNode.clientWidth; + this._canvas.height = this.domNode.domNode.clientHeight; + renderer = new CanvasViewLayerRenderer(this._canvas, this._host, viewportData); + } + const ctx: IRendererContext = { rendLineNumberStart: inp.rendLineNumberStart, @@ -619,3 +635,50 @@ class ViewLayerRenderer { } } } + + +class CanvasViewLayerRenderer { + + readonly domNode: HTMLCanvasElement; + readonly host: IVisibleLinesHost; + readonly viewportData: ViewportData; + + private readonly _ctx: CanvasRenderingContext2D; + + constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { + this.domNode = domNode; + this.host = host; + this.viewportData = viewportData; + + this._ctx = this.domNode.getContext('2d')!; + this._ctx.fillStyle = '#fff'; + const style = getActiveWindow().getComputedStyle(this.domNode); + this._ctx.font = `${style.fontSize} ${style.fontFamily}`; + this._ctx.textBaseline = 'top'; + } + + public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { + + this._ctx.clearRect(0, 0, this.domNode.width, this.domNode.height); + + const ctx: IRendererContext = { + rendLineNumberStart: inContext.rendLineNumberStart, + lines: inContext.lines.slice(0), + linesLength: inContext.linesLength + }; + + let i = 0; + let scrollTop = parseInt(this.domNode.parentElement!.getAttribute('data-adjusted-scroll-top')!); + if (Number.isNaN(scrollTop)) { + scrollTop = 0; + } + for (let lineNumber = startLineNumber; lineNumber <= stopLineNumber; lineNumber++) { + const y = Math.round((-scrollTop + deltaTop[lineNumber - startLineNumber]) * getActiveWindow().devicePixelRatio); + // console.log(this.viewportData.getViewLineRenderingData(lineNumber).content, 0, y); + this._ctx.fillText(this.viewportData.getViewLineRenderingData(lineNumber).content, 0, y); + i++; + } + + return ctx; + } +} diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 1041fd58a58..4d1b5a99581 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -136,7 +136,7 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost, // (1) render lines - ensures lines are in the DOM this._visibleLines.renderLines(viewportData); this._lastRenderedData.setCurrentVisibleRange(viewportData.visibleRange); - this.domNode.setWidth(this._context.viewLayout.getScrollWidth()); - this.domNode.setHeight(Math.min(this._context.viewLayout.getScrollHeight(), 1000000)); + this.domNode.setWidth(this._context.viewLayout.getCurrentViewport().width); + this.domNode.setHeight(Math.min(this._context.viewLayout.getCurrentViewport().height, 1000000)); // (2) compute horizontal scroll position: // - this must happen after the lines are in the DOM since it might need a line that rendered just now @@ -663,8 +663,9 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, this._linesContent.setLayerHinting(this._canUseLayerHinting); this._linesContent.setContain('strict'); const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta; - this._linesContent.setTop(-adjustedScrollTop); - this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft()); + this.domNode.domNode.setAttribute('data-adjusted-scroll-top', adjustedScrollTop.toString()); + // this._linesContent.setTop(-adjustedScrollTop); + // this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft()); } // --- width From 5fe2da599700f44420c7c798c49f247fc8b0d500 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:22:33 -0700 Subject: [PATCH 002/286] More --- src/vs/editor/browser/view/viewLayer.ts | 55 ++++++++++++++++--------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 6e16f0da6a5..c23dc8e4e60 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -257,6 +257,7 @@ export class VisibleLinesCollection { private readonly _linesCollection: RenderedLinesCollection; private readonly _canvas: HTMLCanvasElement; + private readonly _textureAtlasCanvas: HTMLCanvasElement; constructor(host: IVisibleLinesHost) { this._host = host; @@ -267,6 +268,15 @@ export class VisibleLinesCollection { this._canvas.style.width = '100%'; this.domNode.domNode.appendChild(this._canvas); + const textureAtlasCanvas = this._textureAtlasCanvas = document.createElement('canvas'); + const textureAtlasCtx = textureAtlasCanvas.getContext('2d')!; + const style = getActiveWindow().getComputedStyle(this.domNode.domNode); + textureAtlasCtx.fillStyle = '#fff'; + textureAtlasCtx.font = `${style.fontSize} ${style.fontFamily}`; + textureAtlasCtx.textBaseline = 'top'; + textureAtlasCtx.fillText(' ', 0, 0); + textureAtlasCtx.fillText('x', 50, 0); + this._linesCollection = new RenderedLinesCollection(() => this._host.createVisibleLine()); } @@ -363,7 +373,7 @@ export class VisibleLinesCollection { } else { this._canvas.width = this.domNode.domNode.clientWidth; this._canvas.height = this.domNode.domNode.clientHeight; - renderer = new CanvasViewLayerRenderer(this._canvas, this._host, viewportData); + renderer = new CanvasViewLayerRenderer(this._canvas, this._host, viewportData, this._textureAtlasCanvas); } @@ -636,6 +646,7 @@ class ViewLayerRenderer { } } +const useWebgpu = true; class CanvasViewLayerRenderer { @@ -645,38 +656,44 @@ class CanvasViewLayerRenderer { private readonly _ctx: CanvasRenderingContext2D; - constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { + constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData, private readonly _textureAtlasCanvas: HTMLCanvasElement) { this.domNode = domNode; this.host = host; this.viewportData = viewportData; this._ctx = this.domNode.getContext('2d')!; - this._ctx.fillStyle = '#fff'; - const style = getActiveWindow().getComputedStyle(this.domNode); - this._ctx.font = `${style.fontSize} ${style.fontFamily}`; - this._ctx.textBaseline = 'top'; + // const style = getActiveWindow().getComputedStyle(this.domNode); + // this._ctx.font = `${style.fontSize} ${style.fontFamily}`; + // this._ctx.textBaseline = 'top'; } public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { - - this._ctx.clearRect(0, 0, this.domNode.width, this.domNode.height); - const ctx: IRendererContext = { rendLineNumberStart: inContext.rendLineNumberStart, lines: inContext.lines.slice(0), linesLength: inContext.linesLength }; + if (useWebgpu) { + } else { + this._ctx.clearRect(0, 0, this.domNode.width, this.domNode.height); - let i = 0; - let scrollTop = parseInt(this.domNode.parentElement!.getAttribute('data-adjusted-scroll-top')!); - if (Number.isNaN(scrollTop)) { - scrollTop = 0; - } - for (let lineNumber = startLineNumber; lineNumber <= stopLineNumber; lineNumber++) { - const y = Math.round((-scrollTop + deltaTop[lineNumber - startLineNumber]) * getActiveWindow().devicePixelRatio); - // console.log(this.viewportData.getViewLineRenderingData(lineNumber).content, 0, y); - this._ctx.fillText(this.viewportData.getViewLineRenderingData(lineNumber).content, 0, y); - i++; + let i = 0; + let scrollTop = parseInt(this.domNode.parentElement!.getAttribute('data-adjusted-scroll-top')!); + if (Number.isNaN(scrollTop)) { + scrollTop = 0; + } + for (let lineNumber = startLineNumber; lineNumber <= stopLineNumber; lineNumber++) { + const y = Math.round((-scrollTop + deltaTop[lineNumber - startLineNumber])); + // console.log(this.viewportData.getViewLineRenderingData(lineNumber).content, 0, y); + const content = this.viewportData.getViewLineRenderingData(lineNumber).content; + for (let x = 0; x < content.length; x++) { + if (content.charAt(x) === ' ') { + continue; + } + this._ctx.drawImage(this._textureAtlasCanvas, 50, 0, 7, this.viewportData.lineHeight, x * 7, y + 2/* offset x char */, 7, this.viewportData.lineHeight); + } + i++; + } } return ctx; From 679935d556b9115fefdbd24bd3460c47f610806a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 22 Mar 2024 23:23:35 -0700 Subject: [PATCH 003/286] Get webgpu rendering a char --- package.json | 1 + src/tsconfig.json | 3 +- src/vs/editor/browser/view/viewLayer.ts | 431 +++++++++++++++++++++++- yarn.lock | 5 + 4 files changed, 430 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 40300428d56..47c795ab720 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "@vscode/test-web": "^0.0.50", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", + "@webgpu/types": "^0.1.40", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", diff --git a/src/tsconfig.json b/src/tsconfig.json index 1e0c8b14a1a..080fac86bb2 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -13,7 +13,8 @@ "sinon", "winreg", "trusted-types", - "wicg-file-system-access" + "wicg-file-system-access", + "@webgpu/types" ], "plugins": [ { diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index c23dc8e4e60..dc57233c6d1 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -269,8 +269,14 @@ export class VisibleLinesCollection { this.domNode.domNode.appendChild(this._canvas); const textureAtlasCanvas = this._textureAtlasCanvas = document.createElement('canvas'); + textureAtlasCanvas.width = 100; + textureAtlasCanvas.height = 100; const textureAtlasCtx = textureAtlasCanvas.getContext('2d')!; const style = getActiveWindow().getComputedStyle(this.domNode.domNode); + for (let i = 0; i < textureAtlasCanvas.width; i++) { + textureAtlasCtx.fillStyle = `#00${((i / 100) * 255).toString(16)}00`; + textureAtlasCtx.fillRect(i, 0, 1, textureAtlasCanvas.height); + } textureAtlasCtx.fillStyle = '#fff'; textureAtlasCtx.font = `${style.fontSize} ${style.fontFamily}`; textureAtlasCtx.textBaseline = 'top'; @@ -364,6 +370,8 @@ export class VisibleLinesCollection { return this._linesCollection.getLine(lineNumber); } + private _canvasRenderer: CanvasViewLayerRenderer | undefined; + public renderLines(viewportData: ViewportData, viewOverlays?: boolean): void { const inp = this._linesCollection._get(); @@ -373,10 +381,13 @@ export class VisibleLinesCollection { } else { this._canvas.width = this.domNode.domNode.clientWidth; this._canvas.height = this.domNode.domNode.clientHeight; - renderer = new CanvasViewLayerRenderer(this._canvas, this._host, viewportData, this._textureAtlasCanvas); + if (!this._canvasRenderer) { + this._canvasRenderer = new CanvasViewLayerRenderer(this._canvas, this._host, viewportData, this._textureAtlasCanvas); + } + renderer = this._canvasRenderer; + renderer.update(viewportData); } - const ctx: IRendererContext = { rendLineNumberStart: inp.rendLineNumberStart, lines: inp.lines, @@ -648,23 +659,355 @@ class ViewLayerRenderer { const useWebgpu = true; +const enum Constants { + IndicesPerCell = 6 +} + +const enum BindingId { + SpriteInfo = 0, + DynamicUnitInfo = 1, + TextureSampler = 2, + Texture = 3, + Uniforms = 4, + TextureInfoUniform = 5, +} + +const wgsl = ` +struct Uniforms { + canvasDimensions: vec2f, +}; + +struct TextureInfoUniform { + spriteSheetSize: vec2f, +} + +struct SpriteInfo { + position: vec2f, + size: vec2f, +}; + +struct Vertex { + @location(0) position: vec2f, +}; + +struct DynamicUnitInfo { + position: vec2f, + dimensions: vec2f, + unused: f32, + textureId: f32, +}; + +struct VSOutput { + @builtin(position) position: vec4f, + @location(0) texcoord: vec2f, +}; + +@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; +@group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; + +@group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; +@group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; + +@vertex fn vs( + vert: Vertex, + @builtin(instance_index) instanceIndex: u32, + @builtin(vertex_index) vertexIndex : u32 +) -> VSOutput { + let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; + let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureId)]; + + var vsOut: VSOutput; + vsOut.position = vec4f( + (((vert.position * 2 - 1) / uniforms.canvasDimensions)) * dynamicUnitInfo.dimensions + dynamicUnitInfo.position, + 0.0, + 1.0 + ); + + // Textures are flipped from natural direction on the y-axis, so flip it back + vsOut.texcoord = vec2f(vert.position.x, 1.0 - vert.position.y); + vsOut.texcoord = ( + // Sprite offset (0-1) + (spriteInfo.position / textureInfoUniform.spriteSheetSize) + + // Sprite coordinate (0-1) + (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize)) + ); + + return vsOut; +} + +@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; + +@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { + // var a = textureSample(ourTexture, ourSampler, vsOut.texcoord); + // return vec4f(1.0, 0.0, 0.0, 1.0); + return textureSample(ourTexture, ourSampler, vsOut.texcoord); +} +`; + class CanvasViewLayerRenderer { readonly domNode: HTMLCanvasElement; - readonly host: IVisibleLinesHost; - readonly viewportData: ViewportData; + host: IVisibleLinesHost; + viewportData: ViewportData; - private readonly _ctx: CanvasRenderingContext2D; + private readonly _ctx!: CanvasRenderingContext2D; + + private readonly _gpuCtx!: GPUCanvasContext; + + private _adapter!: GPUAdapter; + private _device!: GPUDevice; + private _renderPassDescriptor!: GPURenderPassDescriptor; + private _bindGroup!: GPUBindGroup; + private _pipeline!: GPURenderPipeline; + + private _dataBindBuffer!: GPUBuffer; + private _dataValueBuffers!: ArrayBuffer[]; + private _dataValuesBufferActiveIndex: number = 0; + + private _vertexBuffer!: GPUBuffer; + private _squareVertices!: { vertexData: Float32Array; numVertices: number }; + + private _initialized = false; constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData, private readonly _textureAtlasCanvas: HTMLCanvasElement) { this.domNode = domNode; this.host = host; this.viewportData = viewportData; - this._ctx = this.domNode.getContext('2d')!; - // const style = getActiveWindow().getComputedStyle(this.domNode); - // this._ctx.font = `${style.fontSize} ${style.fontFamily}`; - // this._ctx.textBaseline = 'top'; + if (useWebgpu) { + this._gpuCtx = this.domNode.getContext('webgpu')!; + this.initWebgpu(); + } else { + this._ctx = this.domNode.getContext('2d')!; + } + } + + async initWebgpu() { + if (!navigator.gpu) { + throw new Error('this browser does not support WebGPU'); + } + + this._adapter = (await navigator.gpu.requestAdapter())!; + if (!this._adapter) { + throw new Error('this browser supports webgpu but it appears disabled'); + } + + this._device = await this._adapter.requestDevice(); + + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + this._gpuCtx.configure({ + device: this._device, + format: presentationFormat, + }); + + const module = this._device.createShaderModule({ + label: 'ViewLayer shader module', + code: wgsl, + }); + + this._pipeline = this._device.createRenderPipeline({ + label: 'ViewLayer render pipeline', + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + buffers: [ + { + arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each + attributes: [ + { shaderLocation: 0, offset: 0, format: 'float32x2' }, // position + ], + } + ] + }, + fragment: { + module, + entryPoint: 'fs', + targets: [ + { + format: presentationFormat, + blend: { + color: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha' + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha' + }, + }, + } + ], + }, + }); + + + + // Write standard uniforms + const enum UniformBufferInfo { + Size = 2, // 2x 32 bit floats + OffsetCanvasWidth = 0, + OffsetCanvasHeight = 1 + } + const uniformBuffer = this._device.createBuffer({ + size: UniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + { + const uniformValues = new Float32Array(UniformBufferInfo.Size); + // TODO: Update on canvas resize + uniformValues[UniformBufferInfo.OffsetCanvasWidth] = this.domNode.width; + uniformValues[UniformBufferInfo.OffsetCanvasHeight] = this.domNode.height; + this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); + } + + + + // Upload texture bitmap from atlas + const textureAtlasGpuTexture = this._device.createTexture({ + format: 'rgba8unorm', + size: { width: this._textureAtlasCanvas.width, height: this._textureAtlasCanvas.height }, + usage: GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + this._device.queue.copyExternalImageToTexture( + { source: this._textureAtlasCanvas }, + { texture: textureAtlasGpuTexture }, + { width: this._textureAtlasCanvas.width, height: this._textureAtlasCanvas.height }, + ); + + + + const enum TextureInfoUniformBufferInfo { + Size = 2, + SpriteSheetSize = 0, + } + const textureInfoUniformBufferSize = TextureInfoUniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; + const textureInfoUniformBuffer = this._device.createBuffer({ + size: textureInfoUniformBufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + { + const uniformValues = new Float32Array(TextureInfoUniformBufferInfo.Size); + // TODO: Update on canvas resize + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = this._textureAtlasCanvas.width; + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = this._textureAtlasCanvas.height; + this._device.queue.writeBuffer(textureInfoUniformBuffer, 0, uniformValues); + } + + + const maxRenderedObjects = 10; + + /////////////////// + // Static buffer // + /////////////////// + const enum SpriteInfoStorageBufferInfo { + Size = 2 + 2, + Offset_TexturePosition = 0, + Offset_TextureSize = 2, + } + const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; + const spriteInfoStorageBuffer = this._device.createBuffer({ + label: 'Entity static info buffer', + size: spriteInfoStorageBufferByteSize * maxRenderedObjects, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + { + const sprites: { x: number; y: number; w: number; h: number }[] = [ + { x: 0, y: 0, w: 7, h: 16 }, + { x: 50, y: 0, w: 7, h: 10 } + ]; + const bufferSize = spriteInfoStorageBufferByteSize * sprites.length; + const values = new Float32Array(bufferSize / 4); + let entryOffset = 0; + for (const t of sprites) { + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = t.x; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = t.y; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = t.w; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = t.h; + entryOffset += SpriteInfoStorageBufferInfo.Size; + } + this._device.queue.writeBuffer(spriteInfoStorageBuffer, 0, values); + } + + + + const cellCount = 2; + const bufferSize = cellCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._dataBindBuffer = this._device.createBuffer({ + label: 'Entity dynamic info buffer', + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + this._dataValueBuffers = [ + new ArrayBuffer(bufferSize), + new ArrayBuffer(bufferSize), + ]; + this._updateSquareVertices(); + + + + const sampler = this._device.createSampler({ + magFilter: 'nearest', + minFilter: 'nearest', + }); + this._bindGroup = this._device.createBindGroup({ + label: 'ViewLayer bind group', + layout: this._pipeline.getBindGroupLayout(0), + entries: [ + { binding: BindingId.SpriteInfo, resource: { buffer: spriteInfoStorageBuffer } }, + { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._dataBindBuffer } }, + { binding: BindingId.TextureSampler, resource: sampler }, + { binding: BindingId.Texture, resource: textureAtlasGpuTexture.createView() }, + { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, + { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, + ], + }); + + this._renderPassDescriptor = { + label: 'ViewLayer render pass', + colorAttachments: [ + ( + { + // view: <- to be filled out when we render + loadValue: [0, 0, 0, 0], + loadOp: 'load', + storeOp: 'store', + } as Omit + ) as any as GPURenderPassColorAttachment, + ] as any as Iterable, + }; + + + this._initialized = true; + } + + private _updateSquareVertices() { + this._squareVertices = { + vertexData: new Float32Array([ + 1, 0, + 1, 1, + 0, 1, + 0, 0, + 0, 1, + 1, 0, + ]), + numVertices: 6 + }; + const { vertexData } = this._squareVertices; + + this._vertexBuffer = this._device.createBuffer({ + label: 'vertex buffer vertices', + size: vertexData.byteLength, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }); + this._device.queue.writeBuffer(this._vertexBuffer, 0, vertexData); + } + + update(viewportData: ViewportData) { + this.viewportData = viewportData; } public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { @@ -673,7 +1016,12 @@ class CanvasViewLayerRenderer { lines: inContext.lines.slice(0), linesLength: inContext.linesLength }; + if (useWebgpu) { + if (!this._initialized) { + return ctx; + } + return this._renderWebgpu(ctx, startLineNumber, stopLineNumber, deltaTop); } else { this._ctx.clearRect(0, 0, this.domNode.width, this.domNode.height); @@ -698,4 +1046,69 @@ class CanvasViewLayerRenderer { return ctx; } + + private _renderWebgpu(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { + + const visibleObjectCount = this._updateDataBuffer(); + + // Write buffer and swap it out to unblock writes + const dataBuffer = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); + this._device.queue.writeBuffer(this._dataBindBuffer, 0, dataBuffer, 0, visibleObjectCount * Constants.IndicesPerCell); + + this._dataValuesBufferActiveIndex = (this._dataValuesBufferActiveIndex + 1) % 2; + + const encoder = this._device.createCommandEncoder(); + + (this._renderPassDescriptor.colorAttachments as any)[0].view = this._gpuCtx.getCurrentTexture().createView(); + const pass = encoder.beginRenderPass(this._renderPassDescriptor); + pass.setPipeline(this._pipeline); + pass.setVertexBuffer(0, this._vertexBuffer); + + pass.setBindGroup(0, this._bindGroup); + // TODO: Draws could be split by chunk, this would help minimize moving data around in arrays + pass.draw(this._squareVertices.numVertices, visibleObjectCount); + + pass.end(); + + const commandBuffer = encoder.finish(); + + this._device.queue.submit([commandBuffer]); + + return ctx; + } + + private _updateDataBuffer() { + let screenAbsoluteX: number = 0; + let screenAbsoluteY: number = 0; + let zeroToOneX: number = 0; + let zeroToOneY: number = 0; + let wgslX: number = 0; + let wgslY: number = 0; + + screenAbsoluteX = 100; + screenAbsoluteY = 100; + + screenAbsoluteX = Math.round(screenAbsoluteX); + screenAbsoluteY = Math.round(screenAbsoluteY); + zeroToOneX = screenAbsoluteX / this.domNode.width; + zeroToOneY = screenAbsoluteY / this.domNode.height; + wgslX = zeroToOneX * 2 - 1; + wgslY = zeroToOneY * 2 - 1; + + const offset = 0; + const objectCount = 1; + const data = new Float32Array(objectCount * Constants.IndicesPerCell); + data[offset] = wgslX; // x + data[offset + 1] = -wgslY; // y + data[offset + 2] = 7; // width + data[offset + 3] = 10; // height + data[offset + 4] = 0; // unused + data[offset + 5] = 1; // textureIndex + + + + const storageValues = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); + storageValues.set(data); + return objectCount; + } } diff --git a/yarn.lock b/yarn.lock index 997b34f02cf..49735b48cea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1685,6 +1685,11 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" +"@webgpu/types@^0.1.40": + version "0.1.40" + resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.40.tgz#cf72d1df6f9f8adc5d39556041f20ff2e8a58885" + integrity sha512-/BBkHLS6/eQjyWhY2H7Dx5DHcVrS2ICj9owvSRdgtQT6KcafLZA86tPze0xAOsd4FbsYKCUBUQyNi87q7gV7kw== + "@webpack-cli/configtest@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz#a69720f6c9bad6aef54a8fa6ba9c3533e7ef4c7f" From dea43019c534792227426b41f5918fc94cda6dcf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Mar 2024 17:59:14 -0700 Subject: [PATCH 004/286] Basic texture atlas allocation --- src/vs/editor/browser/view/gpu/gpuUtils.ts | 11 + .../editor/browser/view/gpu/textureAtlas.ts | 197 ++++++++++++++++++ src/vs/editor/browser/view/viewLayer.ts | 87 ++------ 3 files changed, 230 insertions(+), 65 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/gpuUtils.ts create mode 100644 src/vs/editor/browser/view/gpu/textureAtlas.ts diff --git a/src/vs/editor/browser/view/gpu/gpuUtils.ts b/src/vs/editor/browser/view/gpu/gpuUtils.ts new file mode 100644 index 00000000000..4f1d64edb0c --- /dev/null +++ b/src/vs/editor/browser/view/gpu/gpuUtils.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function ensureNonNullable(value: T | null): T { + if (!value) { + throw new Error(`Value "${value}" cannot be null`); + } + return value; +} diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts new file mode 100644 index 00000000000..266b3ccf585 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from 'vs/base/browser/dom'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; + +export class TextureAtlas extends Disposable { + private _canvas: OffscreenCanvas; + private _ctx: OffscreenCanvasRenderingContext2D; + + private _glyphMap: Map = new Map(); + + private _glyphRasterizer: GlyphRasterizer; + + public get source(): OffscreenCanvas { + return this._canvas; + } + + // TODO: Should pull in the font size from config instead of random dom node + constructor(parentDomNode: HTMLElement, maxTextureSize: number) { + super(); + + this._canvas = new OffscreenCanvas(maxTextureSize, maxTextureSize); + this._ctx = ensureNonNullable(this._canvas.getContext('2d')); + + const style = getActiveWindow().getComputedStyle(parentDomNode); + this._ctx.font = `${style.fontSize} ${style.fontFamily}`; + this._ctx.textBaseline = 'top'; + + // TODO: Device pixel ratio? + const fontSize = parseInt(style.fontSize.replace('px', '')); + this._glyphRasterizer = new GlyphRasterizer(fontSize); + + // Reduce impact of a memory leak if this object is not released + this._register(toDisposable(() => { + this._canvas.width = 1; + this._canvas.height = 1; + })); + } + + // TODO: Color, style etc. + public getGlyph(lineContent: string, glyphIndex: number): ITextureAtlasGlyph { + const chars = lineContent.charAt(glyphIndex); + const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars); + this._ctx.drawImage( + rasterizedGlyph.source, + // source + rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.top, + rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top, + // destination + 0, + 0, + rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + ); + let glyph: ITextureAtlasGlyph | undefined = this._glyphMap.get(chars); + if (!glyph) { + // TODO: Implement allocation + glyph = { + x: 0, + y: 0, + width: rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, + height: rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + }; + console.log('Allocating glyph', { + rasterizedGlyph, + glyph + }); + this._glyphMap.set(chars, glyph); + } + return glyph; + } +} + +class GlyphRasterizer extends Disposable { + private _canvas: OffscreenCanvas; + // A temporary context that glyphs are drawn to before being transfered to the atlas. + private _ctx: OffscreenCanvasRenderingContext2D; + + constructor(private readonly _fontSize: number) { + super(); + + this._canvas = new OffscreenCanvas(this._fontSize * 3, this._fontSize * 3); + this._ctx = ensureNonNullable(this._canvas.getContext('2d')); + } + + public rasterizeGlyph(chars: string): IRasterizedGlyph { + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + + this._ctx.fillStyle = '#00FF00'; + this._ctx.fillText(chars, this._fontSize, this._fontSize); + const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + // TODO: Hot path: Reuse object + const result: IRasterizedGlyph = { + source: this._canvas, + boundingBox: this._findGlyphBoundingBox(imageData) + }; + return result; + } + + private _findGlyphBoundingBox(imageData: ImageData): IBoundingBox { + // TODO: Hot path: Reuse object + const boundingBox = { + left: 0, + top: 0, + right: 0, + bottom: 0 + }; + // TODO: This could be optimized to be aware of the font size padding on all sides + const height = this._canvas.height; + const width = this._canvas.width; + let found = false; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.top = y; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.left = 0; + found = false; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.left = x; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.right = width; + found = false; + for (let x = width - 1; x >= boundingBox.left; x--) { + for (let y = 0; y < height; y++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.right = x; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.bottom = boundingBox.top; + found = false; + for (let y = height - 1; y >= 0; y--) { + for (let x = 0; x < width; x++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.bottom = y; + found = true; + break; + } + } + if (found) { + break; + } + } + return boundingBox; + } +} + +export interface ITextureAtlasGlyph { + x: number; + y: number; + width: number; + height: number; +} + +interface IBoundingBox { + left: number; + top: number; + right: number; + bottom: number; +} + +interface IRasterizedGlyph { + source: CanvasImageSource; + boundingBox: IBoundingBox; +} diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index dc57233c6d1..2c7124b730e 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; import { BugIndicatingError } from 'vs/base/common/errors'; +import { TextureAtlas } from 'vs/editor/browser/view/gpu/textureAtlas'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import * as viewEvents from 'vs/editor/common/viewEvents'; @@ -257,7 +257,6 @@ export class VisibleLinesCollection { private readonly _linesCollection: RenderedLinesCollection; private readonly _canvas: HTMLCanvasElement; - private readonly _textureAtlasCanvas: HTMLCanvasElement; constructor(host: IVisibleLinesHost) { this._host = host; @@ -268,21 +267,6 @@ export class VisibleLinesCollection { this._canvas.style.width = '100%'; this.domNode.domNode.appendChild(this._canvas); - const textureAtlasCanvas = this._textureAtlasCanvas = document.createElement('canvas'); - textureAtlasCanvas.width = 100; - textureAtlasCanvas.height = 100; - const textureAtlasCtx = textureAtlasCanvas.getContext('2d')!; - const style = getActiveWindow().getComputedStyle(this.domNode.domNode); - for (let i = 0; i < textureAtlasCanvas.width; i++) { - textureAtlasCtx.fillStyle = `#00${((i / 100) * 255).toString(16)}00`; - textureAtlasCtx.fillRect(i, 0, 1, textureAtlasCanvas.height); - } - textureAtlasCtx.fillStyle = '#fff'; - textureAtlasCtx.font = `${style.fontSize} ${style.fontFamily}`; - textureAtlasCtx.textBaseline = 'top'; - textureAtlasCtx.fillText(' ', 0, 0); - textureAtlasCtx.fillText('x', 50, 0); - this._linesCollection = new RenderedLinesCollection(() => this._host.createVisibleLine()); } @@ -382,7 +366,7 @@ export class VisibleLinesCollection { this._canvas.width = this.domNode.domNode.clientWidth; this._canvas.height = this.domNode.domNode.clientHeight; if (!this._canvasRenderer) { - this._canvasRenderer = new CanvasViewLayerRenderer(this._canvas, this._host, viewportData, this._textureAtlasCanvas); + this._canvasRenderer = new CanvasViewLayerRenderer(this._canvas, this._host, viewportData); } renderer = this._canvasRenderer; renderer.update(viewportData); @@ -657,8 +641,6 @@ class ViewLayerRenderer { } } -const useWebgpu = true; - const enum Constants { IndicesPerCell = 6 } @@ -751,8 +733,6 @@ class CanvasViewLayerRenderer { host: IVisibleLinesHost; viewportData: ViewportData; - private readonly _ctx!: CanvasRenderingContext2D; - private readonly _gpuCtx!: GPUCanvasContext; private _adapter!: GPUAdapter; @@ -770,17 +750,13 @@ class CanvasViewLayerRenderer { private _initialized = false; - constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData, private readonly _textureAtlasCanvas: HTMLCanvasElement) { + constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { this.domNode = domNode; this.host = host; this.viewportData = viewportData; - if (useWebgpu) { - this._gpuCtx = this.domNode.getContext('webgpu')!; - this.initWebgpu(); - } else { - this._ctx = this.domNode.getContext('2d')!; - } + this._gpuCtx = this.domNode.getContext('webgpu')!; + this.initWebgpu(); } async initWebgpu() { @@ -863,19 +839,24 @@ class CanvasViewLayerRenderer { } + // Create texture atlas + const textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); + textureAtlas.getGlyph('ABC', 0); + + // Upload texture bitmap from atlas const textureAtlasGpuTexture = this._device.createTexture({ format: 'rgba8unorm', - size: { width: this._textureAtlasCanvas.width, height: this._textureAtlasCanvas.height }, + size: { width: textureAtlas.source.width, height: textureAtlas.source.height }, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, }); this._device.queue.copyExternalImageToTexture( - { source: this._textureAtlasCanvas }, + { source: textureAtlas.source }, { texture: textureAtlasGpuTexture }, - { width: this._textureAtlasCanvas.width, height: this._textureAtlasCanvas.height }, + { width: textureAtlas.source.width, height: textureAtlas.source.height }, ); @@ -892,8 +873,8 @@ class CanvasViewLayerRenderer { { const uniformValues = new Float32Array(TextureInfoUniformBufferInfo.Size); // TODO: Update on canvas resize - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = this._textureAtlasCanvas.width; - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = this._textureAtlasCanvas.height; + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = textureAtlas.source.width; + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = textureAtlas.source.height; this._device.queue.writeBuffer(textureInfoUniformBuffer, 0, uniformValues); } @@ -916,8 +897,8 @@ class CanvasViewLayerRenderer { }); { const sprites: { x: number; y: number; w: number; h: number }[] = [ - { x: 0, y: 0, w: 7, h: 16 }, - { x: 50, y: 0, w: 7, h: 10 } + { x: 0, y: 0, w: 7, h: 10 }, + { x: 0, y: 0, w: 50, h: 50 } ]; const bufferSize = spriteInfoStorageBufferByteSize * sprites.length; const values = new Float32Array(bufferSize / 4); @@ -1017,34 +998,10 @@ class CanvasViewLayerRenderer { linesLength: inContext.linesLength }; - if (useWebgpu) { - if (!this._initialized) { - return ctx; - } - return this._renderWebgpu(ctx, startLineNumber, stopLineNumber, deltaTop); - } else { - this._ctx.clearRect(0, 0, this.domNode.width, this.domNode.height); - - let i = 0; - let scrollTop = parseInt(this.domNode.parentElement!.getAttribute('data-adjusted-scroll-top')!); - if (Number.isNaN(scrollTop)) { - scrollTop = 0; - } - for (let lineNumber = startLineNumber; lineNumber <= stopLineNumber; lineNumber++) { - const y = Math.round((-scrollTop + deltaTop[lineNumber - startLineNumber])); - // console.log(this.viewportData.getViewLineRenderingData(lineNumber).content, 0, y); - const content = this.viewportData.getViewLineRenderingData(lineNumber).content; - for (let x = 0; x < content.length; x++) { - if (content.charAt(x) === ' ') { - continue; - } - this._ctx.drawImage(this._textureAtlasCanvas, 50, 0, 7, this.viewportData.lineHeight, x * 7, y + 2/* offset x char */, 7, this.viewportData.lineHeight); - } - i++; - } + if (!this._initialized) { + return ctx; } - - return ctx; + return this._renderWebgpu(ctx, startLineNumber, stopLineNumber, deltaTop); } private _renderWebgpu(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { @@ -1100,8 +1057,8 @@ class CanvasViewLayerRenderer { const data = new Float32Array(objectCount * Constants.IndicesPerCell); data[offset] = wgslX; // x data[offset + 1] = -wgslY; // y - data[offset + 2] = 7; // width - data[offset + 3] = 10; // height + data[offset + 2] = 50;// 7; // width + data[offset + 3] = 50;//10; // height data[offset + 4] = 0; // unused data[offset + 5] = 1; // textureIndex From 2a7668408bf95b8c5999b4f25db6efed4abf0537 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Mar 2024 06:14:30 -0700 Subject: [PATCH 005/286] Move to gpuViewLayer.ts --- .../editor/browser/view/gpu/gpuViewLayer.ts | 443 ++++++++++++++++++ src/vs/editor/browser/view/viewLayer.ts | 438 +---------------- 2 files changed, 448 insertions(+), 433 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/gpuViewLayer.ts diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts new file mode 100644 index 00000000000..e9e8fd9d819 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -0,0 +1,443 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TextureAtlas } from 'vs/editor/browser/view/gpu/textureAtlas'; +import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; +import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; + +interface IRendererContext { + rendLineNumberStart: number; + lines: T[]; + linesLength: number; +} + +const enum Constants { + IndicesPerCell = 6 +} + +const enum BindingId { + SpriteInfo = 0, + DynamicUnitInfo = 1, + TextureSampler = 2, + Texture = 3, + Uniforms = 4, + TextureInfoUniform = 5, +} + +const wgsl = ` +struct Uniforms { + canvasDimensions: vec2f, +}; + +struct TextureInfoUniform { + spriteSheetSize: vec2f, +} + +struct SpriteInfo { + position: vec2f, + size: vec2f, +}; + +struct Vertex { + @location(0) position: vec2f, +}; + +struct DynamicUnitInfo { + position: vec2f, + dimensions: vec2f, + unused: f32, + textureId: f32, +}; + +struct VSOutput { + @builtin(position) position: vec4f, + @location(0) texcoord: vec2f, +}; + +@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; +@group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; + +@group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; +@group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; + +@vertex fn vs( + vert: Vertex, + @builtin(instance_index) instanceIndex: u32, + @builtin(vertex_index) vertexIndex : u32 +) -> VSOutput { + let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; + let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureId)]; + + var vsOut: VSOutput; + vsOut.position = vec4f( + (((vert.position * 2 - 1) / uniforms.canvasDimensions)) * dynamicUnitInfo.dimensions + dynamicUnitInfo.position, + 0.0, + 1.0 + ); + + // Textures are flipped from natural direction on the y-axis, so flip it back + vsOut.texcoord = vec2f(vert.position.x, 1.0 - vert.position.y); + vsOut.texcoord = ( + // Sprite offset (0-1) + (spriteInfo.position / textureInfoUniform.spriteSheetSize) + + // Sprite coordinate (0-1) + (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize)) + ); + + return vsOut; +} + +@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; + +@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { + // var a = textureSample(ourTexture, ourSampler, vsOut.texcoord); + // return vec4f(1.0, 0.0, 0.0, 1.0); + return textureSample(ourTexture, ourSampler, vsOut.texcoord); +} +`; + +export class GpuViewLayerRenderer { + + readonly domNode: HTMLCanvasElement; + host: IVisibleLinesHost; + viewportData: ViewportData; + + private readonly _gpuCtx!: GPUCanvasContext; + + private _adapter!: GPUAdapter; + private _device!: GPUDevice; + private _renderPassDescriptor!: GPURenderPassDescriptor; + private _bindGroup!: GPUBindGroup; + private _pipeline!: GPURenderPipeline; + + private _dataBindBuffer!: GPUBuffer; + private _dataValueBuffers!: ArrayBuffer[]; + private _dataValuesBufferActiveIndex: number = 0; + + private _vertexBuffer!: GPUBuffer; + private _squareVertices!: { vertexData: Float32Array; numVertices: number }; + + private _initialized = false; + + constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { + this.domNode = domNode; + this.host = host; + this.viewportData = viewportData; + + this._gpuCtx = this.domNode.getContext('webgpu')!; + this.initWebgpu(); + } + + async initWebgpu() { + if (!navigator.gpu) { + throw new Error('this browser does not support WebGPU'); + } + + this._adapter = (await navigator.gpu.requestAdapter())!; + if (!this._adapter) { + throw new Error('this browser supports webgpu but it appears disabled'); + } + + this._device = await this._adapter.requestDevice(); + + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + this._gpuCtx.configure({ + device: this._device, + format: presentationFormat, + }); + + const module = this._device.createShaderModule({ + label: 'ViewLayer shader module', + code: wgsl, + }); + + this._pipeline = this._device.createRenderPipeline({ + label: 'ViewLayer render pipeline', + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + buffers: [ + { + arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each + attributes: [ + { shaderLocation: 0, offset: 0, format: 'float32x2' }, // position + ], + } + ] + }, + fragment: { + module, + entryPoint: 'fs', + targets: [ + { + format: presentationFormat, + blend: { + color: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha' + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one-minus-src-alpha' + }, + }, + } + ], + }, + }); + + + + // Write standard uniforms + const enum UniformBufferInfo { + Size = 2, // 2x 32 bit floats + OffsetCanvasWidth = 0, + OffsetCanvasHeight = 1 + } + const uniformBuffer = this._device.createBuffer({ + size: UniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + { + const uniformValues = new Float32Array(UniformBufferInfo.Size); + // TODO: Update on canvas resize + uniformValues[UniformBufferInfo.OffsetCanvasWidth] = this.domNode.width; + uniformValues[UniformBufferInfo.OffsetCanvasHeight] = this.domNode.height; + this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); + } + + + // Create texture atlas + const textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); + textureAtlas.getGlyph('ABC', 0); + + + + // Upload texture bitmap from atlas + const textureAtlasGpuTexture = this._device.createTexture({ + format: 'rgba8unorm', + size: { width: textureAtlas.source.width, height: textureAtlas.source.height }, + usage: GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + this._device.queue.copyExternalImageToTexture( + { source: textureAtlas.source }, + { texture: textureAtlasGpuTexture }, + { width: textureAtlas.source.width, height: textureAtlas.source.height }, + ); + + + + const enum TextureInfoUniformBufferInfo { + Size = 2, + SpriteSheetSize = 0, + } + const textureInfoUniformBufferSize = TextureInfoUniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; + const textureInfoUniformBuffer = this._device.createBuffer({ + size: textureInfoUniformBufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + { + const uniformValues = new Float32Array(TextureInfoUniformBufferInfo.Size); + // TODO: Update on canvas resize + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = textureAtlas.source.width; + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = textureAtlas.source.height; + this._device.queue.writeBuffer(textureInfoUniformBuffer, 0, uniformValues); + } + + + const maxRenderedObjects = 10; + + /////////////////// + // Static buffer // + /////////////////// + const enum SpriteInfoStorageBufferInfo { + Size = 2 + 2, + Offset_TexturePosition = 0, + Offset_TextureSize = 2, + } + const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; + const spriteInfoStorageBuffer = this._device.createBuffer({ + label: 'Entity static info buffer', + size: spriteInfoStorageBufferByteSize * maxRenderedObjects, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + { + const sprites: { x: number; y: number; w: number; h: number }[] = [ + { x: 0, y: 0, w: 7, h: 10 }, + { x: 0, y: 0, w: 50, h: 50 } + ]; + const bufferSize = spriteInfoStorageBufferByteSize * sprites.length; + const values = new Float32Array(bufferSize / 4); + let entryOffset = 0; + for (const t of sprites) { + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = t.x; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = t.y; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = t.w; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = t.h; + entryOffset += SpriteInfoStorageBufferInfo.Size; + } + this._device.queue.writeBuffer(spriteInfoStorageBuffer, 0, values); + } + + + + const cellCount = 2; + const bufferSize = cellCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._dataBindBuffer = this._device.createBuffer({ + label: 'Entity dynamic info buffer', + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + this._dataValueBuffers = [ + new ArrayBuffer(bufferSize), + new ArrayBuffer(bufferSize), + ]; + this._updateSquareVertices(); + + + + const sampler = this._device.createSampler({ + magFilter: 'nearest', + minFilter: 'nearest', + }); + this._bindGroup = this._device.createBindGroup({ + label: 'ViewLayer bind group', + layout: this._pipeline.getBindGroupLayout(0), + entries: [ + { binding: BindingId.SpriteInfo, resource: { buffer: spriteInfoStorageBuffer } }, + { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._dataBindBuffer } }, + { binding: BindingId.TextureSampler, resource: sampler }, + { binding: BindingId.Texture, resource: textureAtlasGpuTexture.createView() }, + { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, + { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, + ], + }); + + this._renderPassDescriptor = { + label: 'ViewLayer render pass', + colorAttachments: [ + ( + { + // view: <- to be filled out when we render + loadValue: [0, 0, 0, 0], + loadOp: 'load', + storeOp: 'store', + } as Omit + ) as any as GPURenderPassColorAttachment, + ] as any as Iterable, + }; + + + this._initialized = true; + } + + private _updateSquareVertices() { + this._squareVertices = { + vertexData: new Float32Array([ + 1, 0, + 1, 1, + 0, 1, + 0, 0, + 0, 1, + 1, 0, + ]), + numVertices: 6 + }; + const { vertexData } = this._squareVertices; + + this._vertexBuffer = this._device.createBuffer({ + label: 'vertex buffer vertices', + size: vertexData.byteLength, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }); + this._device.queue.writeBuffer(this._vertexBuffer, 0, vertexData); + } + + update(viewportData: ViewportData) { + this.viewportData = viewportData; + } + + public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { + const ctx: IRendererContext = { + rendLineNumberStart: inContext.rendLineNumberStart, + lines: inContext.lines.slice(0), + linesLength: inContext.linesLength + }; + + if (!this._initialized) { + return ctx; + } + return this._renderWebgpu(ctx, startLineNumber, stopLineNumber, deltaTop); + } + + private _renderWebgpu(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { + + const visibleObjectCount = this._updateDataBuffer(); + + // Write buffer and swap it out to unblock writes + const dataBuffer = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); + this._device.queue.writeBuffer(this._dataBindBuffer, 0, dataBuffer, 0, visibleObjectCount * Constants.IndicesPerCell); + + this._dataValuesBufferActiveIndex = (this._dataValuesBufferActiveIndex + 1) % 2; + + const encoder = this._device.createCommandEncoder(); + + (this._renderPassDescriptor.colorAttachments as any)[0].view = this._gpuCtx.getCurrentTexture().createView(); + const pass = encoder.beginRenderPass(this._renderPassDescriptor); + pass.setPipeline(this._pipeline); + pass.setVertexBuffer(0, this._vertexBuffer); + + pass.setBindGroup(0, this._bindGroup); + // TODO: Draws could be split by chunk, this would help minimize moving data around in arrays + pass.draw(this._squareVertices.numVertices, visibleObjectCount); + + pass.end(); + + const commandBuffer = encoder.finish(); + + this._device.queue.submit([commandBuffer]); + + return ctx; + } + + private _updateDataBuffer() { + let screenAbsoluteX: number = 0; + let screenAbsoluteY: number = 0; + let zeroToOneX: number = 0; + let zeroToOneY: number = 0; + let wgslX: number = 0; + let wgslY: number = 0; + + screenAbsoluteX = 100; + screenAbsoluteY = 100; + + screenAbsoluteX = Math.round(screenAbsoluteX); + screenAbsoluteY = Math.round(screenAbsoluteY); + zeroToOneX = screenAbsoluteX / this.domNode.width; + zeroToOneY = screenAbsoluteY / this.domNode.height; + wgslX = zeroToOneX * 2 - 1; + wgslY = zeroToOneY * 2 - 1; + + const offset = 0; + const objectCount = 1; + const data = new Float32Array(objectCount * Constants.IndicesPerCell); + data[offset] = wgslX; // x + data[offset + 1] = -wgslY; // y + data[offset + 2] = 50;// 7; // width + data[offset + 3] = 50;//10; // height + data[offset + 4] = 0; // unused + data[offset + 5] = 1; // textureIndex + + + + const storageValues = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); + storageValues.set(data); + return objectCount; + } +} diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 2c7124b730e..5b79f9cebab 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -6,7 +6,7 @@ import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; import { BugIndicatingError } from 'vs/base/common/errors'; -import { TextureAtlas } from 'vs/editor/browser/view/gpu/textureAtlas'; +import { GpuViewLayerRenderer } from 'vs/editor/browser/view/gpu/gpuViewLayer'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import * as viewEvents from 'vs/editor/common/viewEvents'; @@ -354,7 +354,7 @@ export class VisibleLinesCollection { return this._linesCollection.getLine(lineNumber); } - private _canvasRenderer: CanvasViewLayerRenderer | undefined; + private _gpuRenderer: GpuViewLayerRenderer | undefined; public renderLines(viewportData: ViewportData, viewOverlays?: boolean): void { const inp = this._linesCollection._get(); @@ -365,10 +365,10 @@ export class VisibleLinesCollection { } else { this._canvas.width = this.domNode.domNode.clientWidth; this._canvas.height = this.domNode.domNode.clientHeight; - if (!this._canvasRenderer) { - this._canvasRenderer = new CanvasViewLayerRenderer(this._canvas, this._host, viewportData); + if (!this._gpuRenderer) { + this._gpuRenderer = new GpuViewLayerRenderer(this._canvas, this._host, viewportData); } - renderer = this._canvasRenderer; + renderer = this._gpuRenderer; renderer.update(viewportData); } @@ -641,431 +641,3 @@ class ViewLayerRenderer { } } -const enum Constants { - IndicesPerCell = 6 -} - -const enum BindingId { - SpriteInfo = 0, - DynamicUnitInfo = 1, - TextureSampler = 2, - Texture = 3, - Uniforms = 4, - TextureInfoUniform = 5, -} - -const wgsl = ` -struct Uniforms { - canvasDimensions: vec2f, -}; - -struct TextureInfoUniform { - spriteSheetSize: vec2f, -} - -struct SpriteInfo { - position: vec2f, - size: vec2f, -}; - -struct Vertex { - @location(0) position: vec2f, -}; - -struct DynamicUnitInfo { - position: vec2f, - dimensions: vec2f, - unused: f32, - textureId: f32, -}; - -struct VSOutput { - @builtin(position) position: vec4f, - @location(0) texcoord: vec2f, -}; - -@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; -@group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; - -@group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; -@group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; - -@vertex fn vs( - vert: Vertex, - @builtin(instance_index) instanceIndex: u32, - @builtin(vertex_index) vertexIndex : u32 -) -> VSOutput { - let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; - let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureId)]; - - var vsOut: VSOutput; - vsOut.position = vec4f( - (((vert.position * 2 - 1) / uniforms.canvasDimensions)) * dynamicUnitInfo.dimensions + dynamicUnitInfo.position, - 0.0, - 1.0 - ); - - // Textures are flipped from natural direction on the y-axis, so flip it back - vsOut.texcoord = vec2f(vert.position.x, 1.0 - vert.position.y); - vsOut.texcoord = ( - // Sprite offset (0-1) - (spriteInfo.position / textureInfoUniform.spriteSheetSize) + - // Sprite coordinate (0-1) - (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize)) - ); - - return vsOut; -} - -@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; -@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; - -@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { - // var a = textureSample(ourTexture, ourSampler, vsOut.texcoord); - // return vec4f(1.0, 0.0, 0.0, 1.0); - return textureSample(ourTexture, ourSampler, vsOut.texcoord); -} -`; - -class CanvasViewLayerRenderer { - - readonly domNode: HTMLCanvasElement; - host: IVisibleLinesHost; - viewportData: ViewportData; - - private readonly _gpuCtx!: GPUCanvasContext; - - private _adapter!: GPUAdapter; - private _device!: GPUDevice; - private _renderPassDescriptor!: GPURenderPassDescriptor; - private _bindGroup!: GPUBindGroup; - private _pipeline!: GPURenderPipeline; - - private _dataBindBuffer!: GPUBuffer; - private _dataValueBuffers!: ArrayBuffer[]; - private _dataValuesBufferActiveIndex: number = 0; - - private _vertexBuffer!: GPUBuffer; - private _squareVertices!: { vertexData: Float32Array; numVertices: number }; - - private _initialized = false; - - constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { - this.domNode = domNode; - this.host = host; - this.viewportData = viewportData; - - this._gpuCtx = this.domNode.getContext('webgpu')!; - this.initWebgpu(); - } - - async initWebgpu() { - if (!navigator.gpu) { - throw new Error('this browser does not support WebGPU'); - } - - this._adapter = (await navigator.gpu.requestAdapter())!; - if (!this._adapter) { - throw new Error('this browser supports webgpu but it appears disabled'); - } - - this._device = await this._adapter.requestDevice(); - - const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); - this._gpuCtx.configure({ - device: this._device, - format: presentationFormat, - }); - - const module = this._device.createShaderModule({ - label: 'ViewLayer shader module', - code: wgsl, - }); - - this._pipeline = this._device.createRenderPipeline({ - label: 'ViewLayer render pipeline', - layout: 'auto', - vertex: { - module, - entryPoint: 'vs', - buffers: [ - { - arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each - attributes: [ - { shaderLocation: 0, offset: 0, format: 'float32x2' }, // position - ], - } - ] - }, - fragment: { - module, - entryPoint: 'fs', - targets: [ - { - format: presentationFormat, - blend: { - color: { - srcFactor: 'one', - dstFactor: 'one-minus-src-alpha' - }, - alpha: { - srcFactor: 'one', - dstFactor: 'one-minus-src-alpha' - }, - }, - } - ], - }, - }); - - - - // Write standard uniforms - const enum UniformBufferInfo { - Size = 2, // 2x 32 bit floats - OffsetCanvasWidth = 0, - OffsetCanvasHeight = 1 - } - const uniformBuffer = this._device.createBuffer({ - size: UniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - { - const uniformValues = new Float32Array(UniformBufferInfo.Size); - // TODO: Update on canvas resize - uniformValues[UniformBufferInfo.OffsetCanvasWidth] = this.domNode.width; - uniformValues[UniformBufferInfo.OffsetCanvasHeight] = this.domNode.height; - this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); - } - - - // Create texture atlas - const textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); - textureAtlas.getGlyph('ABC', 0); - - - - // Upload texture bitmap from atlas - const textureAtlasGpuTexture = this._device.createTexture({ - format: 'rgba8unorm', - size: { width: textureAtlas.source.width, height: textureAtlas.source.height }, - usage: GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT, - }); - this._device.queue.copyExternalImageToTexture( - { source: textureAtlas.source }, - { texture: textureAtlasGpuTexture }, - { width: textureAtlas.source.width, height: textureAtlas.source.height }, - ); - - - - const enum TextureInfoUniformBufferInfo { - Size = 2, - SpriteSheetSize = 0, - } - const textureInfoUniformBufferSize = TextureInfoUniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; - const textureInfoUniformBuffer = this._device.createBuffer({ - size: textureInfoUniformBufferSize, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - { - const uniformValues = new Float32Array(TextureInfoUniformBufferInfo.Size); - // TODO: Update on canvas resize - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = textureAtlas.source.width; - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = textureAtlas.source.height; - this._device.queue.writeBuffer(textureInfoUniformBuffer, 0, uniformValues); - } - - - const maxRenderedObjects = 10; - - /////////////////// - // Static buffer // - /////////////////// - const enum SpriteInfoStorageBufferInfo { - Size = 2 + 2, - Offset_TexturePosition = 0, - Offset_TextureSize = 2, - } - const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; - const spriteInfoStorageBuffer = this._device.createBuffer({ - label: 'Entity static info buffer', - size: spriteInfoStorageBufferByteSize * maxRenderedObjects, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); - { - const sprites: { x: number; y: number; w: number; h: number }[] = [ - { x: 0, y: 0, w: 7, h: 10 }, - { x: 0, y: 0, w: 50, h: 50 } - ]; - const bufferSize = spriteInfoStorageBufferByteSize * sprites.length; - const values = new Float32Array(bufferSize / 4); - let entryOffset = 0; - for (const t of sprites) { - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = t.x; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = t.y; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = t.w; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = t.h; - entryOffset += SpriteInfoStorageBufferInfo.Size; - } - this._device.queue.writeBuffer(spriteInfoStorageBuffer, 0, values); - } - - - - const cellCount = 2; - const bufferSize = cellCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; - this._dataBindBuffer = this._device.createBuffer({ - label: 'Entity dynamic info buffer', - size: bufferSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); - this._dataValueBuffers = [ - new ArrayBuffer(bufferSize), - new ArrayBuffer(bufferSize), - ]; - this._updateSquareVertices(); - - - - const sampler = this._device.createSampler({ - magFilter: 'nearest', - minFilter: 'nearest', - }); - this._bindGroup = this._device.createBindGroup({ - label: 'ViewLayer bind group', - layout: this._pipeline.getBindGroupLayout(0), - entries: [ - { binding: BindingId.SpriteInfo, resource: { buffer: spriteInfoStorageBuffer } }, - { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._dataBindBuffer } }, - { binding: BindingId.TextureSampler, resource: sampler }, - { binding: BindingId.Texture, resource: textureAtlasGpuTexture.createView() }, - { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, - { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, - ], - }); - - this._renderPassDescriptor = { - label: 'ViewLayer render pass', - colorAttachments: [ - ( - { - // view: <- to be filled out when we render - loadValue: [0, 0, 0, 0], - loadOp: 'load', - storeOp: 'store', - } as Omit - ) as any as GPURenderPassColorAttachment, - ] as any as Iterable, - }; - - - this._initialized = true; - } - - private _updateSquareVertices() { - this._squareVertices = { - vertexData: new Float32Array([ - 1, 0, - 1, 1, - 0, 1, - 0, 0, - 0, 1, - 1, 0, - ]), - numVertices: 6 - }; - const { vertexData } = this._squareVertices; - - this._vertexBuffer = this._device.createBuffer({ - label: 'vertex buffer vertices', - size: vertexData.byteLength, - usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - }); - this._device.queue.writeBuffer(this._vertexBuffer, 0, vertexData); - } - - update(viewportData: ViewportData) { - this.viewportData = viewportData; - } - - public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { - const ctx: IRendererContext = { - rendLineNumberStart: inContext.rendLineNumberStart, - lines: inContext.lines.slice(0), - linesLength: inContext.linesLength - }; - - if (!this._initialized) { - return ctx; - } - return this._renderWebgpu(ctx, startLineNumber, stopLineNumber, deltaTop); - } - - private _renderWebgpu(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { - - const visibleObjectCount = this._updateDataBuffer(); - - // Write buffer and swap it out to unblock writes - const dataBuffer = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); - this._device.queue.writeBuffer(this._dataBindBuffer, 0, dataBuffer, 0, visibleObjectCount * Constants.IndicesPerCell); - - this._dataValuesBufferActiveIndex = (this._dataValuesBufferActiveIndex + 1) % 2; - - const encoder = this._device.createCommandEncoder(); - - (this._renderPassDescriptor.colorAttachments as any)[0].view = this._gpuCtx.getCurrentTexture().createView(); - const pass = encoder.beginRenderPass(this._renderPassDescriptor); - pass.setPipeline(this._pipeline); - pass.setVertexBuffer(0, this._vertexBuffer); - - pass.setBindGroup(0, this._bindGroup); - // TODO: Draws could be split by chunk, this would help minimize moving data around in arrays - pass.draw(this._squareVertices.numVertices, visibleObjectCount); - - pass.end(); - - const commandBuffer = encoder.finish(); - - this._device.queue.submit([commandBuffer]); - - return ctx; - } - - private _updateDataBuffer() { - let screenAbsoluteX: number = 0; - let screenAbsoluteY: number = 0; - let zeroToOneX: number = 0; - let zeroToOneY: number = 0; - let wgslX: number = 0; - let wgslY: number = 0; - - screenAbsoluteX = 100; - screenAbsoluteY = 100; - - screenAbsoluteX = Math.round(screenAbsoluteX); - screenAbsoluteY = Math.round(screenAbsoluteY); - zeroToOneX = screenAbsoluteX / this.domNode.width; - zeroToOneY = screenAbsoluteY / this.domNode.height; - wgslX = zeroToOneX * 2 - 1; - wgslY = zeroToOneY * 2 - 1; - - const offset = 0; - const objectCount = 1; - const data = new Float32Array(objectCount * Constants.IndicesPerCell); - data[offset] = wgslX; // x - data[offset + 1] = -wgslY; // y - data[offset + 2] = 50;// 7; // width - data[offset + 3] = 50;//10; // height - data[offset + 4] = 0; // unused - data[offset + 5] = 1; // textureIndex - - - - const storageValues = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); - storageValues.set(data); - return objectCount; - } -} From 6273f2b99a7a2745b029d2d827992e82d63d4633 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Mar 2024 06:30:25 -0700 Subject: [PATCH 006/286] Render using glyph dims --- .../editor/browser/view/gpu/gpuViewLayer.ts | 58 +++++++++---------- .../editor/browser/view/gpu/textureAtlas.ts | 27 +++++---- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index e9e8fd9d819..2f911dca8b7 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextureAtlas } from 'vs/editor/browser/view/gpu/textureAtlas'; +import { TextureAtlas, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -14,10 +14,11 @@ interface IRendererContext { } const enum Constants { - IndicesPerCell = 6 + IndicesPerCell = 3 } const enum BindingId { + // TODO: Improve names SpriteInfo = 0, DynamicUnitInfo = 1, TextureSampler = 2, @@ -46,8 +47,6 @@ struct Vertex { struct DynamicUnitInfo { position: vec2f, - dimensions: vec2f, - unused: f32, textureId: f32, }; @@ -72,7 +71,7 @@ struct VSOutput { var vsOut: VSOutput; vsOut.position = vec4f( - (((vert.position * 2 - 1) / uniforms.canvasDimensions)) * dynamicUnitInfo.dimensions + dynamicUnitInfo.position, + (((vert.position * 2 - 1) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position, 0.0, 1.0 ); @@ -213,23 +212,6 @@ export class GpuViewLayerRenderer { // Create texture atlas const textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); - textureAtlas.getGlyph('ABC', 0); - - - - // Upload texture bitmap from atlas - const textureAtlasGpuTexture = this._device.createTexture({ - format: 'rgba8unorm', - size: { width: textureAtlas.source.width, height: textureAtlas.source.height }, - usage: GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT, - }); - this._device.queue.copyExternalImageToTexture( - { source: textureAtlas.source }, - { texture: textureAtlasGpuTexture }, - { width: textureAtlas.source.width, height: textureAtlas.source.height }, - ); @@ -268,9 +250,11 @@ export class GpuViewLayerRenderer { usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); { - const sprites: { x: number; y: number; w: number; h: number }[] = [ - { x: 0, y: 0, w: 7, h: 10 }, - { x: 0, y: 0, w: 50, h: 50 } + const glyph = textureAtlas.getGlyph('ABC', 0); + const sprites: ITextureAtlasGlyph[] = [ + glyph, + glyph + // { x: 0, y: 0, w: 50, h: 50 }, ]; const bufferSize = spriteInfoStorageBufferByteSize * sprites.length; const values = new Float32Array(bufferSize / 4); @@ -287,6 +271,25 @@ export class GpuViewLayerRenderer { + + + + // Upload texture bitmap from atlas + const textureAtlasGpuTexture = this._device.createTexture({ + format: 'rgba8unorm', + size: { width: textureAtlas.source.width, height: textureAtlas.source.height }, + usage: GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + this._device.queue.copyExternalImageToTexture( + { source: textureAtlas.source }, + { texture: textureAtlasGpuTexture }, + { width: textureAtlas.source.width, height: textureAtlas.source.height }, + ); + + + const cellCount = 2; const bufferSize = cellCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._dataBindBuffer = this._device.createBuffer({ @@ -429,10 +432,7 @@ export class GpuViewLayerRenderer { const data = new Float32Array(objectCount * Constants.IndicesPerCell); data[offset] = wgslX; // x data[offset + 1] = -wgslY; // y - data[offset + 2] = 50;// 7; // width - data[offset + 3] = 50;//10; // height - data[offset + 4] = 0; // unused - data[offset + 5] = 1; // textureIndex + data[offset + 2] = 1; // textureIndex diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 266b3ccf585..86ee8896067 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -26,13 +26,13 @@ export class TextureAtlas extends Disposable { this._canvas = new OffscreenCanvas(maxTextureSize, maxTextureSize); this._ctx = ensureNonNullable(this._canvas.getContext('2d')); - const style = getActiveWindow().getComputedStyle(parentDomNode); - this._ctx.font = `${style.fontSize} ${style.fontFamily}`; + const activeWindow = getActiveWindow(); + const style = activeWindow.getComputedStyle(parentDomNode); + const fontSize = Math.ceil(parseInt(style.fontSize.replace('px', '')) * activeWindow.devicePixelRatio); + this._ctx.font = `${fontSize}px ${style.fontFamily}`; this._ctx.textBaseline = 'top'; - // TODO: Device pixel ratio? - const fontSize = parseInt(style.fontSize.replace('px', '')); - this._glyphRasterizer = new GlyphRasterizer(fontSize); + this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); // Reduce impact of a memory leak if this object is not released this._register(toDisposable(() => { @@ -64,8 +64,8 @@ export class TextureAtlas extends Disposable { glyph = { x: 0, y: 0, - width: rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, - height: rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + w: rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, + h: rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top }; console.log('Allocating glyph', { rasterizedGlyph, @@ -82,18 +82,23 @@ class GlyphRasterizer extends Disposable { // A temporary context that glyphs are drawn to before being transfered to the atlas. private _ctx: OffscreenCanvasRenderingContext2D; - constructor(private readonly _fontSize: number) { + constructor(private readonly _fontSize: number, fontFamily: string) { super(); this._canvas = new OffscreenCanvas(this._fontSize * 3, this._fontSize * 3); this._ctx = ensureNonNullable(this._canvas.getContext('2d')); + this._ctx.font = `${this._fontSize}px ${fontFamily}`; + this._ctx.fillStyle = '#00FF00'; } + // TODO: Support drawing multiple fonts and sizes + // TODO: Should pull in the font size from config instead of random dom node public rasterizeGlyph(chars: string): IRasterizedGlyph { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - this._ctx.fillStyle = '#00FF00'; + // TODO: Draw in middle using alphabetical baseline this._ctx.fillText(chars, this._fontSize, this._fontSize); + const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); // TODO: Hot path: Reuse object const result: IRasterizedGlyph = { @@ -180,8 +185,8 @@ class GlyphRasterizer extends Disposable { export interface ITextureAtlasGlyph { x: number; y: number; - width: number; - height: number; + w: number; + h: number; } interface IBoundingBox { From 207fb91a209608de35e6688a1a192a3e5d7e345f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Mar 2024 06:43:13 -0700 Subject: [PATCH 007/286] Fix text alpha --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 2f911dca8b7..4386a0217eb 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -176,11 +176,11 @@ export class GpuViewLayerRenderer { format: presentationFormat, blend: { color: { - srcFactor: 'one', + srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha' }, alpha: { - srcFactor: 'one', + srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha' }, }, From cf4008bb7463b195a6acb3efef0761fb609b8fce Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Mar 2024 06:44:56 -0700 Subject: [PATCH 008/286] Fix canvas scale --- src/vs/editor/browser/view/viewLayer.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 5b79f9cebab..6798fd03968 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getActiveWindow } from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; import { BugIndicatingError } from 'vs/base/common/errors'; @@ -363,8 +364,9 @@ export class VisibleLinesCollection { if (viewOverlays) { renderer = new ViewLayerRenderer(this.domNode.domNode, this._host, viewportData); } else { - this._canvas.width = this.domNode.domNode.clientWidth; - this._canvas.height = this.domNode.domNode.clientHeight; + const activeWindow = getActiveWindow(); + this._canvas.width = this.domNode.domNode.clientWidth * activeWindow.devicePixelRatio; + this._canvas.height = this.domNode.domNode.clientHeight * activeWindow.devicePixelRatio; if (!this._gpuRenderer) { this._gpuRenderer = new GpuViewLayerRenderer(this._canvas, this._host, viewportData); } From 239c15e4a972aa9079c77a0e78fa5b3e3cd30113 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Mar 2024 06:46:30 -0700 Subject: [PATCH 009/286] Exit early on hot path --- .../editor/browser/view/gpu/textureAtlas.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 86ee8896067..d02ce16bb13 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -59,20 +59,21 @@ export class TextureAtlas extends Disposable { rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top ); let glyph: ITextureAtlasGlyph | undefined = this._glyphMap.get(chars); - if (!glyph) { - // TODO: Implement allocation - glyph = { - x: 0, - y: 0, - w: rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, - h: rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top - }; - console.log('Allocating glyph', { - rasterizedGlyph, - glyph - }); - this._glyphMap.set(chars, glyph); + if (glyph) { + return glyph; } + // TODO: Implement allocation + glyph = { + x: 0, + y: 0, + w: rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, + h: rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + }; + console.log('Allocating glyph', { + rasterizedGlyph, + glyph + }); + this._glyphMap.set(chars, glyph); return glyph; } } @@ -88,7 +89,7 @@ class GlyphRasterizer extends Disposable { this._canvas = new OffscreenCanvas(this._fontSize * 3, this._fontSize * 3); this._ctx = ensureNonNullable(this._canvas.getContext('2d')); this._ctx.font = `${this._fontSize}px ${fontFamily}`; - this._ctx.fillStyle = '#00FF00'; + this._ctx.fillStyle = '#FFFFFF'; } // TODO: Support drawing multiple fonts and sizes From ab97abc95b99be79a76819bd5e2d5c587def57f8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Mar 2024 06:53:21 -0700 Subject: [PATCH 010/286] Track device pixel dimensions of canvas --- src/vs/editor/browser/view/gpu/gpuUtils.ts | 36 +++++++++++++++++++ .../editor/browser/view/gpu/gpuViewLayer.ts | 2 -- .../editor/browser/view/gpu/textureAtlas.ts | 1 - src/vs/editor/browser/view/viewLayer.ts | 15 +++++--- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuUtils.ts b/src/vs/editor/browser/view/gpu/gpuUtils.ts index 4f1d64edb0c..66497f1c205 100644 --- a/src/vs/editor/browser/view/gpu/gpuUtils.ts +++ b/src/vs/editor/browser/view/gpu/gpuUtils.ts @@ -3,9 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { toDisposable, type IDisposable } from 'vs/base/common/lifecycle'; + export function ensureNonNullable(value: T | null): T { if (!value) { throw new Error(`Value "${value}" cannot be null`); } return value; } + +export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { + // Observe any resizes to the element and extract the actual pixel size of the element if the + // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when + // converting between CSS pixels and device pixels which causes blurry rendering when device + // pixel ratio is not a round number. + let observer: ResizeObserver | undefined = new parentWindow.ResizeObserver((entries) => { + const entry = entries.find((entry) => entry.target === element); + if (!entry) { + return; + } + + // Disconnect if devicePixelContentBoxSize isn't supported by the browser + if (!('devicePixelContentBoxSize' in entry)) { + observer?.disconnect(); + observer = undefined; + return; + } + + // Fire the callback, ignore events where the dimensions are 0x0 as the canvas is likely hidden + const width = entry.devicePixelContentBoxSize[0].inlineSize; + const height = entry.devicePixelContentBoxSize[0].blockSize; + if (width > 0 && height > 0) { + callback(width, height); + } + }); + try { + observer.observe(element, { box: ['device-pixel-content-box'] } as any); + } catch { + observer.disconnect(); + observer = undefined; + } + return toDisposable(() => observer?.disconnect()); +} diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 4386a0217eb..a96cdb33ab3 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -434,8 +434,6 @@ export class GpuViewLayerRenderer { data[offset + 1] = -wgslY; // y data[offset + 2] = 1; // textureIndex - - const storageValues = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); storageValues.set(data); return objectCount; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index d02ce16bb13..e52bd360d41 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -30,7 +30,6 @@ export class TextureAtlas extends Disposable { const style = activeWindow.getComputedStyle(parentDomNode); const fontSize = Math.ceil(parseInt(style.fontSize.replace('px', '')) * activeWindow.devicePixelRatio); this._ctx.font = `${fontSize}px ${style.fontFamily}`; - this._ctx.textBaseline = 'top'; this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 6798fd03968..948a739b4fd 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -7,6 +7,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; import { BugIndicatingError } from 'vs/base/common/errors'; +import { observeDevicePixelDimensions } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GpuViewLayerRenderer } from 'vs/editor/browser/view/gpu/gpuViewLayer'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; @@ -266,7 +267,6 @@ export class VisibleLinesCollection { this._canvas = document.createElement('canvas'); this._canvas.style.height = '100%'; this._canvas.style.width = '100%'; - this.domNode.domNode.appendChild(this._canvas); this._linesCollection = new RenderedLinesCollection(() => this._host.createVisibleLine()); } @@ -364,9 +364,16 @@ export class VisibleLinesCollection { if (viewOverlays) { renderer = new ViewLayerRenderer(this.domNode.domNode, this._host, viewportData); } else { - const activeWindow = getActiveWindow(); - this._canvas.width = this.domNode.domNode.clientWidth * activeWindow.devicePixelRatio; - this._canvas.height = this.domNode.domNode.clientHeight * activeWindow.devicePixelRatio; + // If not yet attached, listen for device pixel size and attach + if (!this._canvas.parentElement) { + // TODO: Track disposable + observeDevicePixelDimensions(this._canvas, getActiveWindow(), (w, h) => { + this._canvas.width = w; + this._canvas.height = h; + }); + this.domNode.domNode.appendChild(this._canvas); + } + if (!this._gpuRenderer) { this._gpuRenderer = new GpuViewLayerRenderer(this._canvas, this._host, viewportData); } From bcb90fc6f23e750afe3673c28ca8885deea4a3d9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Mar 2024 08:48:08 -0700 Subject: [PATCH 011/286] Render characters in approximately the right position --- .../editor/browser/view/gpu/gpuViewLayer.ts | 106 +++++++++++++----- .../editor/browser/view/gpu/textureAtlas.ts | 5 + 2 files changed, 83 insertions(+), 28 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index a96cdb33ab3..b5270ce35cf 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getActiveWindow } from 'vs/base/browser/dom'; import { TextureAtlas, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -14,7 +15,7 @@ interface IRendererContext { } const enum Constants { - IndicesPerCell = 3 + IndicesPerCell = 6 } const enum BindingId { @@ -47,7 +48,9 @@ struct Vertex { struct DynamicUnitInfo { position: vec2f, + unused1: vec2f, textureId: f32, + unused2: f32 }; struct VSOutput { @@ -70,14 +73,15 @@ struct VSOutput { let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureId)]; var vsOut: VSOutput; + // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - (((vert.position * 2 - 1) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position, + (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position, 0.0, 1.0 ); // Textures are flipped from natural direction on the y-axis, so flip it back - vsOut.texcoord = vec2f(vert.position.x, 1.0 - vert.position.y); + vsOut.texcoord = vert.position; vsOut.texcoord = ( // Sprite offset (0-1) (spriteInfo.position / textureInfoUniform.spriteSheetSize) + @@ -92,8 +96,6 @@ struct VSOutput { @group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { - // var a = textureSample(ourTexture, ourSampler, vsOut.texcoord); - // return vec4f(1.0, 0.0, 0.0, 1.0); return textureSample(ourTexture, ourSampler, vsOut.texcoord); } `; @@ -119,6 +121,8 @@ export class GpuViewLayerRenderer { private _vertexBuffer!: GPUBuffer; private _squareVertices!: { vertexData: Float32Array; numVertices: number }; + private _textureAtlas!: TextureAtlas; + private _initialized = false; constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { @@ -211,7 +215,7 @@ export class GpuViewLayerRenderer { // Create texture atlas - const textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); + const textureAtlas = this._textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); @@ -290,7 +294,8 @@ export class GpuViewLayerRenderer { - const cellCount = 2; + // TODO: Grow/shrink buffer size dynamically + const cellCount = 10000; const bufferSize = cellCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._dataBindBuffer = this._device.createBuffer({ label: 'Entity dynamic info buffer', @@ -380,11 +385,11 @@ export class GpuViewLayerRenderer { } private _renderWebgpu(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { - - const visibleObjectCount = this._updateDataBuffer(); + // TODO: Improve "data" name + const dataBuffer = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); + const visibleObjectCount = this._updateDataBuffer(dataBuffer, ctx, startLineNumber, stopLineNumber, deltaTop); // Write buffer and swap it out to unblock writes - const dataBuffer = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); this._device.queue.writeBuffer(this._dataBindBuffer, 0, dataBuffer, 0, visibleObjectCount * Constants.IndicesPerCell); this._dataValuesBufferActiveIndex = (this._dataValuesBufferActiveIndex + 1) % 2; @@ -409,7 +414,9 @@ export class GpuViewLayerRenderer { return ctx; } - private _updateDataBuffer() { + // TODO: This update could be moved to an arbitrary task thread if expensive? + private _updateDataBuffer(dataBuffer: Float32Array, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { + let chars: string = ''; let screenAbsoluteX: number = 0; let screenAbsoluteY: number = 0; let zeroToOneX: number = 0; @@ -417,25 +424,68 @@ export class GpuViewLayerRenderer { let wgslX: number = 0; let wgslY: number = 0; - screenAbsoluteX = 100; - screenAbsoluteY = 100; + const activeWindow = getActiveWindow(); + let charCount = 0; + let scrollTop = parseInt(this.domNode.parentElement!.getAttribute('data-adjusted-scroll-top')!); + if (Number.isNaN(scrollTop)) { + scrollTop = 0; + } + for (let lineNumber = startLineNumber; lineNumber <= stopLineNumber; lineNumber++) { + const y = Math.round((-scrollTop + deltaTop[lineNumber - startLineNumber])); + // Offscreen + if (y < 0) { + continue; + } + const content = this.viewportData.getViewLineRenderingData(lineNumber).content; + // console.log(content, 0, y); + for (let x = 0; x < content.length; x++) { + if (content.charAt(x) === ' ') { + continue; + } + // TODO: Handle tab - screenAbsoluteX = Math.round(screenAbsoluteX); - screenAbsoluteY = Math.round(screenAbsoluteY); - zeroToOneX = screenAbsoluteX / this.domNode.width; - zeroToOneY = screenAbsoluteY / this.domNode.height; - wgslX = zeroToOneX * 2 - 1; - wgslY = zeroToOneY * 2 - 1; + chars = content[x]; + // TODO: Get glyph - const offset = 0; - const objectCount = 1; - const data = new Float32Array(objectCount * Constants.IndicesPerCell); - data[offset] = wgslX; // x - data[offset + 1] = -wgslY; // y - data[offset + 2] = 1; // textureIndex + // TODO: Move math to gpu + // TODO: Render using a line offset for partial line scrolling + // TODO: Sub-pixel rendering + screenAbsoluteX = x * 7 * activeWindow.devicePixelRatio; + // TODO: This +10 is because the glyph is being rendered in the wrong position + screenAbsoluteY = Math.round(y * activeWindow.devicePixelRatio); + zeroToOneX = screenAbsoluteX / this.domNode.width; + zeroToOneY = screenAbsoluteY / this.domNode.height; + wgslX = zeroToOneX * 2 - 1; + wgslY = zeroToOneY * 2 - 1; - const storageValues = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); - storageValues.set(data); - return objectCount; + dataBuffer[charCount * Constants.IndicesPerCell + 0] = wgslX; // x + dataBuffer[charCount * Constants.IndicesPerCell + 1] = -wgslY; // y + dataBuffer[charCount * Constants.IndicesPerCell + 2] = 0; + dataBuffer[charCount * Constants.IndicesPerCell + 3] = 0; + dataBuffer[charCount * Constants.IndicesPerCell + 4] = 1; // textureIndex + dataBuffer[charCount * Constants.IndicesPerCell + 5] = 0; + + charCount++; + } + } + // console.log('charCount: ' + charCount); + return charCount; + + + + + // screenAbsoluteX = 100; + // screenAbsoluteY = 100; + + + // const offset = 0; + // const objectCount = 1; + // const data = new Float32Array(objectCount * Constants.IndicesPerCell); + // data[offset] = wgslX; // x + // data[offset + 1] = -wgslY; // y + // data[offset + 2] = 1; // textureIndex + + // storageValues.set(data); + // return objectCount; } } diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index e52bd360d41..aee130db4cc 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -15,6 +15,8 @@ export class TextureAtlas extends Disposable { private _glyphRasterizer: GlyphRasterizer; + private _nextId = 0; + public get source(): OffscreenCanvas { return this._canvas; } @@ -63,6 +65,7 @@ export class TextureAtlas extends Disposable { } // TODO: Implement allocation glyph = { + id: this._nextId++, x: 0, y: 0, w: rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, @@ -108,6 +111,7 @@ class GlyphRasterizer extends Disposable { return result; } + // TODO: Pass back origin offset private _findGlyphBoundingBox(imageData: ImageData): IBoundingBox { // TODO: Hot path: Reuse object const boundingBox = { @@ -183,6 +187,7 @@ class GlyphRasterizer extends Disposable { } export interface ITextureAtlasGlyph { + id: number; x: number; y: number; w: number; From 738438cfd8474eea16440fc1a064c2d879f7e54a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 24 Mar 2024 08:54:34 -0700 Subject: [PATCH 012/286] Fix glyph caching order --- .../editor/browser/view/gpu/gpuViewLayer.ts | 21 ++----------------- .../editor/browser/view/gpu/textureAtlas.ts | 11 +++++----- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index b5270ce35cf..1aeea9fb7c8 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -445,7 +445,7 @@ export class GpuViewLayerRenderer { // TODO: Handle tab chars = content[x]; - // TODO: Get glyph + const glyph = this._textureAtlas.getGlyph(content, x); // TODO: Move math to gpu // TODO: Render using a line offset for partial line scrolling @@ -462,7 +462,7 @@ export class GpuViewLayerRenderer { dataBuffer[charCount * Constants.IndicesPerCell + 1] = -wgslY; // y dataBuffer[charCount * Constants.IndicesPerCell + 2] = 0; dataBuffer[charCount * Constants.IndicesPerCell + 3] = 0; - dataBuffer[charCount * Constants.IndicesPerCell + 4] = 1; // textureIndex + dataBuffer[charCount * Constants.IndicesPerCell + 4] = glyph.id; // textureIndex dataBuffer[charCount * Constants.IndicesPerCell + 5] = 0; charCount++; @@ -470,22 +470,5 @@ export class GpuViewLayerRenderer { } // console.log('charCount: ' + charCount); return charCount; - - - - - // screenAbsoluteX = 100; - // screenAbsoluteY = 100; - - - // const offset = 0; - // const objectCount = 1; - // const data = new Float32Array(objectCount * Constants.IndicesPerCell); - // data[offset] = wgslX; // x - // data[offset + 1] = -wgslY; // y - // data[offset + 2] = 1; // textureIndex - - // storageValues.set(data); - // return objectCount; } } diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index aee130db4cc..a141ff07a27 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -45,6 +45,10 @@ export class TextureAtlas extends Disposable { // TODO: Color, style etc. public getGlyph(lineContent: string, glyphIndex: number): ITextureAtlasGlyph { const chars = lineContent.charAt(glyphIndex); + let glyph: ITextureAtlasGlyph | undefined = this._glyphMap.get(chars); + if (glyph) { + return glyph; + } const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars); this._ctx.drawImage( rasterizedGlyph.source, @@ -59,13 +63,10 @@ export class TextureAtlas extends Disposable { rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top ); - let glyph: ITextureAtlasGlyph | undefined = this._glyphMap.get(chars); - if (glyph) { - return glyph; - } // TODO: Implement allocation glyph = { - id: this._nextId++, + // TODO: Set real id + id: 1, //this._nextId++, x: 0, y: 0, w: rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, From 781654e3a54dbeefa7fd0374d8db2faa858ea83d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 26 Mar 2024 06:17:46 -0700 Subject: [PATCH 013/286] Implement most of a simple texture allocator --- .../editor/browser/view/gpu/gpuViewLayer.ts | 120 +++++++++++------- .../editor/browser/view/gpu/textureAtlas.ts | 58 ++++----- .../browser/view/gpu/textureAtlasAllocator.ts | 76 +++++++++++ 3 files changed, 175 insertions(+), 79 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 1aeea9fb7c8..ed9b1973156 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; -import { TextureAtlas, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; +import { getActiveDocument, getActiveWindow } from 'vs/base/browser/dom'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { TextureAtlas } from 'vs/editor/browser/view/gpu/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -18,6 +19,13 @@ const enum Constants { IndicesPerCell = 6 } +const enum SpriteInfoStorageBufferInfo { + Size = 2 + 2, + Offset_TexturePosition = 0, + Offset_TextureSize = 2, +} +const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; + const enum BindingId { // TODO: Improve names SpriteInfo = 0, @@ -122,9 +130,17 @@ export class GpuViewLayerRenderer { private _squareVertices!: { vertexData: Float32Array; numVertices: number }; private _textureAtlas!: TextureAtlas; + private _spriteInfoStorageBuffer!: GPUBuffer; + private _textureAtlasGpuTexture!: GPUTexture; private _initialized = false; + + + private readonly _testCanvas: HTMLCanvasElement; + private readonly _testCtx: CanvasRenderingContext2D; + + constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { this.domNode = domNode; this.host = host; @@ -132,6 +148,19 @@ export class GpuViewLayerRenderer { this._gpuCtx = this.domNode.getContext('webgpu')!; this.initWebgpu(); + + + + this._testCanvas = document.createElement('canvas'); + this._testCanvas.width = 2048; + this._testCanvas.height = 2048; + this._testCanvas.style.position = 'absolute'; + this._testCanvas.style.top = '0'; + this._testCanvas.style.left = '0'; + this._testCanvas.style.zIndex = '10000'; + this._testCanvas.style.pointerEvents = 'none'; + this._testCtx = ensureNonNullable(this._testCanvas.getContext('2d')); + getActiveDocument().body.appendChild(this._testCanvas); } async initWebgpu() { @@ -237,60 +266,34 @@ export class GpuViewLayerRenderer { } - const maxRenderedObjects = 10; + const maxRenderedObjects = 10000; /////////////////// // Static buffer // /////////////////// - const enum SpriteInfoStorageBufferInfo { - Size = 2 + 2, - Offset_TexturePosition = 0, - Offset_TextureSize = 2, - } - const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; - const spriteInfoStorageBuffer = this._device.createBuffer({ + this._spriteInfoStorageBuffer = this._device.createBuffer({ label: 'Entity static info buffer', size: spriteInfoStorageBufferByteSize * maxRenderedObjects, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); - { - const glyph = textureAtlas.getGlyph('ABC', 0); - const sprites: ITextureAtlasGlyph[] = [ - glyph, - glyph - // { x: 0, y: 0, w: 50, h: 50 }, - ]; - const bufferSize = spriteInfoStorageBufferByteSize * sprites.length; - const values = new Float32Array(bufferSize / 4); - let entryOffset = 0; - for (const t of sprites) { - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = t.x; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = t.y; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = t.w; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = t.h; - entryOffset += SpriteInfoStorageBufferInfo.Size; - } - this._device.queue.writeBuffer(spriteInfoStorageBuffer, 0, values); - } - - - - - // Upload texture bitmap from atlas - const textureAtlasGpuTexture = this._device.createTexture({ + this._textureAtlasGpuTexture = this._device.createTexture({ format: 'rgba8unorm', size: { width: textureAtlas.source.width, height: textureAtlas.source.height }, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, }); - this._device.queue.copyExternalImageToTexture( - { source: textureAtlas.source }, - { texture: textureAtlasGpuTexture }, - { width: textureAtlas.source.width, height: textureAtlas.source.height }, - ); + + + this._updateTextureAtlas(); + + + + + + @@ -318,10 +321,10 @@ export class GpuViewLayerRenderer { label: 'ViewLayer bind group', layout: this._pipeline.getBindGroupLayout(0), entries: [ - { binding: BindingId.SpriteInfo, resource: { buffer: spriteInfoStorageBuffer } }, + { binding: BindingId.SpriteInfo, resource: { buffer: this._spriteInfoStorageBuffer } }, { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._dataBindBuffer } }, { binding: BindingId.TextureSampler, resource: sampler }, - { binding: BindingId.Texture, resource: textureAtlasGpuTexture.createView() }, + { binding: BindingId.Texture, resource: this._textureAtlasGpuTexture.createView() }, { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, ], @@ -371,6 +374,30 @@ export class GpuViewLayerRenderer { this.viewportData = viewportData; } + private _updateTextureAtlas() { + // TODO: Dynamically set buffer size + const bufferSize = spriteInfoStorageBufferByteSize * 10000; + const values = new Float32Array(bufferSize / 4); + let entryOffset = 0; + for (const glyph of this._textureAtlas.glyphs) { + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; + entryOffset += SpriteInfoStorageBufferInfo.Size; + } + this._device.queue.writeBuffer(this._spriteInfoStorageBuffer, 0, values); + + this._device.queue.copyExternalImageToTexture( + { source: this._textureAtlas.source }, + { texture: this._textureAtlasGpuTexture }, + { width: this._textureAtlas.source.width, height: this._textureAtlas.source.height }, + ); + + + this._testCtx.drawImage(this._textureAtlas.source, 0, 0); + } + public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { const ctx: IRendererContext = { rendLineNumberStart: inContext.rendLineNumberStart, @@ -394,6 +421,9 @@ export class GpuViewLayerRenderer { this._dataValuesBufferActiveIndex = (this._dataValuesBufferActiveIndex + 1) % 2; + // TODO: Only do this when needed + this._updateTextureAtlas(); + const encoder = this._device.createCommandEncoder(); (this._renderPassDescriptor.colorAttachments as any)[0].view = this._gpuCtx.getCurrentTexture().createView(); @@ -416,7 +446,7 @@ export class GpuViewLayerRenderer { // TODO: This update could be moved to an arbitrary task thread if expensive? private _updateDataBuffer(dataBuffer: Float32Array, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { - let chars: string = ''; + // let chars: string = ''; let screenAbsoluteX: number = 0; let screenAbsoluteY: number = 0; let zeroToOneX: number = 0; @@ -444,7 +474,7 @@ export class GpuViewLayerRenderer { } // TODO: Handle tab - chars = content[x]; + // chars = content[x]; const glyph = this._textureAtlas.getGlyph(content, x); // TODO: Move math to gpu @@ -462,7 +492,7 @@ export class GpuViewLayerRenderer { dataBuffer[charCount * Constants.IndicesPerCell + 1] = -wgslY; // y dataBuffer[charCount * Constants.IndicesPerCell + 2] = 0; dataBuffer[charCount * Constants.IndicesPerCell + 3] = 0; - dataBuffer[charCount * Constants.IndicesPerCell + 4] = glyph.id; // textureIndex + dataBuffer[charCount * Constants.IndicesPerCell + 4] = glyph.id; // textureId dataBuffer[charCount * Constants.IndicesPerCell + 5] = 0; charCount++; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index a141ff07a27..41df7fb22c9 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -6,16 +6,19 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { ITextureAtlasAllocator, TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; export class TextureAtlas extends Disposable { - private _canvas: OffscreenCanvas; - private _ctx: OffscreenCanvasRenderingContext2D; + private readonly _canvas: OffscreenCanvas; + private readonly _ctx: OffscreenCanvasRenderingContext2D; - private _glyphMap: Map = new Map(); + private readonly _glyphMap: Map = new Map(); + public get glyphs(): IterableIterator { + return this._glyphMap.values(); + } - private _glyphRasterizer: GlyphRasterizer; - - private _nextId = 0; + private readonly _glyphRasterizer: GlyphRasterizer; + private readonly _allocator: ITextureAtlasAllocator; public get source(): OffscreenCanvas { return this._canvas; @@ -26,7 +29,9 @@ export class TextureAtlas extends Disposable { super(); this._canvas = new OffscreenCanvas(maxTextureSize, maxTextureSize); - this._ctx = ensureNonNullable(this._canvas.getContext('2d')); + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); const activeWindow = getActiveWindow(); const style = activeWindow.getComputedStyle(parentDomNode); @@ -34,6 +39,7 @@ export class TextureAtlas extends Disposable { this._ctx.font = `${fontSize}px ${style.fontFamily}`; this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); + this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); // Reduce impact of a memory leak if this object is not released this._register(toDisposable(() => { @@ -50,33 +56,15 @@ export class TextureAtlas extends Disposable { return glyph; } const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars); - this._ctx.drawImage( - rasterizedGlyph.source, - // source - rasterizedGlyph.boundingBox.left, - rasterizedGlyph.boundingBox.top, - rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, - rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top, - // destination - 0, - 0, - rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, - rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top - ); - // TODO: Implement allocation - glyph = { - // TODO: Set real id - id: 1, //this._nextId++, - x: 0, - y: 0, - w: rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left, - h: rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top - }; - console.log('Allocating glyph', { + glyph = this._allocator.allocate(rasterizedGlyph); + this._glyphMap.set(chars, glyph); + + console.log('New glyph', { + chars, rasterizedGlyph, glyph }); - this._glyphMap.set(chars, glyph); + return glyph; } } @@ -90,7 +78,9 @@ class GlyphRasterizer extends Disposable { super(); this._canvas = new OffscreenCanvas(this._fontSize * 3, this._fontSize * 3); - this._ctx = ensureNonNullable(this._canvas.getContext('2d')); + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); this._ctx.font = `${this._fontSize}px ${fontFamily}`; this._ctx.fillStyle = '#FFFFFF'; } @@ -195,14 +185,14 @@ export interface ITextureAtlasGlyph { h: number; } -interface IBoundingBox { +export interface IBoundingBox { left: number; top: number; right: number; bottom: number; } -interface IRasterizedGlyph { +export interface IRasterizedGlyph { source: CanvasImageSource; boundingBox: IBoundingBox; } diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts new file mode 100644 index 00000000000..d9e16a7e07c --- /dev/null +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { IRasterizedGlyph, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; + +export interface ITextureAtlasAllocator { + allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph; +} + +export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { + private _currentRow: ITextureAtlasShelf = { + x: 0, + y: 0, + h: 0 + }; + // TODO: Allow for multiple active rows + // public readonly fixedRows: ICharAtlasActiveRow[] = []; + + private _nextId = 1; + + constructor( + private readonly _canvas: OffscreenCanvas, + private readonly _ctx: OffscreenCanvasRenderingContext2D, + ) { + } + + public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { + // Finalize row if it doesn't fix + if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left > this._canvas.width - this._currentRow.x) { + this._currentRow.x = 0; + this._currentRow.y += this._currentRow.h; + this._currentRow.h = 1; + } + + // TODO: Handle end of atlas page + + // Draw glyph + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top; + this._ctx.drawImage( + rasterizedGlyph.source, + // source + rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.top, + glyphWidth, + glyphHeight, + // destination + this._currentRow.x, + this._currentRow.y, + glyphWidth, + glyphHeight + ); + + // Shift current row + this._currentRow.x += glyphWidth; + this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); + + // Return glyph + const glyph = { + id: this._nextId++, + x: this._currentRow.x, + y: this._currentRow.y, + w: glyphWidth, + h: glyphHeight, + }; + return glyph; + } +} + +interface ITextureAtlasShelf { + x: number; + y: number; + h: number; +} From 8936849eecc7e5972acce4a664b80ec2cb60cf55 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 26 Mar 2024 06:25:16 -0700 Subject: [PATCH 014/286] Correct glyph index --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 6 +++--- src/vs/editor/browser/view/gpu/textureAtlas.ts | 2 +- .../browser/view/gpu/textureAtlasAllocator.ts | 17 +++++++++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index ed9b1973156..f0273ad30a4 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -57,7 +57,7 @@ struct Vertex { struct DynamicUnitInfo { position: vec2f, unused1: vec2f, - textureId: f32, + textureIndex: f32, unused2: f32 }; @@ -78,7 +78,7 @@ struct VSOutput { @builtin(vertex_index) vertexIndex : u32 ) -> VSOutput { let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; - let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureId)]; + let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureIndex)]; var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 @@ -492,7 +492,7 @@ export class GpuViewLayerRenderer { dataBuffer[charCount * Constants.IndicesPerCell + 1] = -wgslY; // y dataBuffer[charCount * Constants.IndicesPerCell + 2] = 0; dataBuffer[charCount * Constants.IndicesPerCell + 3] = 0; - dataBuffer[charCount * Constants.IndicesPerCell + 4] = glyph.id; // textureId + dataBuffer[charCount * Constants.IndicesPerCell + 4] = glyph.index; // textureIndex dataBuffer[charCount * Constants.IndicesPerCell + 5] = 0; charCount++; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 41df7fb22c9..576b5ba1079 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -178,7 +178,7 @@ class GlyphRasterizer extends Disposable { } export interface ITextureAtlasGlyph { - id: number; + index: number; x: number; y: number; w: number; diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index d9e16a7e07c..64eb13729a8 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -18,7 +18,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { // TODO: Allow for multiple active rows // public readonly fixedRows: ICharAtlasActiveRow[] = []; - private _nextId = 1; + private _nextIndex = 0; constructor( private readonly _canvas: OffscreenCanvas, @@ -53,18 +53,19 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { glyphHeight ); - // Shift current row - this._currentRow.x += glyphWidth; - this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); - - // Return glyph - const glyph = { - id: this._nextId++, + // Create glyph object + const glyph: ITextureAtlasGlyph = { + index: this._nextIndex++, x: this._currentRow.x, y: this._currentRow.y, w: glyphWidth, h: glyphHeight, }; + + // Shift current row + this._currentRow.x += glyphWidth; + this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); + return glyph; } } From 1e2295c06b309c837290d3d26fffd4b1e4396255 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 26 Mar 2024 06:35:38 -0700 Subject: [PATCH 015/286] Fix origin offset, kind of --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 8 ++++++-- src/vs/editor/browser/view/gpu/textureAtlas.ts | 10 +++++++++- .../editor/browser/view/gpu/textureAtlasAllocator.ts | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index f0273ad30a4..9ecf463594f 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -20,9 +20,10 @@ const enum Constants { } const enum SpriteInfoStorageBufferInfo { - Size = 2 + 2, + Size = 2 + 2 + 2, Offset_TexturePosition = 0, Offset_TextureSize = 2, + Offset_OriginPosition = 4, } const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; @@ -48,6 +49,7 @@ struct TextureInfoUniform { struct SpriteInfo { position: vec2f, size: vec2f, + origin: vec2f, }; struct Vertex { @@ -83,7 +85,7 @@ struct VSOutput { var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position, + (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position - ((spriteInfo.origin * 2) / uniforms.canvasDimensions), 0.0, 1.0 ); @@ -384,6 +386,8 @@ export class GpuViewLayerRenderer { values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; entryOffset += SpriteInfoStorageBufferInfo.Size; } this._device.queue.writeBuffer(this._spriteInfoStorageBuffer, 0, values); diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 576b5ba1079..0c55dc072ff 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -95,9 +95,14 @@ class GlyphRasterizer extends Disposable { const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); // TODO: Hot path: Reuse object + const boundingBox = this._findGlyphBoundingBox(imageData); const result: IRasterizedGlyph = { source: this._canvas, - boundingBox: this._findGlyphBoundingBox(imageData) + boundingBox, + originOffset: { + x: boundingBox.left - this._fontSize, + y: boundingBox.top - this._fontSize + } }; return result; } @@ -183,6 +188,8 @@ export interface ITextureAtlasGlyph { y: number; w: number; h: number; + originOffsetX: number; + originOffsetY: number; } export interface IBoundingBox { @@ -195,4 +202,5 @@ export interface IBoundingBox { export interface IRasterizedGlyph { source: CanvasImageSource; boundingBox: IBoundingBox; + originOffset: { x: number; y: number }; } diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index 64eb13729a8..788caac2f53 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -60,6 +60,8 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { y: this._currentRow.y, w: glyphWidth, h: glyphHeight, + originOffsetX: rasterizedGlyph.originOffset.x, + originOffsetY: rasterizedGlyph.originOffset.y }; // Shift current row From f5b0aa26497bbd7d0ee90bc8f70d14e352613a2f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 26 Mar 2024 06:55:55 -0700 Subject: [PATCH 016/286] Don't upload texture atlas unless changed --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 10 +++++++++- src/vs/editor/browser/view/gpu/textureAtlas.ts | 6 +++++- .../editor/browser/view/gpu/textureAtlasAllocator.ts | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 9ecf463594f..ce03e0f050e 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -377,6 +377,10 @@ export class GpuViewLayerRenderer { } private _updateTextureAtlas() { + if (!this._textureAtlas.hasChanges) { + return; + } + this._textureAtlas.hasChanges = false; // TODO: Dynamically set buffer size const bufferSize = spriteInfoStorageBufferByteSize * 10000; const values = new Float32Array(bufferSize / 4); @@ -465,7 +469,7 @@ export class GpuViewLayerRenderer { scrollTop = 0; } for (let lineNumber = startLineNumber; lineNumber <= stopLineNumber; lineNumber++) { - const y = Math.round((-scrollTop + deltaTop[lineNumber - startLineNumber])); + const y = Math.round(-scrollTop + deltaTop[lineNumber - startLineNumber]); // Offscreen if (y < 0) { continue; @@ -492,6 +496,10 @@ export class GpuViewLayerRenderer { wgslX = zeroToOneX * 2 - 1; wgslY = zeroToOneY * 2 - 1; + // TODO: We could upload the entire file as a grid, capping out lines at some reasonable amount (200?) + // Optimize for the common case and fallback to a slower path for long line files + // Doing the fast grid path would mean only the cell needs to change on data change, scrolling would simply change the start line index + dataBuffer[charCount * Constants.IndicesPerCell + 0] = wgslX; // x dataBuffer[charCount * Constants.IndicesPerCell + 1] = -wgslY; // y dataBuffer[charCount * Constants.IndicesPerCell + 2] = 0; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 0c55dc072ff..8e00750242c 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -24,6 +24,8 @@ export class TextureAtlas extends Disposable { return this._canvas; } + public hasChanges = false; + // TODO: Should pull in the font size from config instead of random dom node constructor(parentDomNode: HTMLElement, maxTextureSize: number) { super(); @@ -58,6 +60,7 @@ export class TextureAtlas extends Disposable { const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars); glyph = this._allocator.allocate(rasterizedGlyph); this._glyphMap.set(chars, glyph); + this.hasChanges = true; console.log('New glyph', { chars, @@ -82,6 +85,7 @@ class GlyphRasterizer extends Disposable { willReadFrequently: true })); this._ctx.font = `${this._fontSize}px ${fontFamily}`; + this._ctx.textBaseline = 'alphabetic'; this._ctx.fillStyle = '#FFFFFF'; } @@ -91,7 +95,7 @@ class GlyphRasterizer extends Disposable { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); // TODO: Draw in middle using alphabetical baseline - this._ctx.fillText(chars, this._fontSize, this._fontSize); + this._ctx.fillText(chars, this._fontSize * 2, this._fontSize); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); // TODO: Hot path: Reuse object diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index 788caac2f53..a5904eeb8d9 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -39,6 +39,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { // Draw glyph const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top; + // TODO: Prefer putImageData as it doesn't do blending or scaling this._ctx.drawImage( rasterizedGlyph.source, // source From 1ae361ba55207a58d1b4a6fbc30dba8775a4af43 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 26 Mar 2024 07:40:52 -0700 Subject: [PATCH 017/286] Notes --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index ce03e0f050e..d0fdb540207 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -499,6 +499,7 @@ export class GpuViewLayerRenderer { // TODO: We could upload the entire file as a grid, capping out lines at some reasonable amount (200?) // Optimize for the common case and fallback to a slower path for long line files // Doing the fast grid path would mean only the cell needs to change on data change, scrolling would simply change the start line index + // Even better would be a grid for standard sized lines (~120?) and then another buffer that handles larger lines in a slower but more dynamic way dataBuffer[charCount * Constants.IndicesPerCell + 0] = wgslX; // x dataBuffer[charCount * Constants.IndicesPerCell + 1] = -wgslY; // y From 80918f852606e33e9e557542aa0cad39bc99df92 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 06:19:17 -0700 Subject: [PATCH 018/286] Static texture atlas, debounce drawing test atlas --- .../editor/browser/view/gpu/gpuViewLayer.ts | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index d0fdb540207..66c05e2443c 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveDocument, getActiveWindow } from 'vs/base/browser/dom'; +import { debounce } from 'vs/base/common/decorators'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; @@ -131,7 +132,7 @@ export class GpuViewLayerRenderer { private _vertexBuffer!: GPUBuffer; private _squareVertices!: { vertexData: Float32Array; numVertices: number }; - private _textureAtlas!: TextureAtlas; + private static _textureAtlas: TextureAtlas; private _spriteInfoStorageBuffer!: GPUBuffer; private _textureAtlasGpuTexture!: GPUTexture; @@ -139,8 +140,8 @@ export class GpuViewLayerRenderer { - private readonly _testCanvas: HTMLCanvasElement; - private readonly _testCtx: CanvasRenderingContext2D; + private static _testCanvas: HTMLCanvasElement; + private static _testCtx: CanvasRenderingContext2D; constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { @@ -150,19 +151,6 @@ export class GpuViewLayerRenderer { this._gpuCtx = this.domNode.getContext('webgpu')!; this.initWebgpu(); - - - - this._testCanvas = document.createElement('canvas'); - this._testCanvas.width = 2048; - this._testCanvas.height = 2048; - this._testCanvas.style.position = 'absolute'; - this._testCanvas.style.top = '0'; - this._testCanvas.style.left = '0'; - this._testCanvas.style.zIndex = '10000'; - this._testCanvas.style.pointerEvents = 'none'; - this._testCtx = ensureNonNullable(this._testCanvas.getContext('2d')); - getActiveDocument().body.appendChild(this._testCanvas); } async initWebgpu() { @@ -246,7 +234,21 @@ export class GpuViewLayerRenderer { // Create texture atlas - const textureAtlas = this._textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); + if (!GpuViewLayerRenderer._textureAtlas) { + GpuViewLayerRenderer._textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); + + GpuViewLayerRenderer._testCanvas = document.createElement('canvas'); + GpuViewLayerRenderer._testCanvas.width = 2048; + GpuViewLayerRenderer._testCanvas.height = 2048; + GpuViewLayerRenderer._testCanvas.style.position = 'absolute'; + GpuViewLayerRenderer._testCanvas.style.top = '0'; + GpuViewLayerRenderer._testCanvas.style.left = '0'; + GpuViewLayerRenderer._testCanvas.style.zIndex = '10000'; + GpuViewLayerRenderer._testCanvas.style.pointerEvents = 'none'; + GpuViewLayerRenderer._testCtx = ensureNonNullable(GpuViewLayerRenderer._testCanvas.getContext('2d')); + getActiveDocument().body.appendChild(GpuViewLayerRenderer._testCanvas); + } + const textureAtlas = GpuViewLayerRenderer._textureAtlas; @@ -377,15 +379,15 @@ export class GpuViewLayerRenderer { } private _updateTextureAtlas() { - if (!this._textureAtlas.hasChanges) { + if (!GpuViewLayerRenderer._textureAtlas.hasChanges) { return; } - this._textureAtlas.hasChanges = false; + GpuViewLayerRenderer._textureAtlas.hasChanges = false; // TODO: Dynamically set buffer size const bufferSize = spriteInfoStorageBufferByteSize * 10000; const values = new Float32Array(bufferSize / 4); let entryOffset = 0; - for (const glyph of this._textureAtlas.glyphs) { + for (const glyph of GpuViewLayerRenderer._textureAtlas.glyphs) { values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; @@ -397,13 +399,19 @@ export class GpuViewLayerRenderer { this._device.queue.writeBuffer(this._spriteInfoStorageBuffer, 0, values); this._device.queue.copyExternalImageToTexture( - { source: this._textureAtlas.source }, + { source: GpuViewLayerRenderer._textureAtlas.source }, { texture: this._textureAtlasGpuTexture }, - { width: this._textureAtlas.source.width, height: this._textureAtlas.source.height }, + { width: GpuViewLayerRenderer._textureAtlas.source.width, height: GpuViewLayerRenderer._textureAtlas.source.height }, ); - this._testCtx.drawImage(this._textureAtlas.source, 0, 0); + GpuViewLayerRenderer._drawToTextureAtlas(); + } + + @debounce(500) + private static _drawToTextureAtlas() { + GpuViewLayerRenderer._testCtx.clearRect(0, 0, GpuViewLayerRenderer._textureAtlas.source.width, GpuViewLayerRenderer._textureAtlas.source.height); + GpuViewLayerRenderer._testCtx.drawImage(GpuViewLayerRenderer._textureAtlas.source, 0, 0); } public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { @@ -483,7 +491,7 @@ export class GpuViewLayerRenderer { // TODO: Handle tab // chars = content[x]; - const glyph = this._textureAtlas.getGlyph(content, x); + const glyph = GpuViewLayerRenderer._textureAtlas.getGlyph(content, x); // TODO: Move math to gpu // TODO: Render using a line offset for partial line scrolling From 20d5126401cdc0bcbcaa93f0adcd112839030400 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 07:07:12 -0700 Subject: [PATCH 019/286] Create render strategy interface --- .../editor/browser/view/gpu/gpuViewLayer.ts | 298 ++++++++++-------- 1 file changed, 169 insertions(+), 129 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 66c05e2443c..1669d20f29b 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -38,79 +38,6 @@ const enum BindingId { TextureInfoUniform = 5, } -const wgsl = ` -struct Uniforms { - canvasDimensions: vec2f, -}; - -struct TextureInfoUniform { - spriteSheetSize: vec2f, -} - -struct SpriteInfo { - position: vec2f, - size: vec2f, - origin: vec2f, -}; - -struct Vertex { - @location(0) position: vec2f, -}; - -struct DynamicUnitInfo { - position: vec2f, - unused1: vec2f, - textureIndex: f32, - unused2: f32 -}; - -struct VSOutput { - @builtin(position) position: vec4f, - @location(0) texcoord: vec2f, -}; - -@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; -@group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; - -@group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; -@group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; - -@vertex fn vs( - vert: Vertex, - @builtin(instance_index) instanceIndex: u32, - @builtin(vertex_index) vertexIndex : u32 -) -> VSOutput { - let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; - let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureIndex)]; - - var vsOut: VSOutput; - // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 - vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position - ((spriteInfo.origin * 2) / uniforms.canvasDimensions), - 0.0, - 1.0 - ); - - // Textures are flipped from natural direction on the y-axis, so flip it back - vsOut.texcoord = vert.position; - vsOut.texcoord = ( - // Sprite offset (0-1) - (spriteInfo.position / textureInfoUniform.spriteSheetSize) + - // Sprite coordinate (0-1) - (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize)) - ); - - return vsOut; -} - -@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; -@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; - -@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { - return textureSample(ourTexture, ourSampler, vsOut.texcoord); -} -`; - export class GpuViewLayerRenderer { readonly domNode: HTMLCanvasElement; @@ -125,9 +52,6 @@ export class GpuViewLayerRenderer { private _bindGroup!: GPUBindGroup; private _pipeline!: GPURenderPipeline; - private _dataBindBuffer!: GPUBuffer; - private _dataValueBuffers!: ArrayBuffer[]; - private _dataValuesBufferActiveIndex: number = 0; private _vertexBuffer!: GPUBuffer; private _squareVertices!: { vertexData: Float32Array; numVertices: number }; @@ -143,6 +67,8 @@ export class GpuViewLayerRenderer { private static _testCanvas: HTMLCanvasElement; private static _testCtx: CanvasRenderingContext2D; + private _renderStrategy!: IRenderStrategy; + constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { this.domNode = domNode; @@ -171,9 +97,30 @@ export class GpuViewLayerRenderer { format: presentationFormat, }); + + // Create texture atlas + if (!GpuViewLayerRenderer._textureAtlas) { + GpuViewLayerRenderer._textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); + + GpuViewLayerRenderer._testCanvas = document.createElement('canvas'); + GpuViewLayerRenderer._testCanvas.width = 2048; + GpuViewLayerRenderer._testCanvas.height = 2048; + GpuViewLayerRenderer._testCanvas.style.position = 'absolute'; + GpuViewLayerRenderer._testCanvas.style.top = '0'; + GpuViewLayerRenderer._testCanvas.style.left = '0'; + GpuViewLayerRenderer._testCanvas.style.zIndex = '10000'; + GpuViewLayerRenderer._testCanvas.style.pointerEvents = 'none'; + GpuViewLayerRenderer._testCtx = ensureNonNullable(GpuViewLayerRenderer._testCanvas.getContext('2d')); + getActiveDocument().body.appendChild(GpuViewLayerRenderer._testCanvas); + } + const textureAtlas = GpuViewLayerRenderer._textureAtlas; + + + this._renderStrategy = new NaiveViewportRenderStrategy(this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); + const module = this._device.createShaderModule({ label: 'ViewLayer shader module', - code: wgsl, + code: this._renderStrategy.wgsl, }); this._pipeline = this._device.createRenderPipeline({ @@ -233,24 +180,6 @@ export class GpuViewLayerRenderer { } - // Create texture atlas - if (!GpuViewLayerRenderer._textureAtlas) { - GpuViewLayerRenderer._textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); - - GpuViewLayerRenderer._testCanvas = document.createElement('canvas'); - GpuViewLayerRenderer._testCanvas.width = 2048; - GpuViewLayerRenderer._testCanvas.height = 2048; - GpuViewLayerRenderer._testCanvas.style.position = 'absolute'; - GpuViewLayerRenderer._testCanvas.style.top = '0'; - GpuViewLayerRenderer._testCanvas.style.left = '0'; - GpuViewLayerRenderer._testCanvas.style.zIndex = '10000'; - GpuViewLayerRenderer._testCanvas.style.pointerEvents = 'none'; - GpuViewLayerRenderer._testCtx = ensureNonNullable(GpuViewLayerRenderer._testCanvas.getContext('2d')); - getActiveDocument().body.appendChild(GpuViewLayerRenderer._testCanvas); - } - const textureAtlas = GpuViewLayerRenderer._textureAtlas; - - const enum TextureInfoUniformBufferInfo { Size = 2, @@ -295,24 +224,8 @@ export class GpuViewLayerRenderer { + this._renderStrategy.initBuffers(); - - - - - - // TODO: Grow/shrink buffer size dynamically - const cellCount = 10000; - const bufferSize = cellCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; - this._dataBindBuffer = this._device.createBuffer({ - label: 'Entity dynamic info buffer', - size: bufferSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); - this._dataValueBuffers = [ - new ArrayBuffer(bufferSize), - new ArrayBuffer(bufferSize), - ]; this._updateSquareVertices(); @@ -326,11 +239,11 @@ export class GpuViewLayerRenderer { layout: this._pipeline.getBindGroupLayout(0), entries: [ { binding: BindingId.SpriteInfo, resource: { buffer: this._spriteInfoStorageBuffer } }, - { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._dataBindBuffer } }, { binding: BindingId.TextureSampler, resource: sampler }, { binding: BindingId.Texture, resource: this._textureAtlasGpuTexture.createView() }, { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, + ...this._renderStrategy.bindGroupEntries ], }); @@ -424,18 +337,11 @@ export class GpuViewLayerRenderer { if (!this._initialized) { return ctx; } - return this._renderWebgpu(ctx, startLineNumber, stopLineNumber, deltaTop); + return this._render(ctx, startLineNumber, stopLineNumber, deltaTop); } - private _renderWebgpu(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { - // TODO: Improve "data" name - const dataBuffer = new Float32Array(this._dataValueBuffers[this._dataValuesBufferActiveIndex]); - const visibleObjectCount = this._updateDataBuffer(dataBuffer, ctx, startLineNumber, stopLineNumber, deltaTop); - - // Write buffer and swap it out to unblock writes - this._device.queue.writeBuffer(this._dataBindBuffer, 0, dataBuffer, 0, visibleObjectCount * Constants.IndicesPerCell); - - this._dataValuesBufferActiveIndex = (this._dataValuesBufferActiveIndex + 1) % 2; + private _render(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { + const visibleObjectCount = this._renderStrategy.update(ctx, startLineNumber, stopLineNumber, deltaTop); // TODO: Only do this when needed this._updateTextureAtlas(); @@ -449,6 +355,7 @@ export class GpuViewLayerRenderer { pass.setBindGroup(0, this._bindGroup); // TODO: Draws could be split by chunk, this would help minimize moving data around in arrays + pass.draw(this._squareVertices.numVertices, visibleObjectCount); pass.end(); @@ -459,8 +366,139 @@ export class GpuViewLayerRenderer { return ctx; } +} + + +interface IRenderStrategy { + readonly wgsl: string; + readonly bindGroupEntries: GPUBindGroupEntry[]; + + initBuffers(): void; + update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number; +} + +// #region Naive viewport render strategy + +const naiveViewportRenderStrategyWgsl = ` +struct Uniforms { + canvasDimensions: vec2f, +}; + +struct TextureInfoUniform { + spriteSheetSize: vec2f, +} + +struct SpriteInfo { + position: vec2f, + size: vec2f, + origin: vec2f, +}; + +struct Vertex { + @location(0) position: vec2f, +}; + +struct DynamicUnitInfo { + position: vec2f, + unused1: vec2f, + textureIndex: f32, + unused2: f32 +}; + +struct VSOutput { + @builtin(position) position: vec4f, + @location(0) texcoord: vec2f, +}; + +@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; +@group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; + +@group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; +@group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; + +@vertex fn vs( + vert: Vertex, + @builtin(instance_index) instanceIndex: u32, + @builtin(vertex_index) vertexIndex : u32 +) -> VSOutput { + let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; + let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureIndex)]; + + var vsOut: VSOutput; + // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 + vsOut.position = vec4f( + (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position - ((spriteInfo.origin * 2) / uniforms.canvasDimensions), + 0.0, + 1.0 + ); + + // Textures are flipped from natural direction on the y-axis, so flip it back + vsOut.texcoord = vert.position; + vsOut.texcoord = ( + // Sprite offset (0-1) + (spriteInfo.position / textureInfoUniform.spriteSheetSize) + + // Sprite coordinate (0-1) + (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize)) + ); + + return vsOut; +} + +@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; + +@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { + return textureSample(ourTexture, ourSampler, vsOut.texcoord); +} +`; + +class NaiveViewportRenderStrategy implements IRenderStrategy { + readonly wgsl: string = naiveViewportRenderStrategyWgsl; + + private _cellBindBuffer!: GPUBuffer; + private _cellValueBuffers!: ArrayBuffer[]; + private _cellValuesBufferActiveIndex: number = 0; + + get bindGroupEntries(): GPUBindGroupEntry[] { + return [ + { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._cellBindBuffer } } + ]; + } + + constructor( + private readonly _device: GPUDevice, + private readonly _canvas: HTMLCanvasElement, + private readonly _viewportData: ViewportData, + private readonly _textureAtlas: TextureAtlas + ) { + } + + initBuffers(): void { + // TODO: Grow/shrink buffer size dynamically + const cellCount = 10000; + const bufferSize = cellCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._cellBindBuffer = this._device.createBuffer({ + label: 'Entity dynamic info buffer', + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + this._cellValueBuffers = [ + new ArrayBuffer(bufferSize), + new ArrayBuffer(bufferSize), + ]; + } + + update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { + const cellBuffer = new Float32Array(this._cellValueBuffers[this._cellValuesBufferActiveIndex]); + const visibleObjectCount = this._updateDataBuffer(cellBuffer, ctx, startLineNumber, stopLineNumber, deltaTop); + + // Write buffer and swap it out to unblock writes + this._device.queue.writeBuffer(this._cellBindBuffer, 0, cellBuffer, 0, visibleObjectCount * Constants.IndicesPerCell); + + this._cellValuesBufferActiveIndex = (this._cellValuesBufferActiveIndex + 1) % 2; + return visibleObjectCount; + } - // TODO: This update could be moved to an arbitrary task thread if expensive? private _updateDataBuffer(dataBuffer: Float32Array, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { // let chars: string = ''; let screenAbsoluteX: number = 0; @@ -472,7 +510,7 @@ export class GpuViewLayerRenderer { const activeWindow = getActiveWindow(); let charCount = 0; - let scrollTop = parseInt(this.domNode.parentElement!.getAttribute('data-adjusted-scroll-top')!); + let scrollTop = parseInt(this._canvas.parentElement!.getAttribute('data-adjusted-scroll-top')!); if (Number.isNaN(scrollTop)) { scrollTop = 0; } @@ -482,7 +520,7 @@ export class GpuViewLayerRenderer { if (y < 0) { continue; } - const content = this.viewportData.getViewLineRenderingData(lineNumber).content; + const content = this._viewportData.getViewLineRenderingData(lineNumber).content; // console.log(content, 0, y); for (let x = 0; x < content.length; x++) { if (content.charAt(x) === ' ') { @@ -491,7 +529,7 @@ export class GpuViewLayerRenderer { // TODO: Handle tab // chars = content[x]; - const glyph = GpuViewLayerRenderer._textureAtlas.getGlyph(content, x); + const glyph = this._textureAtlas.getGlyph(content, x); // TODO: Move math to gpu // TODO: Render using a line offset for partial line scrolling @@ -499,8 +537,8 @@ export class GpuViewLayerRenderer { screenAbsoluteX = x * 7 * activeWindow.devicePixelRatio; // TODO: This +10 is because the glyph is being rendered in the wrong position screenAbsoluteY = Math.round(y * activeWindow.devicePixelRatio); - zeroToOneX = screenAbsoluteX / this.domNode.width; - zeroToOneY = screenAbsoluteY / this.domNode.height; + zeroToOneX = screenAbsoluteX / this._canvas.width; + zeroToOneY = screenAbsoluteY / this._canvas.height; wgslX = zeroToOneX * 2 - 1; wgslY = zeroToOneY * 2 - 1; @@ -523,3 +561,5 @@ export class GpuViewLayerRenderer { return charCount; } } + +// #endregion Naive viewport render strategy From 2dd6a057414547e1ea74f6fb3544e5c7180cdcc8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 07:48:56 -0700 Subject: [PATCH 020/286] Get rendering of full file data mostly working --- .../editor/browser/view/gpu/gpuViewLayer.ts | 216 +++++++++++++++++- 1 file changed, 214 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 1669d20f29b..960a3916ee0 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -116,7 +116,8 @@ export class GpuViewLayerRenderer { const textureAtlas = GpuViewLayerRenderer._textureAtlas; - this._renderStrategy = new NaiveViewportRenderStrategy(this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); + // this._renderStrategy = new NaiveViewportRenderStrategy(this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); + this._renderStrategy = new FullFileRenderStrategy(this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); const module = this._device.createShaderModule({ label: 'ViewLayer shader module', @@ -356,7 +357,11 @@ export class GpuViewLayerRenderer { pass.setBindGroup(0, this._bindGroup); // TODO: Draws could be split by chunk, this would help minimize moving data around in arrays - pass.draw(this._squareVertices.numVertices, visibleObjectCount); + if (this._renderStrategy?.draw) { + this._renderStrategy.draw(pass, ctx, startLineNumber, stopLineNumber, deltaTop); + } else { + pass.draw(this._squareVertices.numVertices, visibleObjectCount); + } pass.end(); @@ -375,6 +380,7 @@ interface IRenderStrategy { initBuffers(): void; update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number; + draw?(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void; } // #region Naive viewport render strategy @@ -563,3 +569,209 @@ class NaiveViewportRenderStrategy implements IRenderStra } // #endregion Naive viewport render strategy + +// #region Full file render strategy + +const fullFileRenderStrategyWgsl = ` +struct Uniforms { + canvasDimensions: vec2f, +}; + +struct TextureInfoUniform { + spriteSheetSize: vec2f, +} + +struct SpriteInfo { + position: vec2f, + size: vec2f, + origin: vec2f, +}; + +struct Vertex { + @location(0) position: vec2f, +}; + +struct DynamicUnitInfo { + position: vec2f, + unused1: vec2f, + textureIndex: f32, + unused2: f32 +}; + +struct VSOutput { + @builtin(position) position: vec4f, + @location(0) texcoord: vec2f, +}; + +@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; +@group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; + +@group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; +@group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; + +@vertex fn vs( + vert: Vertex, + @builtin(instance_index) instanceIndex: u32, + @builtin(vertex_index) vertexIndex : u32 +) -> VSOutput { + let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; + let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureIndex)]; + + var vsOut: VSOutput; + // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 + vsOut.position = vec4f( + (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position - ((spriteInfo.origin * 2) / uniforms.canvasDimensions), + 0.0, + 1.0 + ); + + // Textures are flipped from natural direction on the y-axis, so flip it back + vsOut.texcoord = vert.position; + vsOut.texcoord = ( + // Sprite offset (0-1) + (spriteInfo.position / textureInfoUniform.spriteSheetSize) + + // Sprite coordinate (0-1) + (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize)) + ); + + return vsOut; +} + +@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; + +@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { + return textureSample(ourTexture, ourSampler, vsOut.texcoord); +} +`; + +class FullFileRenderStrategy implements IRenderStrategy { + + private static _lineCount = 3000; + private static _columnCount = 200; + + readonly wgsl: string = fullFileRenderStrategyWgsl; + + private _cellBindBuffer!: GPUBuffer; + private _cellValueBuffers!: ArrayBuffer[]; + private _cellValuesBufferActiveIndex: number = 0; + + get bindGroupEntries(): GPUBindGroupEntry[] { + return [ + { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._cellBindBuffer } } + ]; + } + + constructor( + private readonly _device: GPUDevice, + private readonly _canvas: HTMLCanvasElement, + private readonly _viewportData: ViewportData, + private readonly _textureAtlas: TextureAtlas + ) { + } + + initBuffers(): void { + const bufferSize = FullFileRenderStrategy._lineCount * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._cellBindBuffer = this._device.createBuffer({ + label: 'Full file cell buffer', + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + this._cellValueBuffers = [ + new ArrayBuffer(bufferSize), + new ArrayBuffer(bufferSize), + ]; + } + + update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { + let y = 0; + let x = 0; + let screenAbsoluteX: number = 0; + let screenAbsoluteY: number = 0; + let zeroToOneX: number = 0; + let zeroToOneY: number = 0; + let wgslX: number = 0; + let wgslY: number = 0; + + const viewportData = this._viewportData; + const activeWindow = getActiveWindow(); + const cellBuffer = new Float32Array(this._cellValueBuffers[this._cellValuesBufferActiveIndex]); + const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; + + let scrollTop = parseInt(this._canvas.parentElement!.getAttribute('data-adjusted-scroll-top')!); + if (Number.isNaN(scrollTop)) { + scrollTop = 0; + } + + for (y = startLineNumber; y <= stopLineNumber; y++) { + const viewLineRenderingData = viewportData.getViewLineRenderingData(y); + const content = viewLineRenderingData.content; + for (x = 0; x < FullFileRenderStrategy._columnCount; x++) { + const glyph = this._textureAtlas.getGlyph(content, x); + + screenAbsoluteX = x * 7 * activeWindow.devicePixelRatio; + // TODO: Send scroll offset instead of setting it here such that the cell data doesn't need to change when scrolling + screenAbsoluteY = Math.round(Math.round(-scrollTop + deltaTop[y - startLineNumber]) * activeWindow.devicePixelRatio); + zeroToOneX = screenAbsoluteX / this._canvas.width; + zeroToOneY = screenAbsoluteY / this._canvas.height; + wgslX = zeroToOneX * 2 - 1; + wgslY = zeroToOneY * 2 - 1; + + const cellIndex = y * lineIndexCount + x * Constants.IndicesPerCell; + cellBuffer[cellIndex + 0] = wgslX; // x + cellBuffer[cellIndex + 1] = -wgslY; // y + cellBuffer[cellIndex + 2] = 0; + cellBuffer[cellIndex + 3] = 0; + cellBuffer[cellIndex + 4] = glyph.index; // textureIndex + cellBuffer[cellIndex + 5] = 0; + } + } + + const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; + + // Write buffer and swap it out to unblock writes + // this._device.queue.writeBuffer( + // this._cellBindBuffer, + // (startLineNumber - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + // // TODO: this cell buffer actually only needs to be the size of the viewport if we are only uploading a range + // // at the maximum each frame + // cellBuffer, + // (startLineNumber - 1) * lineIndexCount, + // (stopLineNumber - startLineNumber) * lineIndexCount + // ); + this._device.queue.writeBuffer( + this._cellBindBuffer, + 0, + cellBuffer + ); + + this._cellValuesBufferActiveIndex = (this._cellValuesBufferActiveIndex + 1) % 2; + + return visibleObjectCount; + } + + draw(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void { + const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; + + // console.log('draw', + // { + // ctx, + // startLineNumber, + // stopLineNumber, + // deltaTop + // }, + // 6, // square verticies + // visibleObjectCount, + // undefined, + // (startLineNumber - 1) * FullFileRenderStrategy._columnCount + // ); + pass.draw( + 6, // square verticies + visibleObjectCount, + undefined, + (startLineNumber - 1) * FullFileRenderStrategy._columnCount + ); + } +} + +// #endregion Full file render strategy From 4dd73e25c0f359fc1f90f196f13a0f7103a2a69b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 08:14:36 -0700 Subject: [PATCH 021/286] Progress --- .../editor/browser/view/gpu/gpuViewLayer.ts | 94 ++++++++++++------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 960a3916ee0..6987c31a869 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -36,6 +36,7 @@ const enum BindingId { Texture = 3, Uniforms = 4, TextureInfoUniform = 5, + ScrollOffset = 6, } export class GpuViewLayerRenderer { @@ -598,6 +599,10 @@ struct DynamicUnitInfo { unused2: f32 }; +struct ScrollOffset { + offset: vec2f +} + struct VSOutput { @builtin(position) position: vec4f, @location(0) texcoord: vec2f, @@ -608,6 +613,7 @@ struct VSOutput { @group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; @group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; +@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; @vertex fn vs( vert: Vertex, @@ -620,7 +626,7 @@ struct VSOutput { var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position - ((spriteInfo.origin * 2) / uniforms.canvasDimensions), + (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position - ((spriteInfo.origin * 2) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions), 0.0, 1.0 ); @@ -654,11 +660,15 @@ class FullFileRenderStrategy implements IRenderStrategy< private _cellBindBuffer!: GPUBuffer; private _cellValueBuffers!: ArrayBuffer[]; - private _cellValuesBufferActiveIndex: number = 0; + private _activeDoubleBufferIndex: number = 0; + + private _scrollOffsetBindBuffer!: GPUBuffer; + private _scrollOffsetValueBuffers!: Float32Array[]; get bindGroupEntries(): GPUBindGroupEntry[] { return [ - { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._cellBindBuffer } } + { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._cellBindBuffer } }, + { binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } } ]; } @@ -681,6 +691,17 @@ class FullFileRenderStrategy implements IRenderStrategy< new ArrayBuffer(bufferSize), new ArrayBuffer(bufferSize), ]; + + const scrollOffsetBufferSize = 2; + this._scrollOffsetBindBuffer = this._device.createBuffer({ + label: 'Scroll offset buffer', + size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + this._scrollOffsetValueBuffers = [ + new Float32Array(scrollOffsetBufferSize), + new Float32Array(scrollOffsetBufferSize), + ]; } update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { @@ -693,15 +714,20 @@ class FullFileRenderStrategy implements IRenderStrategy< let wgslX: number = 0; let wgslY: number = 0; - const viewportData = this._viewportData; - const activeWindow = getActiveWindow(); - const cellBuffer = new Float32Array(this._cellValueBuffers[this._cellValuesBufferActiveIndex]); - const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; - + // Update scroll offset let scrollTop = parseInt(this._canvas.parentElement!.getAttribute('data-adjusted-scroll-top')!); if (Number.isNaN(scrollTop)) { scrollTop = 0; } + const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex]; + scrollOffsetBuffer[1] = scrollTop; + this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, scrollOffsetBuffer); + + // Update cell data + const viewportData = this._viewportData; + const activeWindow = getActiveWindow(); + const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); + const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; for (y = startLineNumber; y <= stopLineNumber; y++) { const viewLineRenderingData = viewportData.getViewLineRenderingData(y); @@ -711,13 +737,13 @@ class FullFileRenderStrategy implements IRenderStrategy< screenAbsoluteX = x * 7 * activeWindow.devicePixelRatio; // TODO: Send scroll offset instead of setting it here such that the cell data doesn't need to change when scrolling - screenAbsoluteY = Math.round(Math.round(-scrollTop + deltaTop[y - startLineNumber]) * activeWindow.devicePixelRatio); + screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); zeroToOneX = screenAbsoluteX / this._canvas.width; zeroToOneY = screenAbsoluteY / this._canvas.height; wgslX = zeroToOneX * 2 - 1; wgslY = zeroToOneY * 2 - 1; - const cellIndex = y * lineIndexCount + x * Constants.IndicesPerCell; + const cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x - 1)) * Constants.IndicesPerCell; cellBuffer[cellIndex + 0] = wgslX; // x cellBuffer[cellIndex + 1] = -wgslY; // y cellBuffer[cellIndex + 2] = 0; @@ -730,22 +756,22 @@ class FullFileRenderStrategy implements IRenderStrategy< const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; // Write buffer and swap it out to unblock writes - // this._device.queue.writeBuffer( - // this._cellBindBuffer, - // (startLineNumber - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, - // // TODO: this cell buffer actually only needs to be the size of the viewport if we are only uploading a range - // // at the maximum each frame - // cellBuffer, - // (startLineNumber - 1) * lineIndexCount, - // (stopLineNumber - startLineNumber) * lineIndexCount - // ); this._device.queue.writeBuffer( this._cellBindBuffer, - 0, - cellBuffer + (startLineNumber - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + // TODO: this cell buffer actually only needs to be the size of the viewport if we are only uploading a range + // at the maximum each frame + cellBuffer.buffer, + (startLineNumber - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + (stopLineNumber - startLineNumber) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT ); + // this._device.queue.writeBuffer( + // this._cellBindBuffer, + // 0, + // cellBuffer + // ); - this._cellValuesBufferActiveIndex = (this._cellValuesBufferActiveIndex + 1) % 2; + this._activeDoubleBufferIndex = (this._activeDoubleBufferIndex + 1) % 2; return visibleObjectCount; } @@ -753,18 +779,18 @@ class FullFileRenderStrategy implements IRenderStrategy< draw(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void { const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; - // console.log('draw', - // { - // ctx, - // startLineNumber, - // stopLineNumber, - // deltaTop - // }, - // 6, // square verticies - // visibleObjectCount, - // undefined, - // (startLineNumber - 1) * FullFileRenderStrategy._columnCount - // ); + console.log('draw', + { + ctx, + startLineNumber, + stopLineNumber, + deltaTop + }, + 6, // square verticies + visibleObjectCount, + undefined, + (startLineNumber - 1) * FullFileRenderStrategy._columnCount + ); pass.draw( 6, // square verticies visibleObjectCount, From a56c3cfab224f3e7efa62cbbdcb830778a32218a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:28:40 -0700 Subject: [PATCH 022/286] Fix scroll offset --- .../editor/browser/view/gpu/gpuViewLayer.ts | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 6987c31a869..adfdd5ade0a 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -660,7 +660,7 @@ class FullFileRenderStrategy implements IRenderStrategy< private _cellBindBuffer!: GPUBuffer; private _cellValueBuffers!: ArrayBuffer[]; - private _activeDoubleBufferIndex: number = 0; + private _activeDoubleBufferIndex: 0 | 1 = 0; private _scrollOffsetBindBuffer!: GPUBuffer; private _scrollOffsetValueBuffers!: Float32Array[]; @@ -714,10 +714,14 @@ class FullFileRenderStrategy implements IRenderStrategy< let wgslX: number = 0; let wgslY: number = 0; + const activeWindow = getActiveWindow(); + // Update scroll offset let scrollTop = parseInt(this._canvas.parentElement!.getAttribute('data-adjusted-scroll-top')!); if (Number.isNaN(scrollTop)) { scrollTop = 0; + } else { + scrollTop *= activeWindow.devicePixelRatio; } const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex]; scrollOffsetBuffer[1] = scrollTop; @@ -725,7 +729,6 @@ class FullFileRenderStrategy implements IRenderStrategy< // Update cell data const viewportData = this._viewportData; - const activeWindow = getActiveWindow(); const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; @@ -765,13 +768,14 @@ class FullFileRenderStrategy implements IRenderStrategy< (startLineNumber - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, (stopLineNumber - startLineNumber) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT ); + // HACK: Replace entire buffer for testing purposes // this._device.queue.writeBuffer( // this._cellBindBuffer, // 0, // cellBuffer // ); - this._activeDoubleBufferIndex = (this._activeDoubleBufferIndex + 1) % 2; + this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; return visibleObjectCount; } @@ -779,18 +783,6 @@ class FullFileRenderStrategy implements IRenderStrategy< draw(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void { const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; - console.log('draw', - { - ctx, - startLineNumber, - stopLineNumber, - deltaTop - }, - 6, // square verticies - visibleObjectCount, - undefined, - (startLineNumber - 1) * FullFileRenderStrategy._columnCount - ); pass.draw( 6, // square verticies visibleObjectCount, From 00fb56c91ef36c3db1e08673bbf25e5f50435f91 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:29:35 -0700 Subject: [PATCH 023/286] Improve types --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index adfdd5ade0a..4f0d678dc54 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -659,11 +659,11 @@ class FullFileRenderStrategy implements IRenderStrategy< readonly wgsl: string = fullFileRenderStrategyWgsl; private _cellBindBuffer!: GPUBuffer; - private _cellValueBuffers!: ArrayBuffer[]; + private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; private _activeDoubleBufferIndex: 0 | 1 = 0; private _scrollOffsetBindBuffer!: GPUBuffer; - private _scrollOffsetValueBuffers!: Float32Array[]; + private _scrollOffsetValueBuffers!: [Float32Array, Float32Array]; get bindGroupEntries(): GPUBindGroupEntry[] { return [ From 07f647a197a684feb7895d96318c68a972927b37 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:35:24 -0700 Subject: [PATCH 024/286] Cache up to date lines in buffer --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 4f0d678dc54..cf04744a13e 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -662,6 +662,8 @@ class FullFileRenderStrategy implements IRenderStrategy< private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; private _activeDoubleBufferIndex: 0 | 1 = 0; + private readonly _upToDateLines: [Set, Set] = [new Set(), new Set()]; + private _scrollOffsetBindBuffer!: GPUBuffer; private _scrollOffsetValueBuffers!: [Float32Array, Float32Array]; @@ -732,7 +734,12 @@ class FullFileRenderStrategy implements IRenderStrategy< const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; + const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex]; + for (y = startLineNumber; y <= stopLineNumber; y++) { + if (upToDateLines.has(y)) { + continue; + } const viewLineRenderingData = viewportData.getViewLineRenderingData(y); const content = viewLineRenderingData.content; for (x = 0; x < FullFileRenderStrategy._columnCount; x++) { @@ -754,8 +761,10 @@ class FullFileRenderStrategy implements IRenderStrategy< cellBuffer[cellIndex + 4] = glyph.index; // textureIndex cellBuffer[cellIndex + 5] = 0; } + upToDateLines.add(y); } + // TODO: Write sub set of buffer const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; // Write buffer and swap it out to unblock writes From 702e78178e751e9057d551a93e68d444d706363f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:38:44 -0700 Subject: [PATCH 025/286] Only write dirty lines --- .../editor/browser/view/gpu/gpuViewLayer.ts | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index cf04744a13e..0d2ff4ad067 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -735,11 +735,16 @@ class FullFileRenderStrategy implements IRenderStrategy< const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex]; + let dirtyLineStart = Number.MAX_SAFE_INTEGER; + let dirtyLineEnd = 0; for (y = startLineNumber; y <= stopLineNumber; y++) { if (upToDateLines.has(y)) { continue; } + dirtyLineStart = Math.min(dirtyLineStart, y); + dirtyLineEnd = Math.max(dirtyLineEnd, y); + const viewLineRenderingData = viewportData.getViewLineRenderingData(y); const content = viewLineRenderingData.content; for (x = 0; x < FullFileRenderStrategy._columnCount; x++) { @@ -767,16 +772,19 @@ class FullFileRenderStrategy implements IRenderStrategy< // TODO: Write sub set of buffer const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; - // Write buffer and swap it out to unblock writes - this._device.queue.writeBuffer( - this._cellBindBuffer, - (startLineNumber - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, - // TODO: this cell buffer actually only needs to be the size of the viewport if we are only uploading a range - // at the maximum each frame - cellBuffer.buffer, - (startLineNumber - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, - (stopLineNumber - startLineNumber) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT - ); + // Only write when there is changed data + if (dirtyLineStart <= dirtyLineEnd) { + // Write buffer and swap it out to unblock writes + this._device.queue.writeBuffer( + this._cellBindBuffer, + (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + // TODO: this cell buffer actually only needs to be the size of the viewport if we are only uploading a range + // at the maximum each frame + cellBuffer.buffer, + (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + (dirtyLineEnd - dirtyLineStart) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT + ); + } // HACK: Replace entire buffer for testing purposes // this._device.queue.writeBuffer( // this._cellBindBuffer, From 87d1332d9008e54bf2ba54bd940ec37f1df8847c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:46:10 -0700 Subject: [PATCH 026/286] Increase tab width --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 0d2ff4ad067..95b28bae02d 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -715,6 +715,8 @@ class FullFileRenderStrategy implements IRenderStrategy< let zeroToOneY: number = 0; let wgslX: number = 0; let wgslY: number = 0; + let chars: string = ''; + let xOffset: number = 0; const activeWindow = getActiveWindow(); @@ -747,10 +749,20 @@ class FullFileRenderStrategy implements IRenderStrategy< const viewLineRenderingData = viewportData.getViewLineRenderingData(y); const content = viewLineRenderingData.content; + xOffset = 0; for (x = 0; x < FullFileRenderStrategy._columnCount; x++) { const glyph = this._textureAtlas.getGlyph(content, x); + chars = content[x]; + switch (chars) { + case ' ': + continue; + case '\t': + // TODO: Pull actual tab size + xOffset += 3; + break; + } - screenAbsoluteX = x * 7 * activeWindow.devicePixelRatio; + screenAbsoluteX = (x + xOffset) * 7 * activeWindow.devicePixelRatio; // TODO: Send scroll offset instead of setting it here such that the cell data doesn't need to change when scrolling screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); zeroToOneX = screenAbsoluteX / this._canvas.width; @@ -758,7 +770,7 @@ class FullFileRenderStrategy implements IRenderStrategy< wgslX = zeroToOneX * 2 - 1; wgslY = zeroToOneY * 2 - 1; - const cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x - 1)) * Constants.IndicesPerCell; + const cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x - 1 + xOffset)) * Constants.IndicesPerCell; cellBuffer[cellIndex + 0] = wgslX; // x cellBuffer[cellIndex + 1] = -wgslY; // y cellBuffer[cellIndex + 2] = 0; @@ -769,7 +781,6 @@ class FullFileRenderStrategy implements IRenderStrategy< upToDateLines.add(y); } - // TODO: Write sub set of buffer const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; // Only write when there is changed data From 394886f8de1bd67a94b58aa6786eeb01413f3c6e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:58:39 -0700 Subject: [PATCH 027/286] Fix alignment of chars --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 4 ++-- src/vs/editor/browser/view/gpu/textureAtlas.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 95b28bae02d..94c4fc8aa32 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -626,7 +626,7 @@ struct VSOutput { var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position - ((spriteInfo.origin * 2) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions), + (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position + ((spriteInfo.origin * vec2f(2, -2)) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions), 0.0, 1.0 ); @@ -764,7 +764,7 @@ class FullFileRenderStrategy implements IRenderStrategy< screenAbsoluteX = (x + xOffset) * 7 * activeWindow.devicePixelRatio; // TODO: Send scroll offset instead of setting it here such that the cell data doesn't need to change when scrolling - screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); + screenAbsoluteY = (Math.round(deltaTop[y - startLineNumber] + viewportData.lineHeight) * activeWindow.devicePixelRatio); zeroToOneX = screenAbsoluteX / this._canvas.width; zeroToOneY = screenAbsoluteY / this._canvas.height; wgslX = zeroToOneX * 2 - 1; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 8e00750242c..be742ab4202 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -85,7 +85,7 @@ class GlyphRasterizer extends Disposable { willReadFrequently: true })); this._ctx.font = `${this._fontSize}px ${fontFamily}`; - this._ctx.textBaseline = 'alphabetic'; + this._ctx.textBaseline = 'top'; this._ctx.fillStyle = '#FFFFFF'; } @@ -95,7 +95,9 @@ class GlyphRasterizer extends Disposable { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); // TODO: Draw in middle using alphabetical baseline - this._ctx.fillText(chars, this._fontSize * 2, this._fontSize); + const originX = this._fontSize; + const originY = this._fontSize; + this._ctx.fillText(chars, originX, originY); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); // TODO: Hot path: Reuse object @@ -104,8 +106,8 @@ class GlyphRasterizer extends Disposable { source: this._canvas, boundingBox, originOffset: { - x: boundingBox.left - this._fontSize, - y: boundingBox.top - this._fontSize + x: boundingBox.left - originX, + y: boundingBox.top - originY } }; return result; From 97b5f42a5eb42d49af58f18345493cf6c1084b34 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 16:59:39 -0700 Subject: [PATCH 028/286] Fix glyph dims --- src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index a5904eeb8d9..d4091d19320 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -37,8 +37,8 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { // TODO: Handle end of atlas page // Draw glyph - const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left; - const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top; + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; // TODO: Prefer putImageData as it doesn't do blending or scaling this._ctx.drawImage( rasterizedGlyph.source, From 8b571152c00b00a080cc5ac4870dc4105321a49b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Mar 2024 17:14:26 -0700 Subject: [PATCH 029/286] Print token todos --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 94c4fc8aa32..79ae8f6528a 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -750,6 +750,12 @@ class FullFileRenderStrategy implements IRenderStrategy< const viewLineRenderingData = viewportData.getViewLineRenderingData(y); const content = viewLineRenderingData.content; xOffset = 0; + // TODO: Handle colors via viewLineRenderingData.tokens + console.log(viewLineRenderingData.tokens); + console.log('fgs'); + for (let i = 0; i < viewLineRenderingData.tokens.getCount(); i++) { + console.log(` ${viewLineRenderingData.tokens.getForeground(i)}`); + } for (x = 0; x < FullFileRenderStrategy._columnCount; x++) { const glyph = this._textureAtlas.getGlyph(content, x); chars = content[x]; @@ -763,8 +769,7 @@ class FullFileRenderStrategy implements IRenderStrategy< } screenAbsoluteX = (x + xOffset) * 7 * activeWindow.devicePixelRatio; - // TODO: Send scroll offset instead of setting it here such that the cell data doesn't need to change when scrolling - screenAbsoluteY = (Math.round(deltaTop[y - startLineNumber] + viewportData.lineHeight) * activeWindow.devicePixelRatio); + screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); zeroToOneX = screenAbsoluteX / this._canvas.width; zeroToOneY = screenAbsoluteY / this._canvas.height; wgslX = zeroToOneX * 2 - 1; From 717ec22ac7a0cacc1d5f984c661ae091477cb01d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Mar 2024 07:15:57 -0700 Subject: [PATCH 030/286] Colors, atlas warm up, texture atlas preview scaling --- src/vs/editor/browser/view.ts | 6 +- .../editor/browser/view/gpu/gpuViewLayer.ts | 341 ++++++------------ src/vs/editor/browser/view/gpu/multiKeyMap.ts | 58 +++ src/vs/editor/browser/view/gpu/taskQueue.ts | 166 +++++++++ .../editor/browser/view/gpu/textureAtlas.ts | 57 ++- src/vs/editor/browser/view/viewLayer.ts | 8 +- src/vs/editor/browser/view/viewOverlays.ts | 22 +- .../browser/viewParts/lines/viewLines.ts | 9 +- 8 files changed, 414 insertions(+), 253 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/multiKeyMap.ts create mode 100644 src/vs/editor/browser/view/gpu/taskQueue.ts diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index a803234c4b4..fb7d9c09f24 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -147,7 +147,7 @@ export class View extends ViewEventHandler { this._viewParts.push(this._scrollbar); // View Lines - this._viewLines = new ViewLines(this._context, this._linesContent); + this._viewLines = this._instantiationService.createInstance(ViewLines, this._context, this._linesContent); // View Zones this._viewZones = new ViewZones(this._context); @@ -161,7 +161,7 @@ export class View extends ViewEventHandler { const scrollDecoration = new ScrollDecorationViewPart(this._context); this._viewParts.push(scrollDecoration); - const contentViewOverlays = new ContentViewOverlays(this._context); + const contentViewOverlays = this._instantiationService.createInstance(ContentViewOverlays, this._context); this._viewParts.push(contentViewOverlays); contentViewOverlays.addDynamicOverlay(new CurrentLineHighlightOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new SelectionsOverlay(this._context)); @@ -169,7 +169,7 @@ export class View extends ViewEventHandler { contentViewOverlays.addDynamicOverlay(new DecorationsOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new WhitespaceOverlay(this._context)); - const marginViewOverlays = new MarginViewOverlays(this._context); + const marginViewOverlays = this._instantiationService.createInstance(MarginViewOverlays, this._context); this._viewParts.push(marginViewOverlays); marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context)); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 79ae8f6528a..baa92b9e83b 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -8,7 +8,10 @@ import { debounce } from 'vs/base/common/decorators'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; +import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; interface IRendererContext { rendLineNumberStart: number; @@ -71,7 +74,12 @@ export class GpuViewLayerRenderer { private _renderStrategy!: IRenderStrategy; - constructor(domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData) { + constructor( + domNode: HTMLCanvasElement, + host: IVisibleLinesHost, + viewportData: ViewportData, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { this.domNode = domNode; this.host = host; this.viewportData = viewportData; @@ -101,11 +109,13 @@ export class GpuViewLayerRenderer { // Create texture atlas if (!GpuViewLayerRenderer._textureAtlas) { - GpuViewLayerRenderer._textureAtlas = new TextureAtlas(this.domNode, this._device.limits.maxTextureDimension2D); + GpuViewLayerRenderer._textureAtlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, this._device.limits.maxTextureDimension2D); GpuViewLayerRenderer._testCanvas = document.createElement('canvas'); - GpuViewLayerRenderer._testCanvas.width = 2048; - GpuViewLayerRenderer._testCanvas.height = 2048; + GpuViewLayerRenderer._testCanvas.width = this._device.limits.maxTextureDimension2D; + GpuViewLayerRenderer._testCanvas.height = this._device.limits.maxTextureDimension2D; + GpuViewLayerRenderer._testCanvas.style.width = `${this._device.limits.maxTextureDimension2D / getActiveWindow().devicePixelRatio}px`; + GpuViewLayerRenderer._testCanvas.style.height = `${this._device.limits.maxTextureDimension2D / getActiveWindow().devicePixelRatio}px`; GpuViewLayerRenderer._testCanvas.style.position = 'absolute'; GpuViewLayerRenderer._testCanvas.style.top = '0'; GpuViewLayerRenderer._testCanvas.style.left = '0'; @@ -118,7 +128,7 @@ export class GpuViewLayerRenderer { // this._renderStrategy = new NaiveViewportRenderStrategy(this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); - this._renderStrategy = new FullFileRenderStrategy(this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); + this._renderStrategy = this._instantiationService.createInstance(FullFileRenderStrategy, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); const module = this._device.createShaderModule({ label: 'ViewLayer shader module', @@ -384,193 +394,6 @@ interface IRenderStrategy { draw?(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void; } -// #region Naive viewport render strategy - -const naiveViewportRenderStrategyWgsl = ` -struct Uniforms { - canvasDimensions: vec2f, -}; - -struct TextureInfoUniform { - spriteSheetSize: vec2f, -} - -struct SpriteInfo { - position: vec2f, - size: vec2f, - origin: vec2f, -}; - -struct Vertex { - @location(0) position: vec2f, -}; - -struct DynamicUnitInfo { - position: vec2f, - unused1: vec2f, - textureIndex: f32, - unused2: f32 -}; - -struct VSOutput { - @builtin(position) position: vec4f, - @location(0) texcoord: vec2f, -}; - -@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; -@group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; - -@group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; -@group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; - -@vertex fn vs( - vert: Vertex, - @builtin(instance_index) instanceIndex: u32, - @builtin(vertex_index) vertexIndex : u32 -) -> VSOutput { - let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; - let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureIndex)]; - - var vsOut: VSOutput; - // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 - vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position - ((spriteInfo.origin * 2) / uniforms.canvasDimensions), - 0.0, - 1.0 - ); - - // Textures are flipped from natural direction on the y-axis, so flip it back - vsOut.texcoord = vert.position; - vsOut.texcoord = ( - // Sprite offset (0-1) - (spriteInfo.position / textureInfoUniform.spriteSheetSize) + - // Sprite coordinate (0-1) - (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize)) - ); - - return vsOut; -} - -@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; -@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; - -@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { - return textureSample(ourTexture, ourSampler, vsOut.texcoord); -} -`; - -class NaiveViewportRenderStrategy implements IRenderStrategy { - readonly wgsl: string = naiveViewportRenderStrategyWgsl; - - private _cellBindBuffer!: GPUBuffer; - private _cellValueBuffers!: ArrayBuffer[]; - private _cellValuesBufferActiveIndex: number = 0; - - get bindGroupEntries(): GPUBindGroupEntry[] { - return [ - { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._cellBindBuffer } } - ]; - } - - constructor( - private readonly _device: GPUDevice, - private readonly _canvas: HTMLCanvasElement, - private readonly _viewportData: ViewportData, - private readonly _textureAtlas: TextureAtlas - ) { - } - - initBuffers(): void { - // TODO: Grow/shrink buffer size dynamically - const cellCount = 10000; - const bufferSize = cellCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; - this._cellBindBuffer = this._device.createBuffer({ - label: 'Entity dynamic info buffer', - size: bufferSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); - this._cellValueBuffers = [ - new ArrayBuffer(bufferSize), - new ArrayBuffer(bufferSize), - ]; - } - - update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { - const cellBuffer = new Float32Array(this._cellValueBuffers[this._cellValuesBufferActiveIndex]); - const visibleObjectCount = this._updateDataBuffer(cellBuffer, ctx, startLineNumber, stopLineNumber, deltaTop); - - // Write buffer and swap it out to unblock writes - this._device.queue.writeBuffer(this._cellBindBuffer, 0, cellBuffer, 0, visibleObjectCount * Constants.IndicesPerCell); - - this._cellValuesBufferActiveIndex = (this._cellValuesBufferActiveIndex + 1) % 2; - return visibleObjectCount; - } - - private _updateDataBuffer(dataBuffer: Float32Array, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { - // let chars: string = ''; - let screenAbsoluteX: number = 0; - let screenAbsoluteY: number = 0; - let zeroToOneX: number = 0; - let zeroToOneY: number = 0; - let wgslX: number = 0; - let wgslY: number = 0; - - const activeWindow = getActiveWindow(); - let charCount = 0; - let scrollTop = parseInt(this._canvas.parentElement!.getAttribute('data-adjusted-scroll-top')!); - if (Number.isNaN(scrollTop)) { - scrollTop = 0; - } - for (let lineNumber = startLineNumber; lineNumber <= stopLineNumber; lineNumber++) { - const y = Math.round(-scrollTop + deltaTop[lineNumber - startLineNumber]); - // Offscreen - if (y < 0) { - continue; - } - const content = this._viewportData.getViewLineRenderingData(lineNumber).content; - // console.log(content, 0, y); - for (let x = 0; x < content.length; x++) { - if (content.charAt(x) === ' ') { - continue; - } - // TODO: Handle tab - - // chars = content[x]; - const glyph = this._textureAtlas.getGlyph(content, x); - - // TODO: Move math to gpu - // TODO: Render using a line offset for partial line scrolling - // TODO: Sub-pixel rendering - screenAbsoluteX = x * 7 * activeWindow.devicePixelRatio; - // TODO: This +10 is because the glyph is being rendered in the wrong position - screenAbsoluteY = Math.round(y * activeWindow.devicePixelRatio); - zeroToOneX = screenAbsoluteX / this._canvas.width; - zeroToOneY = screenAbsoluteY / this._canvas.height; - wgslX = zeroToOneX * 2 - 1; - wgslY = zeroToOneY * 2 - 1; - - // TODO: We could upload the entire file as a grid, capping out lines at some reasonable amount (200?) - // Optimize for the common case and fallback to a slower path for long line files - // Doing the fast grid path would mean only the cell needs to change on data change, scrolling would simply change the start line index - // Even better would be a grid for standard sized lines (~120?) and then another buffer that handles larger lines in a slower but more dynamic way - - dataBuffer[charCount * Constants.IndicesPerCell + 0] = wgslX; // x - dataBuffer[charCount * Constants.IndicesPerCell + 1] = -wgslY; // y - dataBuffer[charCount * Constants.IndicesPerCell + 2] = 0; - dataBuffer[charCount * Constants.IndicesPerCell + 3] = 0; - dataBuffer[charCount * Constants.IndicesPerCell + 4] = glyph.index; // textureIndex - dataBuffer[charCount * Constants.IndicesPerCell + 5] = 0; - - charCount++; - } - } - // console.log('charCount: ' + charCount); - return charCount; - } -} - -// #endregion Naive viewport render strategy - // #region Full file render strategy const fullFileRenderStrategyWgsl = ` @@ -678,7 +501,8 @@ class FullFileRenderStrategy implements IRenderStrategy< private readonly _device: GPUDevice, private readonly _canvas: HTMLCanvasElement, private readonly _viewportData: ViewportData, - private readonly _textureAtlas: TextureAtlas + private readonly _textureAtlas: TextureAtlas, + @IThemeService private readonly _themeService: IThemeService, ) { } @@ -707,16 +531,18 @@ class FullFileRenderStrategy implements IRenderStrategy< } update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { + let chars = ''; let y = 0; let x = 0; - let screenAbsoluteX: number = 0; - let screenAbsoluteY: number = 0; - let zeroToOneX: number = 0; - let zeroToOneY: number = 0; - let wgslX: number = 0; - let wgslY: number = 0; - let chars: string = ''; - let xOffset: number = 0; + let screenAbsoluteX = 0; + let screenAbsoluteY = 0; + let zeroToOneX = 0; + let zeroToOneY = 0; + let wgslX = 0; + let wgslY = 0; + let xOffset = 0; + + let tokens: IViewLineTokens; const activeWindow = getActiveWindow(); @@ -740,6 +566,11 @@ class FullFileRenderStrategy implements IRenderStrategy< let dirtyLineStart = Number.MAX_SAFE_INTEGER; let dirtyLineEnd = 0; + const colorMap = this._themeService.getColorTheme().tokenColorMap; + // const theme = this._themeService.getColorTheme() as ColorThemeData; + // const tokenStyle = theme.getTokenStyleMetadata(type, modifiers, defaultLanguage, true, definitions); + // console.log('colorMap', colorMap); + for (y = startLineNumber; y <= stopLineNumber; y++) { if (upToDateLines.has(y)) { continue; @@ -747,42 +578,90 @@ class FullFileRenderStrategy implements IRenderStrategy< dirtyLineStart = Math.min(dirtyLineStart, y); dirtyLineEnd = Math.max(dirtyLineEnd, y); - const viewLineRenderingData = viewportData.getViewLineRenderingData(y); - const content = viewLineRenderingData.content; + const lineData = viewportData.getViewLineRenderingData(y); + const content = lineData.content; xOffset = 0; + // TODO: Handle colors via viewLineRenderingData.tokens - console.log(viewLineRenderingData.tokens); - console.log('fgs'); - for (let i = 0; i < viewLineRenderingData.tokens.getCount(); i++) { - console.log(` ${viewLineRenderingData.tokens.getForeground(i)}`); - } - for (x = 0; x < FullFileRenderStrategy._columnCount; x++) { - const glyph = this._textureAtlas.getGlyph(content, x); - chars = content[x]; - switch (chars) { - case ' ': - continue; - case '\t': - // TODO: Pull actual tab size - xOffset += 3; + // console.log(lineData.tokens); + // console.log('fg'); + // for (let i = 0; i < lineData.tokens.getCount(); i++) { + // console.log(` ${lineData.tokens.getForeground(i)}`); + // } + + // See ViewLine#renderLine + // const renderLineInput = new RenderLineInput( + // options.useMonospaceOptimizations, + // options.canUseHalfwidthRightwardsArrow, + // lineData.content, + // lineData.continuesWithWrappedLine, + // lineData.isBasicASCII, + // lineData.containsRTL, + // lineData.minColumn - 1, + // lineData.tokens, + // actualInlineDecorations, + // lineData.tabSize, + // lineData.startVisibleColumn, + // options.spaceWidth, + // options.middotWidth, + // options.wsmiddotWidth, + // options.stopRenderingLineAfter, + // options.renderWhitespace, + // options.renderControlCharacters, + // options.fontLigatures !== EditorFontLigatures.OFF, + // selectionsOnLine + // ); + + tokens = lineData.tokens; + let tokenStartIndex = lineData.minColumn - 1; + let tokenFg: number; + for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { + const tokenEndIndex = tokens.getEndOffset(tokenIndex); + if (tokenEndIndex <= tokenStartIndex) { + // The faux indent part of the line should have no token type + continue; + } + tokenFg = tokens.getForeground(tokenIndex); + // console.log(`token: start=${tokenStartIndex}, end=${tokenEndIndex}, fg=${colorMap[tokenFg]}`); + + + for (x = tokenStartIndex; x < tokenEndIndex; x++) { + // HACK: Prevent rendering past the end of the render buffer + // TODO: This needs to move to a dynamic long line rendering strategy + if (x > FullFileRenderStrategy._columnCount) { break; + } + chars = content.charAt(x); + switch (chars) { + case ' ': + continue; + case '\t': + // TODO: Pull actual tab size + xOffset += 3; + break; + } + + const glyph = this._textureAtlas.getGlyph(chars, tokenFg); + + screenAbsoluteX = (x + xOffset) * 7 * activeWindow.devicePixelRatio; + screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); + zeroToOneX = screenAbsoluteX / this._canvas.width; + zeroToOneY = screenAbsoluteY / this._canvas.height; + wgslX = zeroToOneX * 2 - 1; + wgslY = zeroToOneY * 2 - 1; + + const cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x - 1 + xOffset)) * Constants.IndicesPerCell; + cellBuffer[cellIndex + 0] = wgslX; // x + cellBuffer[cellIndex + 1] = -wgslY; // y + cellBuffer[cellIndex + 2] = 0; + cellBuffer[cellIndex + 3] = 0; + cellBuffer[cellIndex + 4] = glyph.index; // textureIndex + cellBuffer[cellIndex + 5] = 0; } - screenAbsoluteX = (x + xOffset) * 7 * activeWindow.devicePixelRatio; - screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); - zeroToOneX = screenAbsoluteX / this._canvas.width; - zeroToOneY = screenAbsoluteY / this._canvas.height; - wgslX = zeroToOneX * 2 - 1; - wgslY = zeroToOneY * 2 - 1; - - const cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x - 1 + xOffset)) * Constants.IndicesPerCell; - cellBuffer[cellIndex + 0] = wgslX; // x - cellBuffer[cellIndex + 1] = -wgslY; // y - cellBuffer[cellIndex + 2] = 0; - cellBuffer[cellIndex + 3] = 0; - cellBuffer[cellIndex + 4] = glyph.index; // textureIndex - cellBuffer[cellIndex + 5] = 0; + tokenStartIndex = tokenEndIndex; } + upToDateLines.add(y); } diff --git a/src/vs/editor/browser/view/gpu/multiKeyMap.ts b/src/vs/editor/browser/view/gpu/multiKeyMap.ts new file mode 100644 index 00000000000..52e49db234c --- /dev/null +++ b/src/vs/editor/browser/view/gpu/multiKeyMap.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export class TwoKeyMap { + private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {}; + + public set(first: TFirst, second: TSecond, value: TValue): void { + if (!this._data[first]) { + this._data[first] = {}; + } + this._data[first as string | number]![second] = value; + } + + public get(first: TFirst, second: TSecond): TValue | undefined { + return this._data[first as string | number] ? this._data[first as string | number]![second] : undefined; + } + + public clear(): void { + this._data = {}; + } + + public *values() { + for (const first in this._data) { + for (const second in this._data[first]) { + const value = this._data[first]![second]; + if (value) { + yield value; + } + } + } + } +} + +export class FourKeyMap { + private _data: TwoKeyMap> = new TwoKeyMap(); + + public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void { + if (!this._data.get(first, second)) { + this._data.set(first, second, new TwoKeyMap()); + } + this._data.get(first, second)!.set(third, fourth, value); + } + + public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined { + return this._data.get(first, second)?.get(third, fourth); + } + + public clear(): void { + this._data.clear(); + } +} diff --git a/src/vs/editor/browser/view/gpu/taskQueue.ts b/src/vs/editor/browser/view/gpu/taskQueue.ts new file mode 100644 index 00000000000..5b96daf2567 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/taskQueue.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from 'vs/base/browser/dom'; + +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +interface ITaskQueue { + /** + * Adds a task to the queue which will run in a future idle callback. + * To avoid perceivable stalls on the mainthread, tasks with heavy workload + * should split their work into smaller pieces and return `true` to get + * called again until the work is done (on falsy return value). + */ + enqueue(task: () => boolean | void): void; + + /** + * Flushes the queue, running all remaining tasks synchronously. + */ + flush(): void; + + /** + * Clears any remaining tasks from the queue, these will not be run. + */ + clear(): void; +} + +interface ITaskDeadline { + timeRemaining(): number; +} +type CallbackWithDeadline = (deadline: ITaskDeadline) => void; + +abstract class TaskQueue implements ITaskQueue { + private _tasks: (() => boolean | void)[] = []; + private _idleCallback?: number; + private _i = 0; + + protected abstract _requestCallback(callback: CallbackWithDeadline): number; + protected abstract _cancelCallback(identifier: number): void; + + public enqueue(task: () => boolean | void): void { + this._tasks.push(task); + this._start(); + } + + public flush(): void { + while (this._i < this._tasks.length) { + if (!this._tasks[this._i]()) { + this._i++; + } + } + this.clear(); + } + + public clear(): void { + if (this._idleCallback) { + this._cancelCallback(this._idleCallback); + this._idleCallback = undefined; + } + this._i = 0; + this._tasks.length = 0; + } + + private _start(): void { + if (!this._idleCallback) { + this._idleCallback = this._requestCallback(this._process.bind(this)); + } + } + + private _process(deadline: ITaskDeadline): void { + this._idleCallback = undefined; + let taskDuration = 0; + let longestTask = 0; + let lastDeadlineRemaining = deadline.timeRemaining(); + let deadlineRemaining = 0; + while (this._i < this._tasks.length) { + taskDuration = Date.now(); + if (!this._tasks[this._i]()) { + this._i++; + } + // other than performance.now, Date.now might not be stable (changes on wall clock changes), + // this is not an issue here as a clock change during a short running task is very unlikely + // in case it still happened and leads to negative duration, simply assume 1 msec + taskDuration = Math.max(1, Date.now() - taskDuration); + longestTask = Math.max(taskDuration, longestTask); + // Guess the following task will take a similar time to the longest task in this batch, allow + // additional room to try avoid exceeding the deadline + deadlineRemaining = deadline.timeRemaining(); + if (longestTask * 1.5 > deadlineRemaining) { + // Warn when the time exceeding the deadline is over 20ms, if this happens in practice the + // task should be split into sub-tasks to ensure the UI remains responsive. + if (lastDeadlineRemaining - taskDuration < -20) { + console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`); + } + this._start(); + return; + } + lastDeadlineRemaining = deadlineRemaining; + } + this.clear(); + } +} + +/** + * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames + * per second. The tasks will run in the order they are enqueued, but they will run some time later, + * and care should be taken to ensure they're non-urgent and will not introduce race conditions. + */ +export class PriorityTaskQueue extends TaskQueue { + protected _requestCallback(callback: CallbackWithDeadline): number { + return getActiveWindow().setTimeout(() => callback(this._createDeadline(16))); + } + + protected _cancelCallback(identifier: number): void { + getActiveWindow().clearTimeout(identifier); + } + + private _createDeadline(duration: number): ITaskDeadline { + const end = Date.now() + duration; + return { + timeRemaining: () => Math.max(0, end - Date.now()) + }; + } +} + +/** + * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's + * deadline given by the environment. The tasks will run in the order they are enqueued, but they + * will run some time later, and care should be taken to ensure they're non-urgent and will not + * introduce race conditions. + */ +export class IdleTaskQueue extends TaskQueue { + protected _requestCallback(callback: IdleRequestCallback): number { + return getActiveWindow().requestIdleCallback(callback); + } + + protected _cancelCallback(identifier: number): void { + getActiveWindow().cancelIdleCallback(identifier); + } +} + +/** + * An object that tracks a single debounced task that will run on the next idle frame. When called + * multiple times, only the last set task will run. + */ +export class DebouncedIdleTask { + private _queue: ITaskQueue; + + constructor() { + this._queue = new IdleTaskQueue(); + } + + public set(task: () => boolean | void): void { + this._queue.clear(); + this._queue.enqueue(task); + } + + public flush(): void { + this._queue.flush(); + } +} diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index be742ab4202..aa8851f4e04 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -4,22 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; +import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { ITextureAtlasAllocator, TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TextureAtlas extends Disposable { private readonly _canvas: OffscreenCanvas; private readonly _ctx: OffscreenCanvasRenderingContext2D; - private readonly _glyphMap: Map = new Map(); + private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); + // HACK: This is an ordered set of glyphs to be passed to the GPU since currently the shader + // uses the index of the glyph. This should be improved to derive from _glyphMap + private readonly _glyphInOrderSet: Set = new Set(); public get glyphs(): IterableIterator { - return this._glyphMap.values(); + return this._glyphInOrderSet.values(); } private readonly _glyphRasterizer: GlyphRasterizer; private readonly _allocator: ITextureAtlasAllocator; + private _colorMap!: string[]; + private _warmUpTask?: IdleTaskQueue; + public get source(): OffscreenCanvas { return this._canvas; } @@ -27,7 +37,11 @@ export class TextureAtlas extends Disposable { public hasChanges = false; // TODO: Should pull in the font size from config instead of random dom node - constructor(parentDomNode: HTMLElement, maxTextureSize: number) { + constructor( + parentDomNode: HTMLElement, + maxTextureSize: number, + @IThemeService private readonly _themeService: IThemeService + ) { super(); this._canvas = new OffscreenCanvas(maxTextureSize, maxTextureSize); @@ -40,6 +54,12 @@ export class TextureAtlas extends Disposable { const fontSize = Math.ceil(parseInt(style.fontSize.replace('px', '')) * activeWindow.devicePixelRatio); this._ctx.font = `${fontSize}px ${style.fontFamily}`; + this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { + // TODO: Clear entire atlas on theme change + this._colorMap = this._themeService.getColorTheme().tokenColorMap; + this._warmUpAtlas(); + })); + this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); @@ -51,25 +71,43 @@ export class TextureAtlas extends Disposable { } // TODO: Color, style etc. - public getGlyph(lineContent: string, glyphIndex: number): ITextureAtlasGlyph { - const chars = lineContent.charAt(glyphIndex); - let glyph: ITextureAtlasGlyph | undefined = this._glyphMap.get(chars); + public getGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { + let glyph: ITextureAtlasGlyph | undefined = this._glyphMap.get(chars, tokenFg); if (glyph) { return glyph; } - const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars); + const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg]); glyph = this._allocator.allocate(rasterizedGlyph); - this._glyphMap.set(chars, glyph); + this._glyphMap.set(chars, tokenFg, glyph); + this._glyphInOrderSet.add(glyph); this.hasChanges = true; console.log('New glyph', { chars, + fg: this._colorMap[tokenFg], rasterizedGlyph, glyph }); return glyph; } + + /** + * Warms up the atlas by rasterizing all printable ASCII characters for each token color. This + * is distrubuted over multiple idle callbacks to avoid blocking the main thread. + */ + private _warmUpAtlas(): void { + // TODO: Clean up on dispose + this._warmUpTask?.clear(); + this._warmUpTask = new IdleTaskQueue(); + for (const tokenFg of this._colorMap.keys()) { + this._warmUpTask.enqueue(() => { + for (let code = 33; code < 126; code++) { + this.getGlyph(String.fromCharCode(code), tokenFg); + } + }); + } + } } class GlyphRasterizer extends Disposable { @@ -91,12 +129,13 @@ class GlyphRasterizer extends Disposable { // TODO: Support drawing multiple fonts and sizes // TODO: Should pull in the font size from config instead of random dom node - public rasterizeGlyph(chars: string): IRasterizedGlyph { + public rasterizeGlyph(chars: string, fg: string): IRasterizedGlyph { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); // TODO: Draw in middle using alphabetical baseline const originX = this._fontSize; const originY = this._fontSize; + this._ctx.fillStyle = fg; this._ctx.fillText(chars, originX, originY); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 948a739b4fd..c7befc25e51 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -13,6 +13,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** * Represents a visible line @@ -260,7 +261,10 @@ export class VisibleLinesCollection { private readonly _canvas: HTMLCanvasElement; - constructor(host: IVisibleLinesHost) { + constructor( + host: IVisibleLinesHost, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { this._host = host; this.domNode = this._createDomNode(); @@ -375,7 +379,7 @@ export class VisibleLinesCollection { } if (!this._gpuRenderer) { - this._gpuRenderer = new GpuViewLayerRenderer(this._canvas, this._host, viewportData); + this._gpuRenderer = this._instantiationService.createInstance(GpuViewLayerRenderer, this._canvas, this._host, viewportData); } renderer = this._gpuRenderer; renderer.update(viewportData); diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 4d1b5a99581..b674d855fe4 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -14,6 +14,7 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ViewOverlays extends ViewPart implements IVisibleLinesHost { @@ -22,10 +23,13 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost(this); + this._visibleLines = instantiationService.createInstance(VisibleLinesCollection, this); this.domNode = this._visibleLines.domNode; const options = this._context.configuration.options; @@ -207,8 +211,11 @@ export class ContentViewOverlays extends ViewOverlays { private _contentWidth: number; - constructor(context: ViewContext) { - super(context); + constructor( + context: ViewContext, + @IInstantiationService instantiationService: IInstantiationService + ) { + super(context, instantiationService); const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); this._contentWidth = layoutInfo.contentWidth; @@ -241,8 +248,11 @@ export class MarginViewOverlays extends ViewOverlays { private _contentLeft: number; - constructor(context: ViewContext) { - super(context); + constructor( + context: ViewContext, + @IInstantiationService instantiationService: IInstantiationService + ) { + super(context, instantiationService); const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 62928fb5ca3..d99b920e065 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -24,6 +24,7 @@ import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { Viewport } from 'vs/editor/common/viewModel'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; class LastRenderedData { @@ -120,11 +121,15 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, private _stickyScrollEnabled: boolean; private _maxNumberStickyLines: number; - constructor(context: ViewContext, linesContent: FastDomNode) { + constructor( + context: ViewContext, + linesContent: FastDomNode, + @IInstantiationService instantiationService: IInstantiationService + ) { super(context); this._linesContent = linesContent; this._textRangeRestingSpot = document.createElement('div'); - this._visibleLines = new VisibleLinesCollection(this); + this._visibleLines = instantiationService.createInstance(VisibleLinesCollection, this); this.domNode = this._visibleLines.domNode; const conf = this._context.configuration; From 71e5eee25c71c57238328b671008462b06a465fc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Mar 2024 07:20:38 -0700 Subject: [PATCH 031/286] Don't draw \t, warm tilde --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 18 +++++++++--------- src/vs/editor/browser/view/gpu/textureAtlas.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index baa92b9e83b..8530b912a4a 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -117,8 +117,8 @@ export class GpuViewLayerRenderer { GpuViewLayerRenderer._testCanvas.style.width = `${this._device.limits.maxTextureDimension2D / getActiveWindow().devicePixelRatio}px`; GpuViewLayerRenderer._testCanvas.style.height = `${this._device.limits.maxTextureDimension2D / getActiveWindow().devicePixelRatio}px`; GpuViewLayerRenderer._testCanvas.style.position = 'absolute'; - GpuViewLayerRenderer._testCanvas.style.top = '0'; - GpuViewLayerRenderer._testCanvas.style.left = '0'; + GpuViewLayerRenderer._testCanvas.style.top = '2px'; + GpuViewLayerRenderer._testCanvas.style.left = '2px'; GpuViewLayerRenderer._testCanvas.style.zIndex = '10000'; GpuViewLayerRenderer._testCanvas.style.pointerEvents = 'none'; GpuViewLayerRenderer._testCtx = ensureNonNullable(GpuViewLayerRenderer._testCanvas.getContext('2d')); @@ -632,13 +632,13 @@ class FullFileRenderStrategy implements IRenderStrategy< break; } chars = content.charAt(x); - switch (chars) { - case ' ': - continue; - case '\t': - // TODO: Pull actual tab size - xOffset += 3; - break; + if (chars === ' ') { + continue; + } + if (chars === '\t') { + // TODO: Pull actual tab size + xOffset += 3; + continue; } const glyph = this._textureAtlas.getGlyph(chars, tokenFg); diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index aa8851f4e04..e1df0326722 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -102,7 +102,7 @@ export class TextureAtlas extends Disposable { this._warmUpTask = new IdleTaskQueue(); for (const tokenFg of this._colorMap.keys()) { this._warmUpTask.enqueue(() => { - for (let code = 33; code < 126; code++) { + for (let code = 33; code <= 126; code++) { this.getGlyph(String.fromCharCode(code), tokenFg); } }); From 8549f0ec7e42beb926bc79cc3e63bb53b7cdb4bd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Mar 2024 07:29:02 -0700 Subject: [PATCH 032/286] Round x to render it properly --- .../editor/browser/view/gpu/gpuViewLayer.ts | 2 +- .../editor/browser/view/gpu/textureAtlas.ts | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 8530b912a4a..e973036c15f 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -643,7 +643,7 @@ class FullFileRenderStrategy implements IRenderStrategy< const glyph = this._textureAtlas.getGlyph(chars, tokenFg); - screenAbsoluteX = (x + xOffset) * 7 * activeWindow.devicePixelRatio; + screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); zeroToOneX = screenAbsoluteX / this._canvas.width; zeroToOneY = screenAbsoluteY / this._canvas.height; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index e1df0326722..078bb58a883 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -12,6 +12,39 @@ import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { ITextureAtlasAllocator, TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +// DEBUG: This helper can be used to draw image data to the console, it's commented out as we don't +// want to ship it, but this is very useful for investigating texture atlas issues. +// (console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => { +// function getBox(width: number, height: number) { +// return { +// string: '+', +// style: 'font-size: 1px; padding: ' + Math.floor(height / 2) + 'px ' + Math.floor(width / 2) + 'px; line-height: ' + height + 'px;' +// }; +// } +// if (source instanceof HTMLCanvasElement) { +// source = source.getContext('2d')?.getImageData(0, 0, source.width, source.height)!; +// } +// const canvas = document.createElement('canvas'); +// canvas.width = source.width; +// canvas.height = source.height; +// const ctx = canvas.getContext('2d')!; +// ctx.putImageData(source, 0, 0); + +// const sw = source.width * scale; +// const sh = source.height * scale; +// const dim = getBox(sw, sh); +// console.log( +// `Image: ${source.width} x ${source.height}\n%c${dim.string}`, +// `${dim.style}background: url(${canvas.toDataURL()}); background-size: ${sw}px ${sh}px; background-repeat: no-repeat; color: transparent;` +// ); +// console.groupCollapsed('Zoomed'); +// console.log( +// `%c${dim.string}`, +// `${getBox(sw * 10, sh * 10).style}background: url(${canvas.toDataURL()}); background-size: ${sw * 10}px ${sh * 10}px; background-repeat: no-repeat; color: transparent; image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;` +// ); +// console.groupEnd(); +// }; + export class TextureAtlas extends Disposable { private readonly _canvas: OffscreenCanvas; private readonly _ctx: OffscreenCanvasRenderingContext2D; @@ -149,6 +182,10 @@ class GlyphRasterizer extends Disposable { y: boundingBox.top - originY } }; + + // DEBUG: Show image data in console + // (console as any).image(imageData); + return result; } From 4d3544a80dd506ec00526392026f832cb0b40d57 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Mar 2024 07:57:19 -0700 Subject: [PATCH 033/286] Clean up --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 1 + src/vs/editor/browser/view/gpu/textureAtlas.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index e973036c15f..df76dc099cd 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -504,6 +504,7 @@ class FullFileRenderStrategy implements IRenderStrategy< private readonly _textureAtlas: TextureAtlas, @IThemeService private readonly _themeService: IThemeService, ) { + // TODO: Detect when lines have been tokenized and clear _upToDateLines } initBuffers(): void { diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 078bb58a883..59c09432435 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from 'vs/base/browser/dom'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; From 13f349743843340d748119cad069a3439597ca14 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Mar 2024 10:23:25 -0700 Subject: [PATCH 034/286] Fix draw buffer indexes --- .../editor/browser/view/gpu/gpuViewLayer.ts | 28 +++++++++++-------- .../editor/browser/view/gpu/textureAtlas.ts | 16 ++++++----- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index df76dc099cd..6639ee1d993 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -505,6 +505,8 @@ class FullFileRenderStrategy implements IRenderStrategy< @IThemeService private readonly _themeService: IThemeService, ) { // TODO: Detect when lines have been tokenized and clear _upToDateLines + const colorMap = this._themeService.getColorTheme().tokenColorMap; + console.log('colorMap', colorMap); } initBuffers(): void { @@ -567,10 +569,8 @@ class FullFileRenderStrategy implements IRenderStrategy< let dirtyLineStart = Number.MAX_SAFE_INTEGER; let dirtyLineEnd = 0; - const colorMap = this._themeService.getColorTheme().tokenColorMap; // const theme = this._themeService.getColorTheme() as ColorThemeData; // const tokenStyle = theme.getTokenStyleMetadata(type, modifiers, defaultLanguage, true, definitions); - // console.log('colorMap', colorMap); for (y = startLineNumber; y <= stopLineNumber; y++) { if (upToDateLines.has(y)) { @@ -651,7 +651,7 @@ class FullFileRenderStrategy implements IRenderStrategy< wgslX = zeroToOneX * 2 - 1; wgslY = zeroToOneY * 2 - 1; - const cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x - 1 + xOffset)) * Constants.IndicesPerCell; + const cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x + xOffset)) * Constants.IndicesPerCell; cellBuffer[cellIndex + 0] = wgslX; // x cellBuffer[cellIndex + 1] = -wgslY; // y cellBuffer[cellIndex + 2] = 0; @@ -666,7 +666,7 @@ class FullFileRenderStrategy implements IRenderStrategy< upToDateLines.add(y); } - const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; + const visibleObjectCount = (stopLineNumber - startLineNumber + 1) * lineIndexCount; // Only write when there is changed data if (dirtyLineStart <= dirtyLineEnd) { @@ -678,7 +678,7 @@ class FullFileRenderStrategy implements IRenderStrategy< // at the maximum each frame cellBuffer.buffer, (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, - (dirtyLineEnd - dirtyLineStart) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT + (dirtyLineEnd - dirtyLineStart + 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT ); } // HACK: Replace entire buffer for testing purposes @@ -694,14 +694,18 @@ class FullFileRenderStrategy implements IRenderStrategy< } draw(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void { - const visibleObjectCount = (stopLineNumber - startLineNumber) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; + const visibleObjectCount = (stopLineNumber - startLineNumber + 1) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; - pass.draw( - 6, // square verticies - visibleObjectCount, - undefined, - (startLineNumber - 1) * FullFileRenderStrategy._columnCount - ); + if (visibleObjectCount <= 0) { + console.error('Attempt to draw 0 objects'); + } else { + pass.draw( + 6, // square verticies + visibleObjectCount, + undefined, + (startLineNumber - 1) * FullFileRenderStrategy._columnCount + ); + } } } diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 59c09432435..ec68a482861 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from 'vs/base/browser/dom'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; @@ -115,12 +115,14 @@ export class TextureAtlas extends Disposable { this._glyphInOrderSet.add(glyph); this.hasChanges = true; - console.log('New glyph', { - chars, - fg: this._colorMap[tokenFg], - rasterizedGlyph, - glyph - }); + if (!this._warmUpTask) { + console.debug('New glyph', { + chars, + fg: this._colorMap[tokenFg], + rasterizedGlyph, + glyph + }); + } return glyph; } From 9534f8f280fa5595ebb10ed7f08681174ce5cabd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Mar 2024 10:36:26 -0700 Subject: [PATCH 035/286] Get scroll top reliably --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 8 ++------ src/vs/editor/browser/viewParts/lines/viewLines.ts | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 6639ee1d993..40cf07390f8 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -550,12 +550,8 @@ class FullFileRenderStrategy implements IRenderStrategy< const activeWindow = getActiveWindow(); // Update scroll offset - let scrollTop = parseInt(this._canvas.parentElement!.getAttribute('data-adjusted-scroll-top')!); - if (Number.isNaN(scrollTop)) { - scrollTop = 0; - } else { - scrollTop *= activeWindow.devicePixelRatio; - } + // TODO: Get at ViewModel in a safe way + const scrollTop = (this._viewportData as any)._model.viewLayout.getCurrentScrollTop() * activeWindow.devicePixelRatio; const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex]; scrollOffsetBuffer[1] = scrollTop; this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, scrollOffsetBuffer); diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index d99b920e065..0d387704a6f 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -667,8 +667,6 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, // (3) handle scrolling this._linesContent.setLayerHinting(this._canUseLayerHinting); this._linesContent.setContain('strict'); - const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta; - this.domNode.domNode.setAttribute('data-adjusted-scroll-top', adjustedScrollTop.toString()); // this._linesContent.setTop(-adjustedScrollTop); // this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft()); } From 1ac620aeca2a8858d2c60e49e6c34ccc0dee04b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 31 Mar 2024 10:47:06 -0700 Subject: [PATCH 036/286] Clear end of line --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 40cf07390f8..33564d0ba1f 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -569,9 +569,10 @@ class FullFileRenderStrategy implements IRenderStrategy< // const tokenStyle = theme.getTokenStyleMetadata(type, modifiers, defaultLanguage, true, definitions); for (y = startLineNumber; y <= stopLineNumber; y++) { - if (upToDateLines.has(y)) { - continue; - } + // TODO: Update on dirty lines; is this known by line before rendering? + // if (upToDateLines.has(y)) { + // continue; + // } dirtyLineStart = Math.min(dirtyLineStart, y); dirtyLineEnd = Math.max(dirtyLineEnd, y); @@ -611,9 +612,10 @@ class FullFileRenderStrategy implements IRenderStrategy< tokens = lineData.tokens; let tokenStartIndex = lineData.minColumn - 1; + let tokenEndIndex = 0; let tokenFg: number; for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { - const tokenEndIndex = tokens.getEndOffset(tokenIndex); + tokenEndIndex = tokens.getEndOffset(tokenIndex); if (tokenEndIndex <= tokenStartIndex) { // The faux indent part of the line should have no token type continue; @@ -659,6 +661,11 @@ class FullFileRenderStrategy implements IRenderStrategy< tokenStartIndex = tokenEndIndex; } + // Clear to end of line + const fillStartIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (tokenEndIndex + xOffset)) * Constants.IndicesPerCell; + const fillEndIndex = (y * FullFileRenderStrategy._columnCount) * Constants.IndicesPerCell; + cellBuffer.fill(0, fillStartIndex, fillEndIndex); + upToDateLines.add(y); } From c3b777b5b942b079e147ebb17d5282e638169d06 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:28:06 -0700 Subject: [PATCH 037/286] Add webgpu types back --- yarn.lock | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index fe761f702c8..65aefb226bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1890,17 +1890,6 @@ "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" -<<<<<<< HEAD -"@webgpu/types@^0.1.40": - version "0.1.40" - resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.40.tgz#cf72d1df6f9f8adc5d39556041f20ff2e8a58885" - integrity sha512-/BBkHLS6/eQjyWhY2H7Dx5DHcVrS2ICj9owvSRdgtQT6KcafLZA86tPze0xAOsd4FbsYKCUBUQyNi87q7gV7kw== - -"@webpack-cli/configtest@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz#a69720f6c9bad6aef54a8fa6ba9c3533e7ef4c7f" - integrity sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A== -======= "@webassemblyjs/wast-printer@1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" @@ -1908,7 +1897,11 @@ dependencies: "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" ->>>>>>> origin/main + +"@webgpu/types@^0.1.40": + version "0.1.40" + resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.40.tgz#cf72d1df6f9f8adc5d39556041f20ff2e8a58885" + integrity sha512-/BBkHLS6/eQjyWhY2H7Dx5DHcVrS2ICj9owvSRdgtQT6KcafLZA86tPze0xAOsd4FbsYKCUBUQyNi87q7gV7kw== "@webpack-cli/configtest@^2.1.1": version "2.1.1" From 604699c0436be9a4096fce212ea6f52016706ab9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:47:10 -0700 Subject: [PATCH 038/286] Optimize update data --- .../editor/browser/view/gpu/gpuViewLayer.ts | 31 +++++++++++++------ .../editor/browser/view/gpu/textureAtlas.ts | 10 +++--- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 33564d0ba1f..029ecf1c77a 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -6,10 +6,11 @@ import { getActiveDocument, getActiveWindow } from 'vs/base/browser/dom'; import { debounce } from 'vs/base/common/decorators'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { TextureAtlas } from 'vs/editor/browser/view/gpu/textureAtlas'; +import { TextureAtlas, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; +import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -534,6 +535,8 @@ class FullFileRenderStrategy implements IRenderStrategy< } update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { + // Pre-allocate variables to be shared within the loop - don't trust the JIT compiler to do + // this optimization to avoid additional blocking time in garbage collector let chars = ''; let y = 0; let x = 0; @@ -544,6 +547,15 @@ class FullFileRenderStrategy implements IRenderStrategy< let wgslX = 0; let wgslY = 0; let xOffset = 0; + let glyph: ITextureAtlasGlyph; + let cellIndex = 0; + let tokenStartIndex = 0; + let tokenEndIndex = 0; + let tokenFg = 0; + let lineData: ViewLineRenderingData; + let content: string = ''; + let fillStartIndex = 0; + let fillEndIndex = 0; let tokens: IViewLineTokens; @@ -576,8 +588,8 @@ class FullFileRenderStrategy implements IRenderStrategy< dirtyLineStart = Math.min(dirtyLineStart, y); dirtyLineEnd = Math.max(dirtyLineEnd, y); - const lineData = viewportData.getViewLineRenderingData(y); - const content = lineData.content; + lineData = viewportData.getViewLineRenderingData(y); + content = lineData.content; xOffset = 0; // TODO: Handle colors via viewLineRenderingData.tokens @@ -611,9 +623,8 @@ class FullFileRenderStrategy implements IRenderStrategy< // ); tokens = lineData.tokens; - let tokenStartIndex = lineData.minColumn - 1; - let tokenEndIndex = 0; - let tokenFg: number; + tokenStartIndex = lineData.minColumn - 1; + tokenEndIndex = 0; for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { tokenEndIndex = tokens.getEndOffset(tokenIndex); if (tokenEndIndex <= tokenStartIndex) { @@ -640,7 +651,7 @@ class FullFileRenderStrategy implements IRenderStrategy< continue; } - const glyph = this._textureAtlas.getGlyph(chars, tokenFg); + glyph = this._textureAtlas.getGlyph(chars, tokenFg); screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); @@ -649,7 +660,7 @@ class FullFileRenderStrategy implements IRenderStrategy< wgslX = zeroToOneX * 2 - 1; wgslY = zeroToOneY * 2 - 1; - const cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x + xOffset)) * Constants.IndicesPerCell; + cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x + xOffset)) * Constants.IndicesPerCell; cellBuffer[cellIndex + 0] = wgslX; // x cellBuffer[cellIndex + 1] = -wgslY; // y cellBuffer[cellIndex + 2] = 0; @@ -662,8 +673,8 @@ class FullFileRenderStrategy implements IRenderStrategy< } // Clear to end of line - const fillStartIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (tokenEndIndex + xOffset)) * Constants.IndicesPerCell; - const fillEndIndex = (y * FullFileRenderStrategy._columnCount) * Constants.IndicesPerCell; + fillStartIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (tokenEndIndex + xOffset)) * Constants.IndicesPerCell; + fillEndIndex = (y * FullFileRenderStrategy._columnCount) * Constants.IndicesPerCell; cellBuffer.fill(0, fillStartIndex, fillEndIndex); upToDateLines.add(y); diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index ec68a482861..8ef168d2a0e 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -105,12 +105,12 @@ export class TextureAtlas extends Disposable { // TODO: Color, style etc. public getGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { - let glyph: ITextureAtlasGlyph | undefined = this._glyphMap.get(chars, tokenFg); - if (glyph) { - return glyph; - } + return this._glyphMap.get(chars, tokenFg) ?? this._createGlyph(chars, tokenFg); + } + + private _createGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg]); - glyph = this._allocator.allocate(rasterizedGlyph); + const glyph = this._allocator.allocate(rasterizedGlyph); this._glyphMap.set(chars, tokenFg, glyph); this._glyphInOrderSet.add(glyph); this.hasChanges = true; From 50ac6f5ec49fdacecb8eb99037bb315279003ccd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 13 Apr 2024 09:57:08 -0700 Subject: [PATCH 039/286] Add toggle to disable non-gpu rendering/layouts --- src/vs/editor/browser/view.ts | 7 +++++++ .../editor/browser/view/gpu/gpuViewLayer.ts | 2 ++ src/vs/editor/browser/view/viewOverlays.ts | 19 +++++++++++++++++++ .../browser/stickyScrollWidget.ts | 12 +++++++++--- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index fb7d9c09f24..aaef0a1deb3 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -15,6 +15,7 @@ import { PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouse import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { IVisibleRangeProvider, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; import { IContentWidget, IContentWidgetPosition, IEditorAriaOptions, IGlyphMarginWidget, IGlyphMarginWidgetPosition, IMouseTarget, IOverlayWidget, IOverlayWidgetPosition, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; +import { disableNonGpuRendering } from 'vs/editor/browser/view/gpu/gpuViewLayer'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; @@ -429,12 +430,18 @@ export class View extends ViewEventHandler { if (this._store.isDisposed) { throw new BugIndicatingError(); } + if (disableNonGpuRendering) { + return; + } return rendering.prepareRender(viewParts, ctx); }, render: (viewParts: ViewPart[], ctx: RestrictedRenderingContext) => { if (this._store.isDisposed) { throw new BugIndicatingError(); } + if (disableNonGpuRendering) { + return; + } return rendering.render(viewParts, ctx); } }); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 029ecf1c77a..59f8b367db8 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -14,6 +14,8 @@ import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +export const disableNonGpuRendering = true; + interface IRendererContext { rendLineNumberStart: number; lines: T[]; diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index b674d855fe4..fe331743db6 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -15,6 +15,7 @@ import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { disableNonGpuRendering } from 'vs/editor/browser/view/gpu/gpuViewLayer'; export class ViewOverlays extends ViewPart implements IVisibleLinesHost { @@ -123,6 +124,9 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost overlay.shouldRender()); for (let i = 0, len = toRender.length; i < len; i++) { @@ -133,6 +137,9 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost { this._linesDomNode.style.left = this._editor.getOption(EditorOption.stickyScroll).scrollWithEditor ? `-${this._editor.getScrollLeft()}px` : '0px'; @@ -135,7 +138,10 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { const isWidgetHeightZero = this._isWidgetHeightZero(_state); const state = isWidgetHeightZero ? undefined : _state; const rebuildFromLine = isWidgetHeightZero ? 0 : this._findLineToRebuildWidgetFrom(_state, _rebuildFromLine); - this._renderRootNode(state, foldingModel, rebuildFromLine); + + if (!disableNonGpuRendering) { + this._renderRootNode(state, foldingModel, rebuildFromLine); + } this._previousState = _state; } From a821ce103292915a897d01a99224e574e7a8d37e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 May 2024 19:21:36 -0700 Subject: [PATCH 040/286] Reduce texture size --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 11 ++++++----- src/vs/editor/browser/view/gpu/textureAtlas.ts | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 59f8b367db8..e9b9d9bcd06 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -112,13 +112,14 @@ export class GpuViewLayerRenderer { // Create texture atlas if (!GpuViewLayerRenderer._textureAtlas) { - GpuViewLayerRenderer._textureAtlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, this._device.limits.maxTextureDimension2D); + const pageSize = 1024; // this._device.limits.maxTextureDimension2D; + GpuViewLayerRenderer._textureAtlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, pageSize, this._device.limits.maxTextureDimension2D); GpuViewLayerRenderer._testCanvas = document.createElement('canvas'); - GpuViewLayerRenderer._testCanvas.width = this._device.limits.maxTextureDimension2D; - GpuViewLayerRenderer._testCanvas.height = this._device.limits.maxTextureDimension2D; - GpuViewLayerRenderer._testCanvas.style.width = `${this._device.limits.maxTextureDimension2D / getActiveWindow().devicePixelRatio}px`; - GpuViewLayerRenderer._testCanvas.style.height = `${this._device.limits.maxTextureDimension2D / getActiveWindow().devicePixelRatio}px`; + GpuViewLayerRenderer._testCanvas.width = pageSize; + GpuViewLayerRenderer._testCanvas.height = pageSize; + GpuViewLayerRenderer._testCanvas.style.width = `${GpuViewLayerRenderer._testCanvas.width / getActiveWindow().devicePixelRatio}px`; + GpuViewLayerRenderer._testCanvas.style.height = `${GpuViewLayerRenderer._testCanvas.height / getActiveWindow().devicePixelRatio}px`; GpuViewLayerRenderer._testCanvas.style.position = 'absolute'; GpuViewLayerRenderer._testCanvas.style.top = '2px'; GpuViewLayerRenderer._testCanvas.style.left = '2px'; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 8ef168d2a0e..9323158ca29 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -72,12 +72,13 @@ export class TextureAtlas extends Disposable { // TODO: Should pull in the font size from config instead of random dom node constructor( parentDomNode: HTMLElement, + pageSize: number, maxTextureSize: number, @IThemeService private readonly _themeService: IThemeService ) { super(); - this._canvas = new OffscreenCanvas(maxTextureSize, maxTextureSize); + this._canvas = new OffscreenCanvas(pageSize, pageSize); this._ctx = ensureNonNullable(this._canvas.getContext('2d', { willReadFrequently: true })); @@ -191,6 +192,7 @@ class GlyphRasterizer extends Disposable { return result; } + // TODO: Does this even need to happen when measure text is used? // TODO: Pass back origin offset private _findGlyphBoundingBox(imageData: ImageData): IBoundingBox { // TODO: Hot path: Reuse object From 88e09b4c93a5ffb067c53bfe5b16a87d5fe2094a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 May 2024 19:23:58 -0700 Subject: [PATCH 041/286] Simplify parseInt --- src/vs/editor/browser/view/gpu/textureAtlas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 9323158ca29..7509dd63ba0 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -85,7 +85,7 @@ export class TextureAtlas extends Disposable { const activeWindow = getActiveWindow(); const style = activeWindow.getComputedStyle(parentDomNode); - const fontSize = Math.ceil(parseInt(style.fontSize.replace('px', '')) * activeWindow.devicePixelRatio); + const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); this._ctx.font = `${fontSize}px ${style.fontFamily}`; this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { From 0d70158daadb4322cb837ddfdbc0f7bad481ee58 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 8 May 2024 22:14:42 -0700 Subject: [PATCH 042/286] Split out TextureAtlasPage --- .../browser/view/gpu/glyphRasterizer.ts | 188 +++++++++++++++++ .../editor/browser/view/gpu/textureAtlas.ts | 196 ++---------------- .../browser/view/gpu/textureAtlasAllocator.ts | 3 +- .../browser/view/gpu/textureAtlasPage.ts | 132 ++++++++++++ 4 files changed, 342 insertions(+), 177 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/glyphRasterizer.ts create mode 100644 src/vs/editor/browser/view/gpu/textureAtlasPage.ts diff --git a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts new file mode 100644 index 00000000000..76cc43f1638 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; + +export class GlyphRasterizer extends Disposable { + private _canvas: OffscreenCanvas; + // A temporary context that glyphs are drawn to before being transfered to the atlas. + private _ctx: OffscreenCanvasRenderingContext2D; + + constructor(private readonly _fontSize: number, fontFamily: string) { + super(); + + this._canvas = new OffscreenCanvas(this._fontSize * 3, this._fontSize * 3); + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); + this._ctx.font = `${this._fontSize}px ${fontFamily}`; + this._ctx.textBaseline = 'top'; + this._ctx.fillStyle = '#FFFFFF'; + } + + // TODO: Support drawing multiple fonts and sizes + // TODO: Should pull in the font size from config instead of random dom node + public rasterizeGlyph(chars: string, fg: string): IRasterizedGlyph { + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + + // TODO: Draw in middle using alphabetical baseline + const originX = this._fontSize; + const originY = this._fontSize; + this._ctx.fillStyle = fg; + const textMetrics = this._ctx.measureText(chars); + this._ctx.fillText(chars, originX, originY); + + const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + // TODO: Hot path: Reuse object + const boundingBox = this._findGlyphBoundingBox(imageData); + const offset = { + x: textMetrics.actualBoundingBoxLeft, + y: textMetrics.actualBoundingBoxAscent + }; + const size = { + w: textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft, + y: textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent, + wInt: Math.ceil(textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft), + yInt: Math.ceil(textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent), + }; + + console.log(`${chars}_${fg}`, textMetrics, boundingBox, originX, originY, { width: boundingBox.right - boundingBox.left, height: boundingBox.bottom - boundingBox.top }); + console.log('new', offset, size); + const result: IRasterizedGlyph = { + source: this._canvas, + boundingBox, + originOffset: { + x: boundingBox.left - originX, + y: boundingBox.top - originY + } + }; + const result2: IRasterizedGlyph = { + source: this._canvas, + boundingBox: { + left: Math.floor(originX - textMetrics.actualBoundingBoxLeft), + right: Math.ceil(originX + textMetrics.actualBoundingBoxRight), + top: Math.floor(originY - textMetrics.actualBoundingBoxAscent), + bottom: Math.ceil(originY + textMetrics.actualBoundingBoxDescent), + }, + originOffset: { + x: Math.floor(boundingBox.left - originX), + y: Math.floor(boundingBox.top - originY) + } + }; + + // DEBUG: Show image data in console + // (console as any).image(imageData); + + // TODO: Verify result 1 and 2 are the same + + // if (result2.boundingBox.left > result.boundingBox.left) { + // debugger; + // } + // if (result2.boundingBox.top > result.boundingBox.top) { + // debugger; + // } + // if (result2.boundingBox.right < result.boundingBox.right) { + // debugger; + // } + // if (result2.boundingBox.bottom < result.boundingBox.bottom) { + // debugger; + // } + if (JSON.stringify(result2.originOffset) !== JSON.stringify(result.originOffset)) { + debugger; + } + + return result2; + } + + // TODO: Does this even need to happen when measure text is used? + // TODO: Pass back origin offset + private _findGlyphBoundingBox(imageData: ImageData): IBoundingBox { + + // TODO: Hot path: Reuse object + const boundingBox = { + left: 0, + top: 0, + right: 0, + bottom: 0 + }; + // TODO: This could be optimized to be aware of the font size padding on all sides + const height = this._canvas.height; + const width = this._canvas.width; + let found = false; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.top = y; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.left = 0; + found = false; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.left = x; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.right = width; + found = false; + for (let x = width - 1; x >= boundingBox.left; x--) { + for (let y = 0; y < height; y++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.right = x; + found = true; + break; + } + } + if (found) { + break; + } + } + boundingBox.bottom = boundingBox.top; + found = false; + for (let y = height - 1; y >= 0; y--) { + for (let x = 0; x < width; x++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + boundingBox.bottom = y; + found = true; + break; + } + } + if (found) { + break; + } + } + return boundingBox; + } +} + +export interface IBoundingBox { + left: number; + top: number; + right: number; + bottom: number; +} + +export interface IRasterizedGlyph { + source: CanvasImageSource; + boundingBox: IBoundingBox; + originOffset: { x: number; y: number }; +} diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 7509dd63ba0..97ce622da72 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -5,11 +5,11 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; -import { ITextureAtlasAllocator, TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; +import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/textureAtlasPage'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; // DEBUG: This helper can be used to draw image data to the console, it's commented out as we don't @@ -46,47 +46,42 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; // }; export class TextureAtlas extends Disposable { - private readonly _canvas: OffscreenCanvas; - private readonly _ctx: OffscreenCanvasRenderingContext2D; - - private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); - // HACK: This is an ordered set of glyphs to be passed to the GPU since currently the shader - // uses the index of the glyph. This should be improved to derive from _glyphMap - private readonly _glyphInOrderSet: Set = new Set(); public get glyphs(): IterableIterator { - return this._glyphInOrderSet.values(); + return this._page.glyphs; } private readonly _glyphRasterizer: GlyphRasterizer; - private readonly _allocator: ITextureAtlasAllocator; private _colorMap!: string[]; private _warmUpTask?: IdleTaskQueue; public get source(): OffscreenCanvas { - return this._canvas; + return this._page.source; } - public hasChanges = false; + public get hasChanges(): boolean { + return this._page.hasChanges; + } + public set hasChanges(value: boolean) { + this._page.hasChanges = value; + } + + private readonly _page: TextureAtlasPage; // TODO: Should pull in the font size from config instead of random dom node constructor( parentDomNode: HTMLElement, pageSize: number, maxTextureSize: number, - @IThemeService private readonly _themeService: IThemeService + @IThemeService private readonly _themeService: IThemeService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); - this._canvas = new OffscreenCanvas(pageSize, pageSize); - this._ctx = ensureNonNullable(this._canvas.getContext('2d', { - willReadFrequently: true - })); - const activeWindow = getActiveWindow(); const style = activeWindow.getComputedStyle(parentDomNode); const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); - this._ctx.font = `${fontSize}px ${style.fontFamily}`; + // this._ctx.font = `${fontSize}px ${style.fontFamily}`; this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { // TODO: Clear entire atlas on theme change @@ -95,37 +90,14 @@ export class TextureAtlas extends Disposable { })); this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); - this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); + // this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); - // Reduce impact of a memory leak if this object is not released - this._register(toDisposable(() => { - this._canvas.width = 1; - this._canvas.height = 1; - })); + this._page = this._register(this._instantiationService.createInstance(TextureAtlasPage, parentDomNode, pageSize, maxTextureSize, this._glyphRasterizer)); } // TODO: Color, style etc. public getGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { - return this._glyphMap.get(chars, tokenFg) ?? this._createGlyph(chars, tokenFg); - } - - private _createGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { - const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg]); - const glyph = this._allocator.allocate(rasterizedGlyph); - this._glyphMap.set(chars, tokenFg, glyph); - this._glyphInOrderSet.add(glyph); - this.hasChanges = true; - - if (!this._warmUpTask) { - console.debug('New glyph', { - chars, - fg: this._colorMap[tokenFg], - rasterizedGlyph, - glyph - }); - } - - return glyph; + return this._page.getGlyph(chars, tokenFg); } /** @@ -146,128 +118,6 @@ export class TextureAtlas extends Disposable { } } -class GlyphRasterizer extends Disposable { - private _canvas: OffscreenCanvas; - // A temporary context that glyphs are drawn to before being transfered to the atlas. - private _ctx: OffscreenCanvasRenderingContext2D; - - constructor(private readonly _fontSize: number, fontFamily: string) { - super(); - - this._canvas = new OffscreenCanvas(this._fontSize * 3, this._fontSize * 3); - this._ctx = ensureNonNullable(this._canvas.getContext('2d', { - willReadFrequently: true - })); - this._ctx.font = `${this._fontSize}px ${fontFamily}`; - this._ctx.textBaseline = 'top'; - this._ctx.fillStyle = '#FFFFFF'; - } - - // TODO: Support drawing multiple fonts and sizes - // TODO: Should pull in the font size from config instead of random dom node - public rasterizeGlyph(chars: string, fg: string): IRasterizedGlyph { - this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); - - // TODO: Draw in middle using alphabetical baseline - const originX = this._fontSize; - const originY = this._fontSize; - this._ctx.fillStyle = fg; - this._ctx.fillText(chars, originX, originY); - - const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - // TODO: Hot path: Reuse object - const boundingBox = this._findGlyphBoundingBox(imageData); - const result: IRasterizedGlyph = { - source: this._canvas, - boundingBox, - originOffset: { - x: boundingBox.left - originX, - y: boundingBox.top - originY - } - }; - - // DEBUG: Show image data in console - // (console as any).image(imageData); - - return result; - } - - // TODO: Does this even need to happen when measure text is used? - // TODO: Pass back origin offset - private _findGlyphBoundingBox(imageData: ImageData): IBoundingBox { - // TODO: Hot path: Reuse object - const boundingBox = { - left: 0, - top: 0, - right: 0, - bottom: 0 - }; - // TODO: This could be optimized to be aware of the font size padding on all sides - const height = this._canvas.height; - const width = this._canvas.width; - let found = false; - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const alphaOffset = y * width * 4 + x * 4 + 3; - if (imageData.data[alphaOffset] !== 0) { - boundingBox.top = y; - found = true; - break; - } - } - if (found) { - break; - } - } - boundingBox.left = 0; - found = false; - for (let x = 0; x < width; x++) { - for (let y = 0; y < height; y++) { - const alphaOffset = y * width * 4 + x * 4 + 3; - if (imageData.data[alphaOffset] !== 0) { - boundingBox.left = x; - found = true; - break; - } - } - if (found) { - break; - } - } - boundingBox.right = width; - found = false; - for (let x = width - 1; x >= boundingBox.left; x--) { - for (let y = 0; y < height; y++) { - const alphaOffset = y * width * 4 + x * 4 + 3; - if (imageData.data[alphaOffset] !== 0) { - boundingBox.right = x; - found = true; - break; - } - } - if (found) { - break; - } - } - boundingBox.bottom = boundingBox.top; - found = false; - for (let y = height - 1; y >= 0; y--) { - for (let x = 0; x < width; x++) { - const alphaOffset = y * width * 4 + x * 4 + 3; - if (imageData.data[alphaOffset] !== 0) { - boundingBox.bottom = y; - found = true; - break; - } - } - if (found) { - break; - } - } - return boundingBox; - } -} - export interface ITextureAtlasGlyph { index: number; x: number; @@ -284,9 +134,3 @@ export interface IBoundingBox { right: number; bottom: number; } - -export interface IRasterizedGlyph { - source: CanvasImageSource; - boundingBox: IBoundingBox; - originOffset: { x: number; y: number }; -} diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index d4091d19320..e013c52bf5f 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IRasterizedGlyph, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/glyphRasterizer'; +import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; export interface ITextureAtlasAllocator { allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph; diff --git a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts new file mode 100644 index 00000000000..f15d4b9c769 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from 'vs/base/browser/dom'; +import { Event } from 'vs/base/common/event'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; +import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; +import { ITextureAtlasAllocator, TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export class TextureAtlasPage extends Disposable { + + private readonly _canvas: OffscreenCanvas; + private readonly _ctx: OffscreenCanvasRenderingContext2D; + + private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); + // HACK: This is an ordered set of glyphs to be passed to the GPU since currently the shader + // uses the index of the glyph. This should be improved to derive from _glyphMap + private readonly _glyphInOrderSet: Set = new Set(); + public get glyphs(): IterableIterator { + return this._glyphInOrderSet.values(); + } + + private readonly _allocator: ITextureAtlasAllocator; + + private _colorMap!: string[]; + private _warmUpTask?: IdleTaskQueue; + + public get source(): OffscreenCanvas { + return this._canvas; + } + + public hasChanges = false; + + + // TODO: Should pull in the font size from config instead of random dom node + constructor( + parentDomNode: HTMLElement, + pageSize: number, + maxTextureSize: number, + private readonly _glyphRasterizer: GlyphRasterizer, + @IThemeService private readonly _themeService: IThemeService + ) { + super(); + + this._canvas = new OffscreenCanvas(pageSize, pageSize); + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); + + const activeWindow = getActiveWindow(); + const style = activeWindow.getComputedStyle(parentDomNode); + const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); + this._ctx.font = `${fontSize}px ${style.fontFamily}`; + + this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { + // TODO: Clear entire atlas on theme change + this._colorMap = this._themeService.getColorTheme().tokenColorMap; + this._warmUpAtlas(); + })); + + this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); + + // Reduce impact of a memory leak if this object is not released + this._register(toDisposable(() => { + this._canvas.width = 1; + this._canvas.height = 1; + })); + } + + // TODO: Color, style etc. + public getGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { + return this._glyphMap.get(chars, tokenFg) ?? this._createGlyph(chars, tokenFg); + } + + private _createGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { + const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg]); + const glyph = this._allocator.allocate(rasterizedGlyph); + this._glyphMap.set(chars, tokenFg, glyph); + this._glyphInOrderSet.add(glyph); + this.hasChanges = true; + + if (!this._warmUpTask) { + console.debug('New glyph', { + chars, + fg: this._colorMap[tokenFg], + rasterizedGlyph, + glyph + }); + } + + return glyph; + } + + /** + * Warms up the atlas by rasterizing all printable ASCII characters for each token color. This + * is distrubuted over multiple idle callbacks to avoid blocking the main thread. + */ + private _warmUpAtlas(): void { + // TODO: Clean up on dispose + this._warmUpTask?.clear(); + this._warmUpTask = new IdleTaskQueue(); + for (const tokenFg of this._colorMap.keys()) { + this._warmUpTask.enqueue(() => { + for (let code = 33; code <= 126; code++) { + this.getGlyph(String.fromCharCode(code), tokenFg); + } + }); + } + } +} +export interface ITextureAtlasGlyph { + index: number; + x: number; + y: number; + w: number; + h: number; + originOffsetX: number; + originOffsetY: number; +} + +export interface IBoundingBox { + left: number; + top: number; + right: number; + bottom: number; +} From 216e138403e3ca56dd23c7bb907a9fe8dd41fb7c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 9 May 2024 00:31:03 -0700 Subject: [PATCH 043/286] Remove warm up from page --- .../browser/view/gpu/textureAtlasPage.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts index f15d4b9c769..f3195dc4804 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts @@ -61,7 +61,6 @@ export class TextureAtlasPage extends Disposable { this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { // TODO: Clear entire atlas on theme change this._colorMap = this._themeService.getColorTheme().tokenColorMap; - this._warmUpAtlas(); })); this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); @@ -96,24 +95,8 @@ export class TextureAtlasPage extends Disposable { return glyph; } - - /** - * Warms up the atlas by rasterizing all printable ASCII characters for each token color. This - * is distrubuted over multiple idle callbacks to avoid blocking the main thread. - */ - private _warmUpAtlas(): void { - // TODO: Clean up on dispose - this._warmUpTask?.clear(); - this._warmUpTask = new IdleTaskQueue(); - for (const tokenFg of this._colorMap.keys()) { - this._warmUpTask.enqueue(() => { - for (let code = 33; code <= 126; code++) { - this.getGlyph(String.fromCharCode(code), tokenFg); - } - }); - } - } } + export interface ITextureAtlasGlyph { index: number; x: number; From 631ff57653e8db39f3c831189c5ca34000a8ce67 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 11:02:49 +0900 Subject: [PATCH 044/286] Atlas actual and usage files --- .../browser/view/gpu/glyphRasterizer.ts | 7 ++- .../editor/browser/view/gpu/gpuViewLayer.ts | 51 +++++++++---------- .../editor/browser/view/gpu/textureAtlas.ts | 15 ++++++ .../browser/view/gpu/textureAtlasPage.ts | 11 ++-- 4 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts index 76cc43f1638..73637e24146 100644 --- a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts @@ -11,7 +11,10 @@ export class GlyphRasterizer extends Disposable { // A temporary context that glyphs are drawn to before being transfered to the atlas. private _ctx: OffscreenCanvasRenderingContext2D; - constructor(private readonly _fontSize: number, fontFamily: string) { + constructor( + private readonly _fontSize: number, + fontFamily: string, + ) { super(); this._canvas = new OffscreenCanvas(this._fontSize * 3, this._fontSize * 3); @@ -94,7 +97,7 @@ export class GlyphRasterizer extends Disposable { debugger; } - return result2; + return result; } // TODO: Does this even need to happen when measure text is used? diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index e9b9d9bcd06..eeb7227f00c 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -3,16 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveDocument, getActiveWindow } from 'vs/base/browser/dom'; +import { getActiveWindow } from 'vs/base/browser/dom'; +import { VSBuffer } from 'vs/base/common/buffer'; import { debounce } from 'vs/base/common/decorators'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { URI } from 'vs/base/common/uri'; import { TextureAtlas, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; +import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export const disableNonGpuRendering = true; @@ -69,19 +72,15 @@ export class GpuViewLayerRenderer { private _initialized = false; - - - private static _testCanvas: HTMLCanvasElement; - private static _testCtx: CanvasRenderingContext2D; - private _renderStrategy!: IRenderStrategy; - constructor( domNode: HTMLCanvasElement, host: IVisibleLinesHost, viewportData: ViewportData, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IFileService private readonly _fileService: IFileService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, ) { this.domNode = domNode; this.host = host; @@ -114,19 +113,6 @@ export class GpuViewLayerRenderer { if (!GpuViewLayerRenderer._textureAtlas) { const pageSize = 1024; // this._device.limits.maxTextureDimension2D; GpuViewLayerRenderer._textureAtlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, pageSize, this._device.limits.maxTextureDimension2D); - - GpuViewLayerRenderer._testCanvas = document.createElement('canvas'); - GpuViewLayerRenderer._testCanvas.width = pageSize; - GpuViewLayerRenderer._testCanvas.height = pageSize; - GpuViewLayerRenderer._testCanvas.style.width = `${GpuViewLayerRenderer._testCanvas.width / getActiveWindow().devicePixelRatio}px`; - GpuViewLayerRenderer._testCanvas.style.height = `${GpuViewLayerRenderer._testCanvas.height / getActiveWindow().devicePixelRatio}px`; - GpuViewLayerRenderer._testCanvas.style.position = 'absolute'; - GpuViewLayerRenderer._testCanvas.style.top = '2px'; - GpuViewLayerRenderer._testCanvas.style.left = '2px'; - GpuViewLayerRenderer._testCanvas.style.zIndex = '10000'; - GpuViewLayerRenderer._testCanvas.style.pointerEvents = 'none'; - GpuViewLayerRenderer._testCtx = ensureNonNullable(GpuViewLayerRenderer._testCanvas.getContext('2d')); - getActiveDocument().body.appendChild(GpuViewLayerRenderer._testCanvas); } const textureAtlas = GpuViewLayerRenderer._textureAtlas; @@ -333,14 +319,25 @@ export class GpuViewLayerRenderer { { width: GpuViewLayerRenderer._textureAtlas.source.width, height: GpuViewLayerRenderer._textureAtlas.source.height }, ); - - GpuViewLayerRenderer._drawToTextureAtlas(); + GpuViewLayerRenderer._drawToTextureAtlas(this._fileService, this._workspaceContextService); } @debounce(500) - private static _drawToTextureAtlas() { - GpuViewLayerRenderer._testCtx.clearRect(0, 0, GpuViewLayerRenderer._textureAtlas.source.width, GpuViewLayerRenderer._textureAtlas.source.height); - GpuViewLayerRenderer._testCtx.drawImage(GpuViewLayerRenderer._textureAtlas.source, 0, 0); + private static async _drawToTextureAtlas(fileService: IFileService, workspaceContextService: IWorkspaceContextService) { + const blob = await GpuViewLayerRenderer._textureAtlas.source.convertToBlob(); + const folders = workspaceContextService.getWorkspace().folders; + if (folders.length > 0) { + await Promise.all([ + fileService.writeFile( + URI.joinPath(folders[0].uri, 'atlas_scratch_actual.png'), + VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer())) + ), + fileService.writeFile( + URI.joinPath(folders[0].uri, 'atlas_scratch_usage.png'), + VSBuffer.wrap(new Uint8Array(await ((await GpuViewLayerRenderer._textureAtlas.getUsagePreview()).arrayBuffer()))) + ) + ]); + } } public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 97ce622da72..9afef14c242 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -7,6 +7,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/textureAtlasPage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -100,6 +101,20 @@ export class TextureAtlas extends Disposable { return this._page.getGlyph(chars, tokenFg); } + public getUsagePreview(): Promise { + const w = this._page.source.width; + const h = this._page.source.height; + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.fillStyle = '#808080'; + ctx.fillRect(0, 0, w, h); + ctx.fillStyle = '#4040FF'; + for (const g of this.glyphs) { + ctx.fillRect(g.x, g.y, g.w, g.h); + } + return canvas.convertToBlob(); + } + /** * Warms up the atlas by rasterizing all printable ASCII characters for each token color. This * is distrubuted over multiple idle callbacks to avoid blocking the main thread. diff --git a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts index f3195dc4804..aba1af5464b 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts @@ -9,8 +9,8 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; -import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { ITextureAtlasAllocator, TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TextureAtlasPage extends Disposable { @@ -29,7 +29,6 @@ export class TextureAtlasPage extends Disposable { private readonly _allocator: ITextureAtlasAllocator; private _colorMap!: string[]; - private _warmUpTask?: IdleTaskQueue; public get source(): OffscreenCanvas { return this._canvas; @@ -37,14 +36,14 @@ export class TextureAtlasPage extends Disposable { public hasChanges = false; - // TODO: Should pull in the font size from config instead of random dom node constructor( parentDomNode: HTMLElement, pageSize: number, maxTextureSize: number, private readonly _glyphRasterizer: GlyphRasterizer, - @IThemeService private readonly _themeService: IThemeService + @ILogService private readonly _logService: ILogService, + @IThemeService private readonly _themeService: IThemeService, ) { super(); @@ -84,8 +83,8 @@ export class TextureAtlasPage extends Disposable { this._glyphInOrderSet.add(glyph); this.hasChanges = true; - if (!this._warmUpTask) { - console.debug('New glyph', { + if (this._logService.getLevel() === LogLevel.Trace) { + this._logService.trace('New glyph', { chars, fg: this._colorMap[tokenFg], rasterizedGlyph, From 73d1a85e3eab7e90a9deaa4f77260fdadb683e7c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 11:13:15 +0900 Subject: [PATCH 045/286] Display wasted space in usage preview --- src/vs/editor/browser/view/gpu/textureAtlas.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 9afef14c242..7687011749d 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -108,9 +108,25 @@ export class TextureAtlas extends Disposable { const ctx = ensureNonNullable(canvas.getContext('2d')); ctx.fillStyle = '#808080'; ctx.fillRect(0, 0, w, h); - ctx.fillStyle = '#4040FF'; + const rowHeight: Map = new Map(); // y -> h + const rowWidth: Map = new Map(); // y -> w + let lastY: number = 0; for (const g of this.glyphs) { + rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); + rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); + lastY = Math.max(lastY, g.y); + } + for (const g of this.glyphs) { + ctx.fillStyle = '#4040FF'; ctx.fillRect(g.x, g.y, g.w, g.h); + ctx.fillStyle = '#FF0000'; + ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); + } + for (const [rowY, rowW] of rowWidth.entries()) { + if (rowY !== lastY) { + ctx.fillStyle = '#FF0000'; + ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); + } } return canvas.convertToBlob(); } From 4e046518cb255f41b8f1a6889a5e99ad0b1397ce Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 11:20:48 +0900 Subject: [PATCH 046/286] Move usage preview into allocator --- .../editor/browser/view/gpu/textureAtlas.ts | 30 +------------ .../browser/view/gpu/textureAtlasAllocator.ts | 43 ++++++++++++++++++- .../browser/view/gpu/textureAtlasPage.ts | 6 ++- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 7687011749d..13fd80ee962 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -7,7 +7,6 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/textureAtlasPage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -82,7 +81,6 @@ export class TextureAtlas extends Disposable { const activeWindow = getActiveWindow(); const style = activeWindow.getComputedStyle(parentDomNode); const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); - // this._ctx.font = `${fontSize}px ${style.fontFamily}`; this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { // TODO: Clear entire atlas on theme change @@ -102,33 +100,7 @@ export class TextureAtlas extends Disposable { } public getUsagePreview(): Promise { - const w = this._page.source.width; - const h = this._page.source.height; - const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - ctx.fillStyle = '#808080'; - ctx.fillRect(0, 0, w, h); - const rowHeight: Map = new Map(); // y -> h - const rowWidth: Map = new Map(); // y -> w - let lastY: number = 0; - for (const g of this.glyphs) { - rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); - rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); - lastY = Math.max(lastY, g.y); - } - for (const g of this.glyphs) { - ctx.fillStyle = '#4040FF'; - ctx.fillRect(g.x, g.y, g.w, g.h); - ctx.fillStyle = '#FF0000'; - ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); - } - for (const [rowY, rowW] of rowWidth.entries()) { - if (rowY !== lastY) { - ctx.fillStyle = '#FF0000'; - ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); - } - } - return canvas.convertToBlob(); + return this._page.getUsagePreview(); } /** diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index e013c52bf5f..bb7fea66f20 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -4,10 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/glyphRasterizer'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; export interface ITextureAtlasAllocator { - allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph; + readonly glyphMap: TwoKeyMap; + allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph; + getUsagePreview(): Promise; } export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { @@ -16,9 +20,12 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { y: 0, h: 0 }; + // TODO: Allow for multiple active rows // public readonly fixedRows: ICharAtlasActiveRow[] = []; + readonly glyphMap: TwoKeyMap = new TwoKeyMap(); + private _nextIndex = 0; constructor( @@ -27,7 +34,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { ) { } - public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { + public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { // Finalize row if it doesn't fix if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left > this._canvas.width - this._currentRow.x) { this._currentRow.x = 0; @@ -70,8 +77,40 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { this._currentRow.x += glyphWidth; this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); + // Set the glyph + this.glyphMap.set(chars, tokenFg, glyph); + return glyph; } + + public getUsagePreview(): Promise { + // TODO: This is specific to the simple shelf allocator + const w = this._canvas.width; + const h = this._canvas.height; + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.fillStyle = '#808080'; + ctx.fillRect(0, 0, w, h); + const rowHeight: Map = new Map(); // y -> h + const rowWidth: Map = new Map(); // y -> w + for (const g of this.glyphMap.values()) { + rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); + rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); + } + for (const g of this.glyphMap.values()) { + ctx.fillStyle = '#4040FF'; + ctx.fillRect(g.x, g.y, g.w, g.h); + ctx.fillStyle = '#FF0000'; + ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); + } + for (const [rowY, rowW] of rowWidth.entries()) { + if (rowY !== this._currentRow.y) { + ctx.fillStyle = '#FF0000'; + ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); + } + } + return canvas.convertToBlob(); + } } interface ITextureAtlasShelf { diff --git a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts index aba1af5464b..57f154a6001 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts @@ -78,7 +78,7 @@ export class TextureAtlasPage extends Disposable { private _createGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg]); - const glyph = this._allocator.allocate(rasterizedGlyph); + const glyph = this._allocator.allocate(chars, tokenFg, rasterizedGlyph); this._glyphMap.set(chars, tokenFg, glyph); this._glyphInOrderSet.add(glyph); this.hasChanges = true; @@ -94,6 +94,10 @@ export class TextureAtlasPage extends Disposable { return glyph; } + + getUsagePreview(): Promise { + return this._allocator.getUsagePreview(); + } } export interface ITextureAtlasGlyph { From 3338d40d83ee56c8c6daa98ca739f828dd9c865d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 11:28:34 +0900 Subject: [PATCH 047/286] Log atlas stats to console --- .../browser/view/gpu/textureAtlasAllocator.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index bb7fea66f20..e122e5304f0 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -91,6 +91,11 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { const ctx = ensureNonNullable(canvas.getContext('2d')); ctx.fillStyle = '#808080'; ctx.fillRect(0, 0, w, h); + + let usedPixels = 0; + let wastedPixels = 0; + const totalPixels = w * h; + const rowHeight: Map = new Map(); // y -> h const rowWidth: Map = new Map(); // y -> w for (const g of this.glyphMap.values()) { @@ -98,6 +103,8 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); } for (const g of this.glyphMap.values()) { + usedPixels += g.w * g.h; + wastedPixels += g.w * (rowHeight.get(g.y)! - g.h); ctx.fillStyle = '#4040FF'; ctx.fillRect(g.x, g.y, g.w, g.h); ctx.fillStyle = '#FF0000'; @@ -107,8 +114,16 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { if (rowY !== this._currentRow.y) { ctx.fillStyle = '#FF0000'; ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); + wastedPixels += (w - rowW) * rowHeight.get(rowY)!; } } + console.log([ + `Texture atlas stats:`, + ` Total: ${totalPixels}`, + ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, + ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, + `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, + ].join('\n')); return canvas.convertToBlob(); } } From c8f4899ce94b9e0ccfc30b38a10f7fcd0b472d69 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 12:10:03 +0900 Subject: [PATCH 048/286] Slab allocator with number experiments --- .../browser/view/gpu/textureAtlasAllocator.ts | 131 ++++++++++++++++++ .../browser/view/gpu/textureAtlasPage.ts | 5 +- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index e122e5304f0..7ace5305ccf 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -14,6 +14,8 @@ export interface ITextureAtlasAllocator { getUsagePreview(): Promise; } +// #region Shelf allocator + export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { private _currentRow: ITextureAtlasShelf = { x: 0, @@ -133,3 +135,132 @@ interface ITextureAtlasShelf { y: number; h: number; } + +// #endregion + +// #region Slab allocator + +export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { + // TODO: Is there a better way to index slabs other than an unsorted list? + private _slabs: ITextureAtlasSlab[] = []; + private _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); + + readonly glyphMap: TwoKeyMap = new TwoKeyMap(); + + private _nextIndex = 0; + + constructor( + private readonly _canvas: OffscreenCanvas, + private readonly _ctx: OffscreenCanvasRenderingContext2D, + ) { + } + + public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { + // Find ideal slab, creating it if there is none suitable + + // Slabs are sized: 1x1, 1x2, 1x4, 1x8, ... + // 2x1, 2x2, 2x4, 2x8, ... + // 4x1, 4x2, 4x4, 4x8, ... + // ... + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; + const desiredSlabSize = { + // TODO: This can probably be optimized + w: 1 << Math.ceil(Math.sqrt(glyphWidth)), + h: 1 << Math.ceil(Math.sqrt(glyphHeight)), + + // w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth, + // h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight, + + // w: glyphWidth, + // h: glyphHeight, + }; + + const slabW = 256; // this._canvas.width / 8; + const slabH = 256; // this._canvas.height / 8; + const slabsPerRow = Math.floor(this._canvas.width / slabW); + + let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h); + if (!slab) { + slab = { + x: Math.floor(this._slabs.length % slabsPerRow) * slabW, + y: Math.floor(this._slabs.length / slabsPerRow) * slabH, + entryW: desiredSlabSize.w, + entryH: desiredSlabSize.h, + count: 0 + }; + // console.log('new slab (full slab)', slab); + this._slabs.push(slab); + this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); + } + + // Create another slab if this one is full + const glyphsPerSlab = Math.floor(slabW / slab.entryW) * Math.floor(slabH / slab.entryH); + if (slab.count >= glyphsPerSlab) { + slab = { + x: Math.floor(this._slabs.length % slabsPerRow) * slabW, + y: Math.floor(this._slabs.length / slabsPerRow) * slabH, + entryW: desiredSlabSize.w, + entryH: desiredSlabSize.h, + count: 0 + }; + // console.log('new slab (full slab)', slab); + this._slabs.push(slab); + this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); + } + + // Draw glyph + // TODO: Prefer putImageData as it doesn't do blending or scaling + const glyphsPerRow = Math.floor(slabW / slab.entryW); + const dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW; + const dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH; + console.log('dx dy', dx, dy); + this._ctx.drawImage( + rasterizedGlyph.source, + // source + rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.top, + glyphWidth, + glyphHeight, + // destination + dx, + dy, + glyphWidth, + glyphHeight + ); + + // Create glyph object + const glyph: ITextureAtlasGlyph = { + index: this._nextIndex++, + x: dx, + y: dy, + w: glyphWidth, + h: glyphHeight, + originOffsetX: rasterizedGlyph.originOffset.x, + originOffsetY: rasterizedGlyph.originOffset.y + }; + + // Shift current row + slab.count++; + + // Set the glyph + this.glyphMap.set(chars, tokenFg, glyph); + + return glyph; + } + + public getUsagePreview(): Promise { + // TODO: Impl + return this._canvas.convertToBlob(); + } +} + +interface ITextureAtlasSlab { + x: number; + y: number; + entryH: number; + entryW: number; + count: number; +} + +// #endregion diff --git a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts index 57f154a6001..ac684b36ef8 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasPage.ts @@ -9,7 +9,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; -import { ITextureAtlasAllocator, TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; +import { ITextureAtlasAllocator, TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -62,7 +62,8 @@ export class TextureAtlasPage extends Disposable { this._colorMap = this._themeService.getColorTheme().tokenColorMap; })); - this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); + // this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); + this._allocator = new TextureAtlasSlabAllocator(this._canvas, this._ctx); // Reduce impact of a memory leak if this object is not released this._register(toDisposable(() => { From bdfac3347798e0c8ccb9d48efcf39dfbc6327688 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 12:34:51 +0900 Subject: [PATCH 049/286] Slab size iteration --- .../browser/view/gpu/textureAtlasAllocator.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index 7ace5305ccf..e05a89b4a08 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getActiveWindow } from 'vs/base/browser/dom'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/glyphRasterizer'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; @@ -164,20 +165,34 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // ... const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; - const desiredSlabSize = { - // TODO: This can probably be optimized - w: 1 << Math.ceil(Math.sqrt(glyphWidth)), - h: 1 << Math.ceil(Math.sqrt(glyphHeight)), + const dpr = getActiveWindow().devicePixelRatio; + // Round slab glyph dimensions to the nearest x pixels, where x scaled with device pixel ratio + const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5)); + const desiredSlabSize = { + // Nearest square number + // TODO: This can probably be optimized + // w: 1 << Math.ceil(Math.sqrt(glyphWidth)), + // h: 1 << Math.ceil(Math.sqrt(glyphHeight)), + + // Nearest x px + w: Math.ceil(glyphWidth / nearestXPixels) * nearestXPixels, + h: Math.ceil(glyphHeight / nearestXPixels) * nearestXPixels, + + // Round odd numbers up // w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth, // h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight, + // Exact number only // w: glyphWidth, // h: glyphHeight, }; - const slabW = 256; // this._canvas.width / 8; - const slabH = 256; // this._canvas.height / 8; + // TODO: Keeping track of the slab's x and y could allow variable sized slabs and less waste + // TODO: The unused rectangle at the bottom and side of a slab could house micro glyphs like `.` + + const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); // this._canvas.width / 8; + const slabH = slabW; // this._canvas.height / 8; const slabsPerRow = Math.floor(this._canvas.width / slabW); let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h); From 348a0584357264e536e080b12606e5a21ed6c501 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 13:11:36 +0900 Subject: [PATCH 050/286] Visualize slab allocator --- .../browser/view/gpu/textureAtlasAllocator.ts | 128 +++++++++++++++--- 1 file changed, 110 insertions(+), 18 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index e05a89b4a08..fee5520e90f 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -146,6 +146,8 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private _slabs: ITextureAtlasSlab[] = []; private _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); + private _unusedRects: ITextureAtlasSlabUnusedRect[] = []; + readonly glyphMap: TwoKeyMap = new TwoKeyMap(); private _nextIndex = 0; @@ -195,7 +197,18 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { const slabH = slabW; // this._canvas.height / 8; const slabsPerRow = Math.floor(this._canvas.width / slabW); + // Get any existing slab let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h); + + // Check if the slab is full + if (slab) { + const glyphsPerSlab = Math.floor(slabW / slab.entryW) * Math.floor(slabH / slab.entryH); + if (slab.count >= glyphsPerSlab) { + slab = undefined; + } + } + + // Create a new slab if (!slab) { slab = { x: Math.floor(this._slabs.length % slabsPerRow) * slabW, @@ -204,22 +217,34 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { entryH: desiredSlabSize.h, count: 0 }; - // console.log('new slab (full slab)', slab); - this._slabs.push(slab); - this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); - } - - // Create another slab if this one is full - const glyphsPerSlab = Math.floor(slabW / slab.entryW) * Math.floor(slabH / slab.entryH); - if (slab.count >= glyphsPerSlab) { - slab = { - x: Math.floor(this._slabs.length % slabsPerRow) * slabW, - y: Math.floor(this._slabs.length / slabsPerRow) * slabH, - entryW: desiredSlabSize.w, - entryH: desiredSlabSize.h, - count: 0 - }; - // console.log('new slab (full slab)', slab); + // Track unused regions to use for small glyphs + // +-------------+----+ + // | | | + // | | | <- Unused W region + // | | | + // |-------------+----+ + // | | <- Unused H region + // +------------------+ + const unusedW = slabW % slab.entryW; + const unusedH = slabH % slab.entryH; + if (unusedW) { + this._unusedRects.push({ + x: slab.x + slabW - unusedW, + w: unusedW, + y: slab.y, + h: slabH - (unusedH ?? 0) + }); + console.log('new unused (W)', this._unusedRects.at(-1)); + } + if (unusedH) { + this._unusedRects.push({ + x: slab.x, + w: slabW, + y: slab.y + slabH - unusedH, + h: unusedH + }); + console.log('new unused (H)', this._unusedRects.at(-1)); + } this._slabs.push(slab); this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); } @@ -265,8 +290,68 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } public getUsagePreview(): Promise { - // TODO: Impl - return this._canvas.convertToBlob(); + // TODO: This is specific to the simple shelf allocator + const w = this._canvas.width; + const h = this._canvas.height; + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + + ctx.fillStyle = '#808080'; + ctx.fillRect(0, 0, w, h); + + let slabEntryPixels = 0; + let usedPixels = 0; + let wastedPixels = 0; + const totalPixels = w * h; + const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); + + // Draw wasted underneath glyphs first + for (const slab of this._slabs) { + let x = 0; + let y = 0; + for (let i = 0; i < slab.count; i++) { + if (x + slab.entryW > slabW) { + x = 0; + y += slab.entryH; + } + // TODO: This doesn't visualize wasted space between entries - draw glyphs on top? + ctx.fillStyle = '#FF0000'; + ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH); + slabEntryPixels += slab.entryW * slab.entryH; + x += slab.entryW; + } + } + + // Draw glyphs + for (const g of this.glyphMap.values()) { + usedPixels += g.w * g.h; + ctx.fillStyle = '#4040FF'; + ctx.fillRect(g.x, g.y, g.w, g.h); + } + + // Draw unused space on side (currently wasted) + for (const r of this._unusedRects) { + ctx.fillStyle = '#FF0000'; + ctx.fillRect(r.x, r.y, r.w, r.h); + } + + // Overlay actual glyphs on top + ctx.globalAlpha = 0.5; + ctx.drawImage(this._canvas, 0, 0); + ctx.globalAlpha = 1; + + wastedPixels = slabEntryPixels - usedPixels; + + // Report stats + console.log([ + `Texture atlas stats:`, + ` Total: ${totalPixels}`, + ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, + ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, + `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, + ].join('\n')); + + return canvas.convertToBlob(); } } @@ -278,4 +363,11 @@ interface ITextureAtlasSlab { count: number; } +export interface ITextureAtlasSlabUnusedRect { + x: number; + y: number; + w: number; + h: number; +} + // #endregion From 86e5a9ac73d32f1be189d0f22d02039e245121c3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 13:14:59 +0900 Subject: [PATCH 051/286] Fix efficiency log --- src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index fee5520e90f..b1e70627993 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -185,7 +185,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth, // h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight, - // Exact number only + // Exact number only (100% efficiency) // w: glyphWidth, // h: glyphHeight, }; @@ -341,6 +341,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { ctx.globalAlpha = 1; wastedPixels = slabEntryPixels - usedPixels; + const efficiency = usedPixels / (usedPixels + wastedPixels); // Report stats console.log([ @@ -348,7 +349,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { ` Total: ${totalPixels}`, ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, - `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, + `Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toPrecision(2)}%`, ].join('\n')); return canvas.convertToBlob(); From e62af8fa1c8b961f7da7dbc52701712d475272a9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 13:43:08 +0900 Subject: [PATCH 052/286] Use unused space on edge of slabs --- .../browser/view/gpu/textureAtlasAllocator.ts | 144 ++++++++++++------ 1 file changed, 96 insertions(+), 48 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index b1e70627993..fb7b3c39368 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -160,11 +160,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { // Find ideal slab, creating it if there is none suitable - - // Slabs are sized: 1x1, 1x2, 1x4, 1x8, ... - // 2x1, 2x2, 2x4, 2x8, ... - // 4x1, 4x2, 4x4, 4x8, ... - // ... const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; const dpr = getActiveWindow().devicePixelRatio; @@ -185,7 +180,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth, // h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight, - // Exact number only (100% efficiency) + // Exact number only // w: glyphWidth, // h: glyphHeight, }; @@ -208,52 +203,107 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } } - // Create a new slab + let dx: number | undefined; + let dy: number | undefined; + + // Search for suitable space in unused rectangles if (!slab) { - slab = { - x: Math.floor(this._slabs.length % slabsPerRow) * slabW, - y: Math.floor(this._slabs.length / slabsPerRow) * slabH, - entryW: desiredSlabSize.w, - entryH: desiredSlabSize.h, - count: 0 - }; - // Track unused regions to use for small glyphs - // +-------------+----+ - // | | | - // | | | <- Unused W region - // | | | - // |-------------+----+ - // | | <- Unused H region - // +------------------+ - const unusedW = slabW % slab.entryW; - const unusedH = slabH % slab.entryH; - if (unusedW) { - this._unusedRects.push({ - x: slab.x + slabW - unusedW, - w: unusedW, - y: slab.y, - h: slabH - (unusedH ?? 0) - }); - console.log('new unused (W)', this._unusedRects.at(-1)); + for (const [i, r] of this._unusedRects.entries()) { + if (r.w < r.h) { + if (r.w >= glyphWidth && r.h >= glyphHeight) { + dx = r.x; + dy = r.y; + if (glyphWidth < r.w) { + this._unusedRects.push({ + x: r.x + glyphWidth, + y: r.y, + w: r.w - glyphWidth, + h: glyphHeight + }); + } + r.y += glyphHeight; + r.h -= glyphHeight; + if (r.h === 0) { + // TODO: This is slow + this._unusedRects.splice(i, 1); + } + break; + } + } else { + if (r.w >= glyphWidth && r.h >= glyphHeight) { + dx = r.x; + dy = r.y; + if (glyphHeight < r.h) { + this._unusedRects.push({ + x: r.x, + y: r.y + glyphHeight, + w: glyphWidth, + h: r.h - glyphHeight + }); + } + r.x += glyphWidth; + r.w -= glyphWidth; + if (r.w === 0) { + // TODO: This is slow + this._unusedRects.splice(i, 1); + } + } + } } - if (unusedH) { - this._unusedRects.push({ - x: slab.x, - w: slabW, - y: slab.y + slabH - unusedH, - h: unusedH - }); - console.log('new unused (H)', this._unusedRects.at(-1)); + } + + // Create a new slab + if (dx === undefined || dy === undefined) { + if (!slab) { + slab = { + x: Math.floor(this._slabs.length % slabsPerRow) * slabW, + y: Math.floor(this._slabs.length / slabsPerRow) * slabH, + entryW: desiredSlabSize.w, + entryH: desiredSlabSize.h, + count: 0 + }; + // Track unused regions to use for small glyphs + // +-------------+----+ + // | | | + // | | | <- Unused W region + // | | | + // |-------------+----+ + // | | <- Unused H region + // +------------------+ + const unusedW = slabW % slab.entryW; + const unusedH = slabH % slab.entryH; + if (unusedW) { + this._unusedRects.push({ + x: slab.x + slabW - unusedW, + w: unusedW, + y: slab.y, + h: slabH - (unusedH ?? 0) + }); + console.log('new unused (W)', this._unusedRects.at(-1)); + } + if (unusedH) { + this._unusedRects.push({ + x: slab.x, + w: slabW, + y: slab.y + slabH - unusedH, + h: unusedH + }); + console.log('new unused (H)', this._unusedRects.at(-1)); + } + this._slabs.push(slab); + this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); } - this._slabs.push(slab); - this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); + + const glyphsPerRow = Math.floor(slabW / slab.entryW); + dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW; + dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH; + + // Shift current row + slab.count++; } // Draw glyph // TODO: Prefer putImageData as it doesn't do blending or scaling - const glyphsPerRow = Math.floor(slabW / slab.entryW); - const dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW; - const dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH; console.log('dx dy', dx, dy); this._ctx.drawImage( rasterizedGlyph.source, @@ -280,9 +330,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { originOffsetY: rasterizedGlyph.originOffset.y }; - // Shift current row - slab.count++; - // Set the glyph this.glyphMap.set(chars, tokenFg, glyph); @@ -331,6 +378,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Draw unused space on side (currently wasted) for (const r of this._unusedRects) { + // TODO: Unused space claimed by unused rects isn't handled ctx.fillStyle = '#FF0000'; ctx.fillRect(r.x, r.y, r.w, r.h); } From 44f2c064cd8000e93aba21a2a8d5d2abd763c0a2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 13 May 2024 14:22:11 +0900 Subject: [PATCH 053/286] Fix up usage stats --- .../browser/view/gpu/glyphRasterizer.ts | 24 ++++++------ .../browser/view/gpu/textureAtlasAllocator.ts | 39 ++++++++++++------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts index 73637e24146..7a5fe1539d8 100644 --- a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts @@ -41,19 +41,17 @@ export class GlyphRasterizer extends Disposable { const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); // TODO: Hot path: Reuse object const boundingBox = this._findGlyphBoundingBox(imageData); - const offset = { - x: textMetrics.actualBoundingBoxLeft, - y: textMetrics.actualBoundingBoxAscent - }; - const size = { - w: textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft, - y: textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent, - wInt: Math.ceil(textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft), - yInt: Math.ceil(textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent), - }; - - console.log(`${chars}_${fg}`, textMetrics, boundingBox, originX, originY, { width: boundingBox.right - boundingBox.left, height: boundingBox.bottom - boundingBox.top }); - console.log('new', offset, size); + // const offset = { + // x: textMetrics.actualBoundingBoxLeft, + // y: textMetrics.actualBoundingBoxAscent + // }; + // const size = { + // w: textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft, + // y: textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent, + // wInt: Math.ceil(textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft), + // yInt: Math.ceil(textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent), + // }; + // console.log(`${chars}_${fg}`, textMetrics, boundingBox, originX, originY, { width: boundingBox.right - boundingBox.left, height: boundingBox.bottom - boundingBox.top }); const result: IRasterizedGlyph = { source: this._canvas, boundingBox, diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index fb7b3c39368..29f3942dbcf 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -185,9 +185,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // h: glyphHeight, }; - // TODO: Keeping track of the slab's x and y could allow variable sized slabs and less waste - // TODO: The unused rectangle at the bottom and side of a slab could house micro glyphs like `.` - const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); // this._canvas.width / 8; const slabH = slabW; // this._canvas.height / 8; const slabsPerRow = Math.floor(this._canvas.width / slabW); @@ -279,7 +276,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { y: slab.y, h: slabH - (unusedH ?? 0) }); - console.log('new unused (W)', this._unusedRects.at(-1)); } if (unusedH) { this._unusedRects.push({ @@ -288,7 +284,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { y: slab.y + slabH - unusedH, h: unusedH }); - console.log('new unused (H)', this._unusedRects.at(-1)); } this._slabs.push(slab); this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); @@ -304,7 +299,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Draw glyph // TODO: Prefer putImageData as it doesn't do blending or scaling - console.log('dx dy', dx, dy); this._ctx.drawImage( rasterizedGlyph.source, // source @@ -348,9 +342,12 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { let slabEntryPixels = 0; let usedPixels = 0; + let slabEdgePixels = 0; let wastedPixels = 0; + let restrictedPixels = 0; const totalPixels = w * h; const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); + const slabH = slabW; // Draw wasted underneath glyphs first for (const slab of this._slabs) { @@ -364,9 +361,14 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // TODO: This doesn't visualize wasted space between entries - draw glyphs on top? ctx.fillStyle = '#FF0000'; ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH); + slabEntryPixels += slab.entryW * slab.entryH; x += slab.entryW; } + const entriesPerRow = Math.floor(slabW / slab.entryW); + const entriesPerCol = Math.floor(slabH / slab.entryH); + const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol; + slabEdgePixels += (slabW * slabH) - thisSlabPixels; } // Draw glyphs @@ -376,28 +378,35 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { ctx.fillRect(g.x, g.y, g.w, g.h); } - // Draw unused space on side (currently wasted) + // Draw unused space on side for (const r of this._unusedRects) { - // TODO: Unused space claimed by unused rects isn't handled - ctx.fillStyle = '#FF0000'; + ctx.fillStyle = '#FF000080'; ctx.fillRect(r.x, r.y, r.w, r.h); + restrictedPixels += r.w * r.h; } + const edgeUsedPixels = slabEdgePixels - restrictedPixels; + console.log({ edgeUsedPixels, slabEdgePixels, restrictedPixels }); + wastedPixels = slabEntryPixels - (usedPixels - edgeUsedPixels); + + // Overlay actual glyphs on top ctx.globalAlpha = 0.5; ctx.drawImage(this._canvas, 0, 0); ctx.globalAlpha = 1; - wastedPixels = slabEntryPixels - usedPixels; - const efficiency = usedPixels / (usedPixels + wastedPixels); + // usedPixels += slabEdgePixels - restrictedPixels; + const efficiency = usedPixels / (usedPixels + wastedPixels + restrictedPixels); // Report stats console.log([ `Texture atlas stats:`, - ` Total: ${totalPixels}`, - ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, - ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, - `Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toPrecision(2)}%`, + ` Total: ${totalPixels}px`, + ` Used: ${usedPixels}px (${((usedPixels / totalPixels) * 100).toFixed(2)}%)`, + ` Wasted: ${wastedPixels}px (${((wastedPixels / totalPixels) * 100).toFixed(2)}%)`, + `Restricted: ${restrictedPixels}px (${((restrictedPixels / totalPixels) * 100).toFixed(2)}%) (hard to allocate)`, + `Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toFixed(2)}%`, + ` Slabs: ${this._slabs.length} of ${Math.floor(this._canvas.width / slabW) * Math.floor(this._canvas.height / slabH)}` ].join('\n')); return canvas.convertToBlob(); From eeadf297a3694c415226aa089a403506b16dae84 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 15 May 2024 20:15:27 +1000 Subject: [PATCH 054/286] Tweak nearest x pixels --- src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index 29f3942dbcf..812b979d4e7 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -164,8 +164,11 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; const dpr = getActiveWindow().devicePixelRatio; + // TODO: Include font size as well as DPR in nearestXPixels calculation + // Round slab glyph dimensions to the nearest x pixels, where x scaled with device pixel ratio - const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5)); + // const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5)); + const nearestXPixels = Math.max(1, Math.floor(dpr)); const desiredSlabSize = { // Nearest square number // TODO: This can probably be optimized From 3de59fcc49c151915196a246e375574c09d58532 Mon Sep 17 00:00:00 2001 From: Ritam Mukherjee Date: Wed, 22 May 2024 21:03:54 +0530 Subject: [PATCH 055/286] feat: allows cli to serve locally cached server when update service not available --- cli/src/commands/serve_web.rs | 24 ++++++++++++++++++++++-- cli/src/download_cache.rs | 25 ++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index fba92723426..313386a380c 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -516,7 +516,7 @@ impl ConnectionManager { platform, args, log: ctx.log.clone(), - cache: DownloadCache::new(ctx.paths.web_server_storage()), + cache: DownloadCache::load(ctx.paths.web_server_storage()), update_service: UpdateService::new( ctx.log.clone(), Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())), @@ -544,6 +544,7 @@ impl ConnectionManager { pub async fn get_latest_release(&self) -> Result { let mut latest = self.latest_version.lock().await; let now = Instant::now(); + let target_kind = TargetKind::Web; if let Some((checked_at, release)) = &*latest { if checked_at.elapsed() < Duration::from_secs(RELEASE_CACHE_SECS) { return Ok(release.clone()); @@ -558,7 +559,7 @@ impl ConnectionManager { let release = self .update_service - .get_latest_commit(self.platform, TargetKind::Web, quality) + .get_latest_commit(self.platform, target_kind, quality) .await .map_err(|e| CodeError::UpdateCheckFailed(e.to_string())); @@ -568,6 +569,25 @@ impl ConnectionManager { return Ok(previous.clone()); } + // If the update service is unavailable and we have cached data, use that + if let Err(e) = &release { + warning!(self.log, "error getting latest release: {}", e); + if let Some(latest_commit) = self.cache.get().first() { + warning!(self.log, "using latest release available from cache"); + let release = Release { + name: String::from("0.0.0"), // Version information not stored on cache + commit: latest_commit.clone(), + platform: self.platform, + target: target_kind, + quality + }; + + *latest = Some((now, release.clone())); + + return Ok(release) + } + } + let release = release?; debug!(self.log, "refreshed latest release: {}", release); *latest = Some((now, release.clone())); diff --git a/cli/src/download_cache.rs b/cli/src/download_cache.rs index d3f05d2237f..5f245e810a1 100644 --- a/cli/src/download_cache.rs +++ b/cli/src/download_cache.rs @@ -20,6 +20,7 @@ const KEEP_LRU: usize = 5; const STAGING_SUFFIX: &str = ".staging"; const RENAME_ATTEMPTS: u32 = 20; const RENAME_DELAY: std::time::Duration = std::time::Duration::from_millis(200); +const PERSISTED_STATE_FILE_NAME: &str = "lru.json"; #[derive(Clone)] pub struct DownloadCache { @@ -30,11 +31,33 @@ pub struct DownloadCache { impl DownloadCache { pub fn new(path: PathBuf) -> DownloadCache { DownloadCache { - state: PersistedState::new(path.join("lru.json")), + state: PersistedState::new(path.join(PERSISTED_STATE_FILE_NAME)), path, } } + /// Gets an DownloadCache with previously persisted value if it exists + /// on the persistant storage, else returns a new DownloadCache. + pub fn load(path: PathBuf) -> DownloadCache { + let state = PersistedState::>::new(path.join(PERSISTED_STATE_FILE_NAME)); + match state.load().is_empty() { + true => DownloadCache { + state: PersistedState::new(path.join(PERSISTED_STATE_FILE_NAME)), + path, + }, + false => DownloadCache { + state, + path, + } + } + } + + /// Gets the value stored on the state + pub fn get(&self) -> Vec { + let state_value = self.state.load(); + state_value + } + /// Gets the download cache path. Names of cache entries can be formed by /// joining them to the path. pub fn path(&self) -> &Path { From 23f2247b7274c2a750d3e7fa778362bf16a33ddb Mon Sep 17 00:00:00 2001 From: Ritam Mukherjee Date: Wed, 22 May 2024 22:33:16 +0530 Subject: [PATCH 056/286] refactor: fix clippy let-and-return error in linting --- cli/src/download_cache.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/src/download_cache.rs b/cli/src/download_cache.rs index 5f245e810a1..dae12a6ea62 100644 --- a/cli/src/download_cache.rs +++ b/cli/src/download_cache.rs @@ -54,8 +54,7 @@ impl DownloadCache { /// Gets the value stored on the state pub fn get(&self) -> Vec { - let state_value = self.state.load(); - state_value + self.state.load() } /// Gets the download cache path. Names of cache entries can be formed by From bc5e7b51a2d538c0d36da7e1a0dd0d9829707a82 Mon Sep 17 00:00:00 2001 From: Ritam Mukherjee Date: Wed, 22 May 2024 23:05:10 +0530 Subject: [PATCH 057/286] refactor: removed unnecessary check for empty vector in DownloadCache load --- cli/src/download_cache.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cli/src/download_cache.rs b/cli/src/download_cache.rs index dae12a6ea62..5a343315d86 100644 --- a/cli/src/download_cache.rs +++ b/cli/src/download_cache.rs @@ -40,15 +40,10 @@ impl DownloadCache { /// on the persistant storage, else returns a new DownloadCache. pub fn load(path: PathBuf) -> DownloadCache { let state = PersistedState::>::new(path.join(PERSISTED_STATE_FILE_NAME)); - match state.load().is_empty() { - true => DownloadCache { - state: PersistedState::new(path.join(PERSISTED_STATE_FILE_NAME)), - path, - }, - false => DownloadCache { - state, - path, - } + state.load(); + DownloadCache { + state, + path, } } From a5b3f579dbd6c8aa89f94ac5612f79aac9a3c472 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 24 May 2024 08:12:54 +0900 Subject: [PATCH 058/286] progress --- .../browser/view/gpu/glyphRasterizer.ts | 35 ++-- .../editor/browser/view/gpu/gpuViewLayer.ts | 2 +- .../editor/browser/view/gpu/textureAtlas.ts | 22 ++- .../browser/view/gpu/textureAtlasAllocator.ts | 165 +++++++++++++----- 4 files changed, 160 insertions(+), 64 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts index 7a5fe1539d8..a4b9829b5df 100644 --- a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts @@ -35,7 +35,8 @@ export class GlyphRasterizer extends Disposable { const originX = this._fontSize; const originY = this._fontSize; this._ctx.fillStyle = fg; - const textMetrics = this._ctx.measureText(chars); + // TODO: This might actually be slower + // const textMetrics = this._ctx.measureText(chars); this._ctx.fillText(chars, originX, originY); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); @@ -60,19 +61,19 @@ export class GlyphRasterizer extends Disposable { y: boundingBox.top - originY } }; - const result2: IRasterizedGlyph = { - source: this._canvas, - boundingBox: { - left: Math.floor(originX - textMetrics.actualBoundingBoxLeft), - right: Math.ceil(originX + textMetrics.actualBoundingBoxRight), - top: Math.floor(originY - textMetrics.actualBoundingBoxAscent), - bottom: Math.ceil(originY + textMetrics.actualBoundingBoxDescent), - }, - originOffset: { - x: Math.floor(boundingBox.left - originX), - y: Math.floor(boundingBox.top - originY) - } - }; + // const result2: IRasterizedGlyph = { + // source: this._canvas, + // boundingBox: { + // left: Math.floor(originX - textMetrics.actualBoundingBoxLeft), + // right: Math.ceil(originX + textMetrics.actualBoundingBoxRight), + // top: Math.floor(originY - textMetrics.actualBoundingBoxAscent), + // bottom: Math.ceil(originY + textMetrics.actualBoundingBoxDescent), + // }, + // originOffset: { + // x: Math.floor(boundingBox.left - originX), + // y: Math.floor(boundingBox.top - originY) + // } + // }; // DEBUG: Show image data in console // (console as any).image(imageData); @@ -91,9 +92,9 @@ export class GlyphRasterizer extends Disposable { // if (result2.boundingBox.bottom < result.boundingBox.bottom) { // debugger; // } - if (JSON.stringify(result2.originOffset) !== JSON.stringify(result.originOffset)) { - debugger; - } + // if (JSON.stringify(result2.originOffset) !== JSON.stringify(result.originOffset)) { + // debugger; + // } return result; } diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index eeb7227f00c..0d62f45cfa0 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -111,7 +111,7 @@ export class GpuViewLayerRenderer { // Create texture atlas if (!GpuViewLayerRenderer._textureAtlas) { - const pageSize = 1024; // this._device.limits.maxTextureDimension2D; + const pageSize = this._device.limits.maxTextureDimension2D; GpuViewLayerRenderer._textureAtlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, pageSize, this._device.limits.maxTextureDimension2D); } const textureAtlas = GpuViewLayerRenderer._textureAtlas; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 13fd80ee962..5d71fb53af2 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -111,9 +111,27 @@ export class TextureAtlas extends Disposable { // TODO: Clean up on dispose this._warmUpTask?.clear(); this._warmUpTask = new IdleTaskQueue(); - for (const tokenFg of this._colorMap.keys()) { + // Warm up using roughly the larger glyphs first to help optimize atlas allocation + // A-Z + for (let code = 65; code <= 90; code++) { this._warmUpTask.enqueue(() => { - for (let code = 33; code <= 126; code++) { + for (const tokenFg of this._colorMap.keys()) { + this.getGlyph(String.fromCharCode(code), tokenFg); + } + }); + } + // a-z + for (let code = 97; code <= 122; code++) { + this._warmUpTask.enqueue(() => { + for (const tokenFg of this._colorMap.keys()) { + this.getGlyph(String.fromCharCode(code), tokenFg); + } + }); + } + // Remaining ascii + for (let code = 33; code <= 126; code++) { + this._warmUpTask.enqueue(() => { + for (const tokenFg of this._colorMap.keys()) { this.getGlyph(String.fromCharCode(code), tokenFg); } }); diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index 812b979d4e7..e881337feb4 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -148,6 +148,9 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private _unusedRects: ITextureAtlasSlabUnusedRect[] = []; + private _openRegionsByHeight: Map = new Map(); + private _openRegionsByWidth: Map = new Map(); + readonly glyphMap: TwoKeyMap = new TwoKeyMap(); private _nextIndex = 0; @@ -168,7 +171,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Round slab glyph dimensions to the nearest x pixels, where x scaled with device pixel ratio // const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5)); - const nearestXPixels = Math.max(1, Math.floor(dpr)); + // const nearestXPixels = Math.max(1, Math.floor(dpr)); const desiredSlabSize = { // Nearest square number // TODO: This can probably be optimized @@ -176,16 +179,16 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // h: 1 << Math.ceil(Math.sqrt(glyphHeight)), // Nearest x px - w: Math.ceil(glyphWidth / nearestXPixels) * nearestXPixels, - h: Math.ceil(glyphHeight / nearestXPixels) * nearestXPixels, + // w: Math.ceil(glyphWidth / nearestXPixels) * nearestXPixels, + // h: Math.ceil(glyphHeight / nearestXPixels) * nearestXPixels, // Round odd numbers up // w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth, // h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight, // Exact number only - // w: glyphWidth, - // h: glyphHeight, + w: glyphWidth, + h: glyphHeight, }; const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); // this._canvas.width / 8; @@ -208,48 +211,112 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Search for suitable space in unused rectangles if (!slab) { - for (const [i, r] of this._unusedRects.entries()) { - if (r.w < r.h) { - if (r.w >= glyphWidth && r.h >= glyphHeight) { - dx = r.x; - dy = r.y; - if (glyphWidth < r.w) { - this._unusedRects.push({ - x: r.x + glyphWidth, - y: r.y, - w: r.w - glyphWidth, - h: glyphHeight - }); + // Only check availability for the smallest side + if (glyphWidth < glyphHeight) { + const openRegions = this._openRegionsByWidth.get(glyphWidth); + if (openRegions?.length) { + // TODO: Don't search everything? + // Search from the end so we can typically pop it off the stack + for (let i = openRegions.length - 1; i >= 0; i--) { + const r = openRegions[i]; + if (r.w >= glyphWidth && r.h >= glyphHeight) { + dx = r.x; + dy = r.y; + if (glyphWidth < r.w) { + this._unusedRects.push({ + x: r.x + glyphWidth, + y: r.y, + w: r.w - glyphWidth, + h: glyphHeight + }); + } + r.y += glyphHeight; + r.h -= glyphHeight; + if (r.h === 0) { + if (i === openRegions.length - 1) { + openRegions.pop(); + } else { + this._unusedRects.splice(i, 1); + } + } + break; } - r.y += glyphHeight; - r.h -= glyphHeight; - if (r.h === 0) { - // TODO: This is slow - this._unusedRects.splice(i, 1); - } - break; } - } else { - if (r.w >= glyphWidth && r.h >= glyphHeight) { - dx = r.x; - dy = r.y; - if (glyphHeight < r.h) { - this._unusedRects.push({ - x: r.x, - y: r.y + glyphHeight, - w: glyphWidth, - h: r.h - glyphHeight - }); - } - r.x += glyphWidth; - r.w -= glyphWidth; - if (r.w === 0) { - // TODO: This is slow - this._unusedRects.splice(i, 1); + } + } else { + const openRegions = this._openRegionsByHeight.get(glyphHeight); + if (openRegions?.length) { + // TODO: Don't search everything? + // Search from the end so we can typically pop it off the stack + for (let i = openRegions.length - 1; i >= 0; i--) { + const r = openRegions[i]; + if (r.w >= glyphWidth && r.h >= glyphHeight) { + dx = r.x; + dy = r.y; + if (glyphHeight < r.h) { + this._unusedRects.push({ + x: r.x, + y: r.y + glyphHeight, + w: glyphWidth, + h: r.h - glyphHeight + }); + } + r.x += glyphWidth; + r.w -= glyphWidth; + if (r.h === 0) { + if (i === openRegions.length - 1) { + openRegions.pop(); + } else { + this._unusedRects.splice(i, 1); + } + } + break; } } } } + // for (const [i, r] of this._unusedRects.entries()) { + // if (r.w < r.h) { + // if (r.w >= glyphWidth && r.h >= glyphHeight) { + // dx = r.x; + // dy = r.y; + // if (glyphWidth < r.w) { + // this._unusedRects.push({ + // x: r.x + glyphWidth, + // y: r.y, + // w: r.w - glyphWidth, + // h: glyphHeight + // }); + // } + // r.y += glyphHeight; + // r.h -= glyphHeight; + // if (r.h === 0) { + // // TODO: This is slow + // this._unusedRects.splice(i, 1); + // } + // break; + // } + // } else { + // if (r.w >= glyphWidth && r.h >= glyphHeight) { + // dx = r.x; + // dy = r.y; + // if (glyphHeight < r.h) { + // this._unusedRects.push({ + // x: r.x, + // y: r.y + glyphHeight, + // w: glyphWidth, + // h: r.h - glyphHeight + // }); + // } + // r.x += glyphWidth; + // r.w -= glyphWidth; + // if (r.w === 0) { + // // TODO: This is slow + // this._unusedRects.splice(i, 1); + // } + // } + // } + // } } // Create a new slab @@ -273,7 +340,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { const unusedW = slabW % slab.entryW; const unusedH = slabH % slab.entryH; if (unusedW) { - this._unusedRects.push({ + addEntryToMapArray(this._openRegionsByWidth, unusedW, { x: slab.x + slabW - unusedW, w: unusedW, y: slab.y, @@ -281,7 +348,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { }); } if (unusedH) { - this._unusedRects.push({ + addEntryToMapArray(this._openRegionsByHeight, unusedH, { x: slab.x, w: slabW, y: slab.y + slabH - unusedH, @@ -382,7 +449,8 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } // Draw unused space on side - for (const r of this._unusedRects) { + const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat()); + for (const r of unusedRegions) { ctx.fillStyle = '#FF000080'; ctx.fillRect(r.x, r.y, r.w, r.h); restrictedPixels += r.w * r.h; @@ -431,4 +499,13 @@ export interface ITextureAtlasSlabUnusedRect { h: number; } +function addEntryToMapArray(map: Map, key: K, entry: V) { + let list = map.get(key); + if (!list) { + list = []; + map.set(key, list); + } + list.push(entry); +} + // #endregion From 076e5f82ffb7de93997bffb904c3455a4a836474 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:29:26 -0700 Subject: [PATCH 059/286] Fix lint issue --- src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index e881337feb4..b04436619d3 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -165,7 +165,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Find ideal slab, creating it if there is none suitable const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; - const dpr = getActiveWindow().devicePixelRatio; + // const dpr = getActiveWindow().devicePixelRatio; // TODO: Include font size as well as DPR in nearestXPixels calculation From 6ae98635236fd1bc99bbf47de99d0e71b8f200b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:11:43 -0700 Subject: [PATCH 060/286] Reuse hot rasterized glyph/bbox objects --- .../browser/view/gpu/glyphRasterizer.ts | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts index a4b9829b5df..5b760c56ae3 100644 --- a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts @@ -6,6 +6,21 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +const $rasterizedGlyph: IRasterizedGlyph = { + source: null!, + boundingBox: { + left: 0, + bottom: 0, + right: 0, + top: 0, + }, + originOffset: { + x: 0, + y: 0, + } +}; +const $bbox = $rasterizedGlyph.boundingBox; + export class GlyphRasterizer extends Disposable { private _canvas: OffscreenCanvas; // A temporary context that glyphs are drawn to before being transfered to the atlas. @@ -28,6 +43,10 @@ export class GlyphRasterizer extends Disposable { // TODO: Support drawing multiple fonts and sizes // TODO: Should pull in the font size from config instead of random dom node + /** + * Rasterizes a glyph. Note that the returned object is reused across different glyphs and + * therefore is only safe for synchronous access. + */ public rasterizeGlyph(chars: string, fg: string): IRasterizedGlyph { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); @@ -40,8 +59,7 @@ export class GlyphRasterizer extends Disposable { this._ctx.fillText(chars, originX, originY); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - // TODO: Hot path: Reuse object - const boundingBox = this._findGlyphBoundingBox(imageData); + this._findGlyphBoundingBox(imageData, $rasterizedGlyph.boundingBox); // const offset = { // x: textMetrics.actualBoundingBoxLeft, // y: textMetrics.actualBoundingBoxAscent @@ -53,14 +71,10 @@ export class GlyphRasterizer extends Disposable { // yInt: Math.ceil(textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent), // }; // console.log(`${chars}_${fg}`, textMetrics, boundingBox, originX, originY, { width: boundingBox.right - boundingBox.left, height: boundingBox.bottom - boundingBox.top }); - const result: IRasterizedGlyph = { - source: this._canvas, - boundingBox, - originOffset: { - x: boundingBox.left - originX, - y: boundingBox.top - originY - } - }; + $rasterizedGlyph.source = this._canvas; + $rasterizedGlyph.originOffset.x = $bbox.left - originX; + $rasterizedGlyph.originOffset.y = $bbox.top - originY; + // const result2: IRasterizedGlyph = { // source: this._canvas, // boundingBox: { @@ -96,20 +110,12 @@ export class GlyphRasterizer extends Disposable { // debugger; // } - return result; + return $rasterizedGlyph; } // TODO: Does this even need to happen when measure text is used? // TODO: Pass back origin offset - private _findGlyphBoundingBox(imageData: ImageData): IBoundingBox { - - // TODO: Hot path: Reuse object - const boundingBox = { - left: 0, - top: 0, - right: 0, - bottom: 0 - }; + private _findGlyphBoundingBox(imageData: ImageData, outBoundingBox: IBoundingBox) { // TODO: This could be optimized to be aware of the font size padding on all sides const height = this._canvas.height; const width = this._canvas.width; @@ -118,7 +124,7 @@ export class GlyphRasterizer extends Disposable { for (let x = 0; x < width; x++) { const alphaOffset = y * width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { - boundingBox.top = y; + outBoundingBox.top = y; found = true; break; } @@ -127,13 +133,13 @@ export class GlyphRasterizer extends Disposable { break; } } - boundingBox.left = 0; + outBoundingBox.left = 0; found = false; for (let x = 0; x < width; x++) { for (let y = 0; y < height; y++) { const alphaOffset = y * width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { - boundingBox.left = x; + outBoundingBox.left = x; found = true; break; } @@ -142,13 +148,13 @@ export class GlyphRasterizer extends Disposable { break; } } - boundingBox.right = width; + outBoundingBox.right = width; found = false; - for (let x = width - 1; x >= boundingBox.left; x--) { + for (let x = width - 1; x >= outBoundingBox.left; x--) { for (let y = 0; y < height; y++) { const alphaOffset = y * width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { - boundingBox.right = x; + outBoundingBox.right = x; found = true; break; } @@ -157,13 +163,13 @@ export class GlyphRasterizer extends Disposable { break; } } - boundingBox.bottom = boundingBox.top; + outBoundingBox.bottom = outBoundingBox.top; found = false; for (let y = height - 1; y >= 0; y--) { for (let x = 0; x < width; x++) { const alphaOffset = y * width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { - boundingBox.bottom = y; + outBoundingBox.bottom = y; found = true; break; } @@ -172,7 +178,6 @@ export class GlyphRasterizer extends Disposable { break; } } - return boundingBox; } } @@ -185,6 +190,9 @@ export interface IBoundingBox { export interface IRasterizedGlyph { source: CanvasImageSource; + /** + * The bounding box of the glyph within {@link source}. + */ boundingBox: IBoundingBox; originOffset: { x: number; y: number }; } From eba629461bb649f1b51728dc8ee1c296cf69a920 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:32:44 -0700 Subject: [PATCH 061/286] Remove todos --- src/vs/editor/browser/view/gpu/glyphRasterizer.ts | 2 +- src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts index 5b760c56ae3..82a2f75910a 100644 --- a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/glyphRasterizer.ts @@ -189,7 +189,7 @@ export interface IBoundingBox { } export interface IRasterizedGlyph { - source: CanvasImageSource; + source: OffscreenCanvas; /** * The bounding box of the glyph within {@link source}. */ diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index b04436619d3..136e2e6fd28 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -50,7 +50,6 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { // Draw glyph const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; - // TODO: Prefer putImageData as it doesn't do blending or scaling this._ctx.drawImage( rasterizedGlyph.source, // source @@ -87,7 +86,6 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { } public getUsagePreview(): Promise { - // TODO: This is specific to the simple shelf allocator const w = this._canvas.width; const h = this._canvas.height; const canvas = new OffscreenCanvas(w, h); @@ -401,7 +399,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } public getUsagePreview(): Promise { - // TODO: This is specific to the simple shelf allocator const w = this._canvas.width; const h = this._canvas.height; const canvas = new OffscreenCanvas(w, h); From f32367aaa06cba3322207dbee05ed8767afa3ad7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:43:31 -0700 Subject: [PATCH 062/286] Clean up warm up task on dispose --- src/vs/editor/browser/view/gpu/taskQueue.ts | 8 +++++++- src/vs/editor/browser/view/gpu/textureAtlas.ts | 15 +++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/taskQueue.ts b/src/vs/editor/browser/view/gpu/taskQueue.ts index 5b96daf2567..e682c5fa58d 100644 --- a/src/vs/editor/browser/view/gpu/taskQueue.ts +++ b/src/vs/editor/browser/view/gpu/taskQueue.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from 'vs/base/browser/dom'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; /** * Copyright (c) 2022 The xterm.js authors. All rights reserved. @@ -35,11 +36,16 @@ interface ITaskDeadline { } type CallbackWithDeadline = (deadline: ITaskDeadline) => void; -abstract class TaskQueue implements ITaskQueue { +abstract class TaskQueue extends Disposable implements ITaskQueue { private _tasks: (() => boolean | void)[] = []; private _idleCallback?: number; private _i = 0; + constructor() { + super(); + this._register(toDisposable(() => this.clear())); + } + protected abstract _requestCallback(callback: CallbackWithDeadline): number; protected abstract _cancelCallback(identifier: number): void; diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/textureAtlas.ts index 5d71fb53af2..ef6a9b0c4c2 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlas.ts @@ -5,7 +5,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/textureAtlasPage'; @@ -53,7 +53,7 @@ export class TextureAtlas extends Disposable { private readonly _glyphRasterizer: GlyphRasterizer; private _colorMap!: string[]; - private _warmUpTask?: IdleTaskQueue; + private readonly _warmUpTask: MutableDisposable = new MutableDisposable(); public get source(): OffscreenCanvas { return this._page.source; @@ -108,13 +108,12 @@ export class TextureAtlas extends Disposable { * is distrubuted over multiple idle callbacks to avoid blocking the main thread. */ private _warmUpAtlas(): void { - // TODO: Clean up on dispose - this._warmUpTask?.clear(); - this._warmUpTask = new IdleTaskQueue(); + this._warmUpTask.value?.clear(); + const taskQueue = this._warmUpTask.value = new IdleTaskQueue(); // Warm up using roughly the larger glyphs first to help optimize atlas allocation // A-Z for (let code = 65; code <= 90; code++) { - this._warmUpTask.enqueue(() => { + taskQueue.enqueue(() => { for (const tokenFg of this._colorMap.keys()) { this.getGlyph(String.fromCharCode(code), tokenFg); } @@ -122,7 +121,7 @@ export class TextureAtlas extends Disposable { } // a-z for (let code = 97; code <= 122; code++) { - this._warmUpTask.enqueue(() => { + taskQueue.enqueue(() => { for (const tokenFg of this._colorMap.keys()) { this.getGlyph(String.fromCharCode(code), tokenFg); } @@ -130,7 +129,7 @@ export class TextureAtlas extends Disposable { } // Remaining ascii for (let code = 33; code <= 126; code++) { - this._warmUpTask.enqueue(() => { + taskQueue.enqueue(() => { for (const tokenFg of this._colorMap.keys()) { this.getGlyph(String.fromCharCode(code), tokenFg); } From 8027716a2b2daa45a8d223d49d2293c06d7f2715 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Aug 2024 06:50:01 -0700 Subject: [PATCH 063/286] Move atlas/raster into folders --- .../view/gpu/{ => atlas}/textureAtlas.ts | 5 +- .../view/gpu/atlas/textureAtlasAllocator.ts | 508 ++++++++++++++++++ .../view/gpu/{ => atlas}/textureAtlasPage.ts | 4 +- .../editor/browser/view/gpu/gpuViewLayer.ts | 2 +- .../view/gpu/{ => raster}/glyphRasterizer.ts | 0 .../browser/view/gpu/textureAtlasAllocator.ts | 4 +- 6 files changed, 515 insertions(+), 8 deletions(-) rename src/vs/editor/browser/view/gpu/{ => atlas}/textureAtlas.ts (95%) create mode 100644 src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts rename src/vs/editor/browser/view/gpu/{ => atlas}/textureAtlasPage.ts (97%) rename src/vs/editor/browser/view/gpu/{ => raster}/glyphRasterizer.ts (100%) diff --git a/src/vs/editor/browser/view/gpu/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts similarity index 95% rename from src/vs/editor/browser/view/gpu/textureAtlas.ts rename to src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index ef6a9b0c4c2..3dcd7f9d718 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -6,9 +6,9 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; +import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; -import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/textureAtlasPage'; +import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -89,7 +89,6 @@ export class TextureAtlas extends Disposable { })); this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); - // this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); this._page = this._register(this._instantiationService.createInstance(TextureAtlasPage, parentDomNode, pageSize, maxTextureSize, this._glyphRasterizer)); } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts new file mode 100644 index 00000000000..31fb76852c5 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts @@ -0,0 +1,508 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from 'vs/base/browser/dom'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; +import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; + +export interface ITextureAtlasAllocator { + readonly glyphMap: TwoKeyMap; + allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph; + getUsagePreview(): Promise; +} + +// #region Shelf allocator + +export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { + private _currentRow: ITextureAtlasShelf = { + x: 0, + y: 0, + h: 0 + }; + + // TODO: Allow for multiple active rows + // public readonly fixedRows: ICharAtlasActiveRow[] = []; + + readonly glyphMap: TwoKeyMap = new TwoKeyMap(); + + private _nextIndex = 0; + + constructor( + private readonly _canvas: OffscreenCanvas, + private readonly _ctx: OffscreenCanvasRenderingContext2D, + ) { + } + + public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { + // Finalize row if it doesn't fix + if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left > this._canvas.width - this._currentRow.x) { + this._currentRow.x = 0; + this._currentRow.y += this._currentRow.h; + this._currentRow.h = 1; + } + + // TODO: Handle end of atlas page + + // Draw glyph + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; + this._ctx.drawImage( + rasterizedGlyph.source, + // source + rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.top, + glyphWidth, + glyphHeight, + // destination + this._currentRow.x, + this._currentRow.y, + glyphWidth, + glyphHeight + ); + + // Create glyph object + const glyph: ITextureAtlasGlyph = { + index: this._nextIndex++, + x: this._currentRow.x, + y: this._currentRow.y, + w: glyphWidth, + h: glyphHeight, + originOffsetX: rasterizedGlyph.originOffset.x, + originOffsetY: rasterizedGlyph.originOffset.y + }; + + // Shift current row + this._currentRow.x += glyphWidth; + this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); + + // Set the glyph + this.glyphMap.set(chars, tokenFg, glyph); + + return glyph; + } + + public getUsagePreview(): Promise { + const w = this._canvas.width; + const h = this._canvas.height; + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.fillStyle = '#808080'; + ctx.fillRect(0, 0, w, h); + + let usedPixels = 0; + let wastedPixels = 0; + const totalPixels = w * h; + + const rowHeight: Map = new Map(); // y -> h + const rowWidth: Map = new Map(); // y -> w + for (const g of this.glyphMap.values()) { + rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); + rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); + } + for (const g of this.glyphMap.values()) { + usedPixels += g.w * g.h; + wastedPixels += g.w * (rowHeight.get(g.y)! - g.h); + ctx.fillStyle = '#4040FF'; + ctx.fillRect(g.x, g.y, g.w, g.h); + ctx.fillStyle = '#FF0000'; + ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); + } + for (const [rowY, rowW] of rowWidth.entries()) { + if (rowY !== this._currentRow.y) { + ctx.fillStyle = '#FF0000'; + ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); + wastedPixels += (w - rowW) * rowHeight.get(rowY)!; + } + } + console.log([ + `Texture atlas stats:`, + ` Total: ${totalPixels}`, + ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, + ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, + `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, + ].join('\n')); + return canvas.convertToBlob(); + } +} + +interface ITextureAtlasShelf { + x: number; + y: number; + h: number; +} + +// #endregion + +// #region Slab allocator + +export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { + // TODO: Is there a better way to index slabs other than an unsorted list? + private _slabs: ITextureAtlasSlab[] = []; + private _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); + + private _unusedRects: ITextureAtlasSlabUnusedRect[] = []; + + private _openRegionsByHeight: Map = new Map(); + private _openRegionsByWidth: Map = new Map(); + + readonly glyphMap: TwoKeyMap = new TwoKeyMap(); + + private _nextIndex = 0; + + constructor( + private readonly _canvas: OffscreenCanvas, + private readonly _ctx: OffscreenCanvasRenderingContext2D, + ) { + } + + public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { + // Find ideal slab, creating it if there is none suitable + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; + // const dpr = getActiveWindow().devicePixelRatio; + + // TODO: Include font size as well as DPR in nearestXPixels calculation + + // Round slab glyph dimensions to the nearest x pixels, where x scaled with device pixel ratio + // const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5)); + // const nearestXPixels = Math.max(1, Math.floor(dpr)); + const desiredSlabSize = { + // Nearest square number + // TODO: This can probably be optimized + // w: 1 << Math.ceil(Math.sqrt(glyphWidth)), + // h: 1 << Math.ceil(Math.sqrt(glyphHeight)), + + // Nearest x px + // w: Math.ceil(glyphWidth / nearestXPixels) * nearestXPixels, + // h: Math.ceil(glyphHeight / nearestXPixels) * nearestXPixels, + + // Round odd numbers up + // w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth, + // h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight, + + // Exact number only + w: glyphWidth, + h: glyphHeight, + }; + + const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); // this._canvas.width / 8; + const slabH = slabW; // this._canvas.height / 8; + const slabsPerRow = Math.floor(this._canvas.width / slabW); + + // Get any existing slab + let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h); + + // Check if the slab is full + if (slab) { + const glyphsPerSlab = Math.floor(slabW / slab.entryW) * Math.floor(slabH / slab.entryH); + if (slab.count >= glyphsPerSlab) { + slab = undefined; + } + } + + let dx: number | undefined; + let dy: number | undefined; + + // Search for suitable space in unused rectangles + if (!slab) { + // Only check availability for the smallest side + if (glyphWidth < glyphHeight) { + const openRegions = this._openRegionsByWidth.get(glyphWidth); + if (openRegions?.length) { + // TODO: Don't search everything? + // Search from the end so we can typically pop it off the stack + for (let i = openRegions.length - 1; i >= 0; i--) { + const r = openRegions[i]; + if (r.w >= glyphWidth && r.h >= glyphHeight) { + dx = r.x; + dy = r.y; + if (glyphWidth < r.w) { + this._unusedRects.push({ + x: r.x + glyphWidth, + y: r.y, + w: r.w - glyphWidth, + h: glyphHeight + }); + } + r.y += glyphHeight; + r.h -= glyphHeight; + if (r.h === 0) { + if (i === openRegions.length - 1) { + openRegions.pop(); + } else { + this._unusedRects.splice(i, 1); + } + } + break; + } + } + } + } else { + const openRegions = this._openRegionsByHeight.get(glyphHeight); + if (openRegions?.length) { + // TODO: Don't search everything? + // Search from the end so we can typically pop it off the stack + for (let i = openRegions.length - 1; i >= 0; i--) { + const r = openRegions[i]; + if (r.w >= glyphWidth && r.h >= glyphHeight) { + dx = r.x; + dy = r.y; + if (glyphHeight < r.h) { + this._unusedRects.push({ + x: r.x, + y: r.y + glyphHeight, + w: glyphWidth, + h: r.h - glyphHeight + }); + } + r.x += glyphWidth; + r.w -= glyphWidth; + if (r.h === 0) { + if (i === openRegions.length - 1) { + openRegions.pop(); + } else { + this._unusedRects.splice(i, 1); + } + } + break; + } + } + } + } + // for (const [i, r] of this._unusedRects.entries()) { + // if (r.w < r.h) { + // if (r.w >= glyphWidth && r.h >= glyphHeight) { + // dx = r.x; + // dy = r.y; + // if (glyphWidth < r.w) { + // this._unusedRects.push({ + // x: r.x + glyphWidth, + // y: r.y, + // w: r.w - glyphWidth, + // h: glyphHeight + // }); + // } + // r.y += glyphHeight; + // r.h -= glyphHeight; + // if (r.h === 0) { + // // TODO: This is slow + // this._unusedRects.splice(i, 1); + // } + // break; + // } + // } else { + // if (r.w >= glyphWidth && r.h >= glyphHeight) { + // dx = r.x; + // dy = r.y; + // if (glyphHeight < r.h) { + // this._unusedRects.push({ + // x: r.x, + // y: r.y + glyphHeight, + // w: glyphWidth, + // h: r.h - glyphHeight + // }); + // } + // r.x += glyphWidth; + // r.w -= glyphWidth; + // if (r.w === 0) { + // // TODO: This is slow + // this._unusedRects.splice(i, 1); + // } + // } + // } + // } + } + + // Create a new slab + if (dx === undefined || dy === undefined) { + if (!slab) { + slab = { + x: Math.floor(this._slabs.length % slabsPerRow) * slabW, + y: Math.floor(this._slabs.length / slabsPerRow) * slabH, + entryW: desiredSlabSize.w, + entryH: desiredSlabSize.h, + count: 0 + }; + // Track unused regions to use for small glyphs + // +-------------+----+ + // | | | + // | | | <- Unused W region + // | | | + // |-------------+----+ + // | | <- Unused H region + // +------------------+ + const unusedW = slabW % slab.entryW; + const unusedH = slabH % slab.entryH; + if (unusedW) { + addEntryToMapArray(this._openRegionsByWidth, unusedW, { + x: slab.x + slabW - unusedW, + w: unusedW, + y: slab.y, + h: slabH - (unusedH ?? 0) + }); + } + if (unusedH) { + addEntryToMapArray(this._openRegionsByHeight, unusedH, { + x: slab.x, + w: slabW, + y: slab.y + slabH - unusedH, + h: unusedH + }); + } + this._slabs.push(slab); + this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); + } + + const glyphsPerRow = Math.floor(slabW / slab.entryW); + dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW; + dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH; + + // Shift current row + slab.count++; + } + + // Draw glyph + // TODO: Prefer putImageData as it doesn't do blending or scaling + this._ctx.drawImage( + rasterizedGlyph.source, + // source + rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.top, + glyphWidth, + glyphHeight, + // destination + dx, + dy, + glyphWidth, + glyphHeight + ); + + // Create glyph object + const glyph: ITextureAtlasGlyph = { + index: this._nextIndex++, + x: dx, + y: dy, + w: glyphWidth, + h: glyphHeight, + originOffsetX: rasterizedGlyph.originOffset.x, + originOffsetY: rasterizedGlyph.originOffset.y + }; + + // Set the glyph + this.glyphMap.set(chars, tokenFg, glyph); + + return glyph; + } + + public getUsagePreview(): Promise { + const w = this._canvas.width; + const h = this._canvas.height; + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + + ctx.fillStyle = '#808080'; + ctx.fillRect(0, 0, w, h); + + let slabEntryPixels = 0; + let usedPixels = 0; + let slabEdgePixels = 0; + let wastedPixels = 0; + let restrictedPixels = 0; + const totalPixels = w * h; + const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); + const slabH = slabW; + + // Draw wasted underneath glyphs first + for (const slab of this._slabs) { + let x = 0; + let y = 0; + for (let i = 0; i < slab.count; i++) { + if (x + slab.entryW > slabW) { + x = 0; + y += slab.entryH; + } + // TODO: This doesn't visualize wasted space between entries - draw glyphs on top? + ctx.fillStyle = '#FF0000'; + ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH); + + slabEntryPixels += slab.entryW * slab.entryH; + x += slab.entryW; + } + const entriesPerRow = Math.floor(slabW / slab.entryW); + const entriesPerCol = Math.floor(slabH / slab.entryH); + const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol; + slabEdgePixels += (slabW * slabH) - thisSlabPixels; + } + + // Draw glyphs + for (const g of this.glyphMap.values()) { + usedPixels += g.w * g.h; + ctx.fillStyle = '#4040FF'; + ctx.fillRect(g.x, g.y, g.w, g.h); + } + + // Draw unused space on side + const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat()); + for (const r of unusedRegions) { + ctx.fillStyle = '#FF000080'; + ctx.fillRect(r.x, r.y, r.w, r.h); + restrictedPixels += r.w * r.h; + } + + const edgeUsedPixels = slabEdgePixels - restrictedPixels; + console.log({ edgeUsedPixels, slabEdgePixels, restrictedPixels }); + wastedPixels = slabEntryPixels - (usedPixels - edgeUsedPixels); + + + // Overlay actual glyphs on top + ctx.globalAlpha = 0.5; + ctx.drawImage(this._canvas, 0, 0); + ctx.globalAlpha = 1; + + // usedPixels += slabEdgePixels - restrictedPixels; + const efficiency = usedPixels / (usedPixels + wastedPixels + restrictedPixels); + + // Report stats + console.log([ + `Texture atlas stats:`, + ` Total: ${totalPixels}px`, + ` Used: ${usedPixels}px (${((usedPixels / totalPixels) * 100).toFixed(2)}%)`, + ` Wasted: ${wastedPixels}px (${((wastedPixels / totalPixels) * 100).toFixed(2)}%)`, + `Restricted: ${restrictedPixels}px (${((restrictedPixels / totalPixels) * 100).toFixed(2)}%) (hard to allocate)`, + `Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toFixed(2)}%`, + ` Slabs: ${this._slabs.length} of ${Math.floor(this._canvas.width / slabW) * Math.floor(this._canvas.height / slabH)}` + ].join('\n')); + + return canvas.convertToBlob(); + } +} + +interface ITextureAtlasSlab { + x: number; + y: number; + entryH: number; + entryW: number; + count: number; +} + +export interface ITextureAtlasSlabUnusedRect { + x: number; + y: number; + w: number; + h: number; +} + +function addEntryToMapArray(map: Map, key: K, entry: V) { + let list = map.get(key); + if (!list) { + list = []; + map.set(key, list); + } + list.push(entry); +} + +// #endregion diff --git a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts similarity index 97% rename from src/vs/editor/browser/view/gpu/textureAtlasPage.ts rename to src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index ac684b36ef8..9941665ff39 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -6,10 +6,10 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/glyphRasterizer'; +import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; -import { ITextureAtlasAllocator, TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/textureAtlasAllocator'; +import { ITextureAtlasAllocator, TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasAllocator'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 0d62f45cfa0..1f7d8a3513b 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -7,7 +7,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { VSBuffer } from 'vs/base/common/buffer'; import { debounce } from 'vs/base/common/decorators'; import { URI } from 'vs/base/common/uri'; -import { TextureAtlas, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; +import { TextureAtlas, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; diff --git a/src/vs/editor/browser/view/gpu/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts similarity index 100% rename from src/vs/editor/browser/view/gpu/glyphRasterizer.ts rename to src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts index 136e2e6fd28..31fb76852c5 100644 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from 'vs/base/browser/dom'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/glyphRasterizer'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; -import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/textureAtlas'; +import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; export interface ITextureAtlasAllocator { readonly glyphMap: TwoKeyMap; From f19297acbf5b8d657072e6057a665500c6380054 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Aug 2024 06:50:21 -0700 Subject: [PATCH 064/286] Simplify TwiKeyMap.get impl --- src/vs/editor/browser/view/gpu/multiKeyMap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/browser/view/gpu/multiKeyMap.ts b/src/vs/editor/browser/view/gpu/multiKeyMap.ts index 52e49db234c..82012542d43 100644 --- a/src/vs/editor/browser/view/gpu/multiKeyMap.ts +++ b/src/vs/editor/browser/view/gpu/multiKeyMap.ts @@ -19,7 +19,7 @@ export class TwoKeyMap Date: Tue, 13 Aug 2024 07:01:02 -0700 Subject: [PATCH 065/286] Move multi key maps into maps.ts --- src/vs/base/common/map.ts | 49 ++++++++++++++ src/vs/base/test/common/map.test.ts | 64 ++++++++++++++++++- .../view/gpu/atlas/textureAtlasAllocator.ts | 2 +- .../view/gpu/atlas/textureAtlasPage.ts | 2 +- src/vs/editor/browser/view/gpu/multiKeyMap.ts | 58 ----------------- 5 files changed, 113 insertions(+), 62 deletions(-) delete mode 100644 src/vs/editor/browser/view/gpu/multiKeyMap.ts diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 21441069af0..e8bc3445c0b 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -875,3 +875,52 @@ export function mapsStrictEqualIgnoreOrder(a: Map, b: Map { + private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {}; + + public set(first: TFirst, second: TSecond, value: TValue): void { + if (!this._data[first]) { + this._data[first] = {}; + } + this._data[first as string | number]![second] = value; + } + + public get(first: TFirst, second: TSecond): TValue | undefined { + return this._data[first as string | number]?.[second]; + } + + public clear(): void { + this._data = {}; + } + + public *values(): IterableIterator { + for (const first in this._data) { + for (const second in this._data[first]) { + const value = this._data[first]![second]; + if (value) { + yield value; + } + } + } + } +} + +export class FourKeyMap { + private _data: TwoKeyMap> = new TwoKeyMap(); + + public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void { + if (!this._data.get(first, second)) { + this._data.set(first, second, new TwoKeyMap()); + } + this._data.get(first, second)!.set(third, fourth, value); + } + + public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined { + return this._data.get(first, second)?.get(third, fourth); + } + + public clear(): void { + this._data.clear(); + } +} diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index f837920f1f9..6b4fa47e1e6 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { BidirectionalMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, Touch } from 'vs/base/common/map'; +import { BidirectionalMap, FourKeyMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, Touch, TwoKeyMap } from 'vs/base/common/map'; import { extUriIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; @@ -681,5 +681,65 @@ suite('SetMap', () => { const setMap = new SetMap(); assert.deepStrictEqual([...setMap.get('a')], []); }); - +}); + +suite('TwoKeyMap', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('set and get', () => { + const map = new TwoKeyMap(); + map.set('a', 'b', 1); + map.set('a', 'c', 2); + map.set('b', 'c', 3); + assert.strictEqual(map.get('a', 'b'), 1); + assert.strictEqual(map.get('a', 'c'), 2); + assert.strictEqual(map.get('b', 'c'), 3); + assert.strictEqual(map.get('a', 'd'), undefined); + }); + + test('clear', () => { + const map = new TwoKeyMap(); + map.set('a', 'b', 1); + map.set('a', 'c', 2); + map.set('b', 'c', 3); + map.clear(); + assert.strictEqual(map.get('a', 'b'), undefined); + assert.strictEqual(map.get('a', 'c'), undefined); + assert.strictEqual(map.get('b', 'c'), undefined); + }); + + test('values', () => { + const map = new TwoKeyMap(); + map.set('a', 'b', 1); + map.set('a', 'c', 2); + map.set('b', 'c', 3); + assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]); + }); +}); + + +suite('FourKeyMap', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('set and get', () => { + const map = new FourKeyMap(); + map.set('a', 'b', 'c', 'd', 1); + map.set('a', 'c', 'c', 'd', 2); + map.set('b', 'e', 'f', 'g', 3); + assert.strictEqual(map.get('a', 'b', 'c', 'd'), 1); + assert.strictEqual(map.get('a', 'c', 'c', 'd'), 2); + assert.strictEqual(map.get('b', 'e', 'f', 'g'), 3); + assert.strictEqual(map.get('a', 'b', 'c', 'a'), undefined); + }); + + test('clear', () => { + const map = new FourKeyMap(); + map.set('a', 'b', 'c', 'd', 1); + map.set('a', 'c', 'c', 'd', 2); + map.set('b', 'e', 'f', 'g', 3); + map.clear(); + assert.strictEqual(map.get('a', 'b', 'c', 'd'), undefined); + assert.strictEqual(map.get('a', 'c', 'c', 'd'), undefined); + assert.strictEqual(map.get('b', 'e', 'f', 'g'), undefined); + }); }); diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts index 31fb76852c5..16c0d8b39d0 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts @@ -6,8 +6,8 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { TwoKeyMap } from 'vs/base/common/map'; export interface ITextureAtlasAllocator { readonly glyphMap: TwoKeyMap; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 9941665ff39..e8e0e7a03d6 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -8,10 +8,10 @@ import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; import { ITextureAtlasAllocator, TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasAllocator'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { TwoKeyMap } from 'vs/base/common/map'; export class TextureAtlasPage extends Disposable { diff --git a/src/vs/editor/browser/view/gpu/multiKeyMap.ts b/src/vs/editor/browser/view/gpu/multiKeyMap.ts deleted file mode 100644 index 82012542d43..00000000000 --- a/src/vs/editor/browser/view/gpu/multiKeyMap.ts +++ /dev/null @@ -1,58 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Copyright (c) 2022 The xterm.js authors. All rights reserved. - * @license MIT - */ - -export class TwoKeyMap { - private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {}; - - public set(first: TFirst, second: TSecond, value: TValue): void { - if (!this._data[first]) { - this._data[first] = {}; - } - this._data[first as string | number]![second] = value; - } - - public get(first: TFirst, second: TSecond): TValue | undefined { - return this._data[first as string | number]?.[second]; - } - - public clear(): void { - this._data = {}; - } - - public *values() { - for (const first in this._data) { - for (const second in this._data[first]) { - const value = this._data[first]![second]; - if (value) { - yield value; - } - } - } - } -} - -export class FourKeyMap { - private _data: TwoKeyMap> = new TwoKeyMap(); - - public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void { - if (!this._data.get(first, second)) { - this._data.set(first, second, new TwoKeyMap()); - } - this._data.get(first, second)!.set(third, fourth, value); - } - - public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined { - return this._data.get(first, second)?.get(third, fourth); - } - - public clear(): void { - this._data.clear(); - } -} From 45feb8c9e43964dd9f16c40745c0d990e707ca55 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 13 Aug 2024 07:06:23 -0700 Subject: [PATCH 066/286] Allocator docs --- .../browser/view/gpu/atlas/textureAtlasAllocator.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts index 16c0d8b39d0..3f1914f532c 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts @@ -17,6 +17,10 @@ export interface ITextureAtlasAllocator { // #region Shelf allocator +/** + * The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the + * current row is full. Due to its simplicity, it can waste space but it is very fast. + */ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { private _currentRow: ITextureAtlasShelf = { x: 0, @@ -139,6 +143,15 @@ interface ITextureAtlasShelf { // #region Slab allocator +/** + * The slab allocator is a more complex allocator that places glyphs in square slabs of a fixed + * size. Slabs are defined by a small range of glyphs sizes they can house, this places like-sized + * glyphs in the same slab which reduces wasted space. + * + * Slabs also may contain "unused" regions on the left and bottom depending on the size of the + * glyphs they include. This space is used to place very thin or short glyphs, which would otherwise + * waste a lot of space in their own slab. + */ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // TODO: Is there a better way to index slabs other than an unsorted list? private _slabs: ITextureAtlasSlab[] = []; From 3b679508530e4d821a993a93287f84de0d3d787d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:16:28 -0700 Subject: [PATCH 067/286] Add unit testsf or TextureAtlasShelfAllocator --- .../view/gpu/atlas/textureAtlasAllocator.ts | 2 +- .../browser/view/gpu/textureAtlasAllocator.ts | 508 ------------------ .../gpu/atlas/textureAtlasAllocator.test.ts | 142 +++++ 3 files changed, 143 insertions(+), 509 deletions(-) delete mode 100644 src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts create mode 100644 src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts index 3f1914f532c..3d34fa29d54 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts @@ -43,7 +43,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { // Finalize row if it doesn't fix - if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left > this._canvas.width - this._currentRow.x) { + if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) { this._currentRow.x = 0; this._currentRow.y += this._currentRow.h; this._currentRow.h = 1; diff --git a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts deleted file mode 100644 index 31fb76852c5..00000000000 --- a/src/vs/editor/browser/view/gpu/textureAtlasAllocator.ts +++ /dev/null @@ -1,508 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getActiveWindow } from 'vs/base/browser/dom'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { TwoKeyMap } from 'vs/editor/browser/view/gpu/multiKeyMap'; -import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; - -export interface ITextureAtlasAllocator { - readonly glyphMap: TwoKeyMap; - allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph; - getUsagePreview(): Promise; -} - -// #region Shelf allocator - -export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { - private _currentRow: ITextureAtlasShelf = { - x: 0, - y: 0, - h: 0 - }; - - // TODO: Allow for multiple active rows - // public readonly fixedRows: ICharAtlasActiveRow[] = []; - - readonly glyphMap: TwoKeyMap = new TwoKeyMap(); - - private _nextIndex = 0; - - constructor( - private readonly _canvas: OffscreenCanvas, - private readonly _ctx: OffscreenCanvasRenderingContext2D, - ) { - } - - public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { - // Finalize row if it doesn't fix - if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left > this._canvas.width - this._currentRow.x) { - this._currentRow.x = 0; - this._currentRow.y += this._currentRow.h; - this._currentRow.h = 1; - } - - // TODO: Handle end of atlas page - - // Draw glyph - const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; - const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; - this._ctx.drawImage( - rasterizedGlyph.source, - // source - rasterizedGlyph.boundingBox.left, - rasterizedGlyph.boundingBox.top, - glyphWidth, - glyphHeight, - // destination - this._currentRow.x, - this._currentRow.y, - glyphWidth, - glyphHeight - ); - - // Create glyph object - const glyph: ITextureAtlasGlyph = { - index: this._nextIndex++, - x: this._currentRow.x, - y: this._currentRow.y, - w: glyphWidth, - h: glyphHeight, - originOffsetX: rasterizedGlyph.originOffset.x, - originOffsetY: rasterizedGlyph.originOffset.y - }; - - // Shift current row - this._currentRow.x += glyphWidth; - this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); - - // Set the glyph - this.glyphMap.set(chars, tokenFg, glyph); - - return glyph; - } - - public getUsagePreview(): Promise { - const w = this._canvas.width; - const h = this._canvas.height; - const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - ctx.fillStyle = '#808080'; - ctx.fillRect(0, 0, w, h); - - let usedPixels = 0; - let wastedPixels = 0; - const totalPixels = w * h; - - const rowHeight: Map = new Map(); // y -> h - const rowWidth: Map = new Map(); // y -> w - for (const g of this.glyphMap.values()) { - rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); - rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); - } - for (const g of this.glyphMap.values()) { - usedPixels += g.w * g.h; - wastedPixels += g.w * (rowHeight.get(g.y)! - g.h); - ctx.fillStyle = '#4040FF'; - ctx.fillRect(g.x, g.y, g.w, g.h); - ctx.fillStyle = '#FF0000'; - ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); - } - for (const [rowY, rowW] of rowWidth.entries()) { - if (rowY !== this._currentRow.y) { - ctx.fillStyle = '#FF0000'; - ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); - wastedPixels += (w - rowW) * rowHeight.get(rowY)!; - } - } - console.log([ - `Texture atlas stats:`, - ` Total: ${totalPixels}`, - ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, - ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, - `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, - ].join('\n')); - return canvas.convertToBlob(); - } -} - -interface ITextureAtlasShelf { - x: number; - y: number; - h: number; -} - -// #endregion - -// #region Slab allocator - -export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { - // TODO: Is there a better way to index slabs other than an unsorted list? - private _slabs: ITextureAtlasSlab[] = []; - private _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); - - private _unusedRects: ITextureAtlasSlabUnusedRect[] = []; - - private _openRegionsByHeight: Map = new Map(); - private _openRegionsByWidth: Map = new Map(); - - readonly glyphMap: TwoKeyMap = new TwoKeyMap(); - - private _nextIndex = 0; - - constructor( - private readonly _canvas: OffscreenCanvas, - private readonly _ctx: OffscreenCanvasRenderingContext2D, - ) { - } - - public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { - // Find ideal slab, creating it if there is none suitable - const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; - const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; - // const dpr = getActiveWindow().devicePixelRatio; - - // TODO: Include font size as well as DPR in nearestXPixels calculation - - // Round slab glyph dimensions to the nearest x pixels, where x scaled with device pixel ratio - // const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5)); - // const nearestXPixels = Math.max(1, Math.floor(dpr)); - const desiredSlabSize = { - // Nearest square number - // TODO: This can probably be optimized - // w: 1 << Math.ceil(Math.sqrt(glyphWidth)), - // h: 1 << Math.ceil(Math.sqrt(glyphHeight)), - - // Nearest x px - // w: Math.ceil(glyphWidth / nearestXPixels) * nearestXPixels, - // h: Math.ceil(glyphHeight / nearestXPixels) * nearestXPixels, - - // Round odd numbers up - // w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth, - // h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight, - - // Exact number only - w: glyphWidth, - h: glyphHeight, - }; - - const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); // this._canvas.width / 8; - const slabH = slabW; // this._canvas.height / 8; - const slabsPerRow = Math.floor(this._canvas.width / slabW); - - // Get any existing slab - let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h); - - // Check if the slab is full - if (slab) { - const glyphsPerSlab = Math.floor(slabW / slab.entryW) * Math.floor(slabH / slab.entryH); - if (slab.count >= glyphsPerSlab) { - slab = undefined; - } - } - - let dx: number | undefined; - let dy: number | undefined; - - // Search for suitable space in unused rectangles - if (!slab) { - // Only check availability for the smallest side - if (glyphWidth < glyphHeight) { - const openRegions = this._openRegionsByWidth.get(glyphWidth); - if (openRegions?.length) { - // TODO: Don't search everything? - // Search from the end so we can typically pop it off the stack - for (let i = openRegions.length - 1; i >= 0; i--) { - const r = openRegions[i]; - if (r.w >= glyphWidth && r.h >= glyphHeight) { - dx = r.x; - dy = r.y; - if (glyphWidth < r.w) { - this._unusedRects.push({ - x: r.x + glyphWidth, - y: r.y, - w: r.w - glyphWidth, - h: glyphHeight - }); - } - r.y += glyphHeight; - r.h -= glyphHeight; - if (r.h === 0) { - if (i === openRegions.length - 1) { - openRegions.pop(); - } else { - this._unusedRects.splice(i, 1); - } - } - break; - } - } - } - } else { - const openRegions = this._openRegionsByHeight.get(glyphHeight); - if (openRegions?.length) { - // TODO: Don't search everything? - // Search from the end so we can typically pop it off the stack - for (let i = openRegions.length - 1; i >= 0; i--) { - const r = openRegions[i]; - if (r.w >= glyphWidth && r.h >= glyphHeight) { - dx = r.x; - dy = r.y; - if (glyphHeight < r.h) { - this._unusedRects.push({ - x: r.x, - y: r.y + glyphHeight, - w: glyphWidth, - h: r.h - glyphHeight - }); - } - r.x += glyphWidth; - r.w -= glyphWidth; - if (r.h === 0) { - if (i === openRegions.length - 1) { - openRegions.pop(); - } else { - this._unusedRects.splice(i, 1); - } - } - break; - } - } - } - } - // for (const [i, r] of this._unusedRects.entries()) { - // if (r.w < r.h) { - // if (r.w >= glyphWidth && r.h >= glyphHeight) { - // dx = r.x; - // dy = r.y; - // if (glyphWidth < r.w) { - // this._unusedRects.push({ - // x: r.x + glyphWidth, - // y: r.y, - // w: r.w - glyphWidth, - // h: glyphHeight - // }); - // } - // r.y += glyphHeight; - // r.h -= glyphHeight; - // if (r.h === 0) { - // // TODO: This is slow - // this._unusedRects.splice(i, 1); - // } - // break; - // } - // } else { - // if (r.w >= glyphWidth && r.h >= glyphHeight) { - // dx = r.x; - // dy = r.y; - // if (glyphHeight < r.h) { - // this._unusedRects.push({ - // x: r.x, - // y: r.y + glyphHeight, - // w: glyphWidth, - // h: r.h - glyphHeight - // }); - // } - // r.x += glyphWidth; - // r.w -= glyphWidth; - // if (r.w === 0) { - // // TODO: This is slow - // this._unusedRects.splice(i, 1); - // } - // } - // } - // } - } - - // Create a new slab - if (dx === undefined || dy === undefined) { - if (!slab) { - slab = { - x: Math.floor(this._slabs.length % slabsPerRow) * slabW, - y: Math.floor(this._slabs.length / slabsPerRow) * slabH, - entryW: desiredSlabSize.w, - entryH: desiredSlabSize.h, - count: 0 - }; - // Track unused regions to use for small glyphs - // +-------------+----+ - // | | | - // | | | <- Unused W region - // | | | - // |-------------+----+ - // | | <- Unused H region - // +------------------+ - const unusedW = slabW % slab.entryW; - const unusedH = slabH % slab.entryH; - if (unusedW) { - addEntryToMapArray(this._openRegionsByWidth, unusedW, { - x: slab.x + slabW - unusedW, - w: unusedW, - y: slab.y, - h: slabH - (unusedH ?? 0) - }); - } - if (unusedH) { - addEntryToMapArray(this._openRegionsByHeight, unusedH, { - x: slab.x, - w: slabW, - y: slab.y + slabH - unusedH, - h: unusedH - }); - } - this._slabs.push(slab); - this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); - } - - const glyphsPerRow = Math.floor(slabW / slab.entryW); - dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW; - dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH; - - // Shift current row - slab.count++; - } - - // Draw glyph - // TODO: Prefer putImageData as it doesn't do blending or scaling - this._ctx.drawImage( - rasterizedGlyph.source, - // source - rasterizedGlyph.boundingBox.left, - rasterizedGlyph.boundingBox.top, - glyphWidth, - glyphHeight, - // destination - dx, - dy, - glyphWidth, - glyphHeight - ); - - // Create glyph object - const glyph: ITextureAtlasGlyph = { - index: this._nextIndex++, - x: dx, - y: dy, - w: glyphWidth, - h: glyphHeight, - originOffsetX: rasterizedGlyph.originOffset.x, - originOffsetY: rasterizedGlyph.originOffset.y - }; - - // Set the glyph - this.glyphMap.set(chars, tokenFg, glyph); - - return glyph; - } - - public getUsagePreview(): Promise { - const w = this._canvas.width; - const h = this._canvas.height; - const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - - ctx.fillStyle = '#808080'; - ctx.fillRect(0, 0, w, h); - - let slabEntryPixels = 0; - let usedPixels = 0; - let slabEdgePixels = 0; - let wastedPixels = 0; - let restrictedPixels = 0; - const totalPixels = w * h; - const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); - const slabH = slabW; - - // Draw wasted underneath glyphs first - for (const slab of this._slabs) { - let x = 0; - let y = 0; - for (let i = 0; i < slab.count; i++) { - if (x + slab.entryW > slabW) { - x = 0; - y += slab.entryH; - } - // TODO: This doesn't visualize wasted space between entries - draw glyphs on top? - ctx.fillStyle = '#FF0000'; - ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH); - - slabEntryPixels += slab.entryW * slab.entryH; - x += slab.entryW; - } - const entriesPerRow = Math.floor(slabW / slab.entryW); - const entriesPerCol = Math.floor(slabH / slab.entryH); - const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol; - slabEdgePixels += (slabW * slabH) - thisSlabPixels; - } - - // Draw glyphs - for (const g of this.glyphMap.values()) { - usedPixels += g.w * g.h; - ctx.fillStyle = '#4040FF'; - ctx.fillRect(g.x, g.y, g.w, g.h); - } - - // Draw unused space on side - const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat()); - for (const r of unusedRegions) { - ctx.fillStyle = '#FF000080'; - ctx.fillRect(r.x, r.y, r.w, r.h); - restrictedPixels += r.w * r.h; - } - - const edgeUsedPixels = slabEdgePixels - restrictedPixels; - console.log({ edgeUsedPixels, slabEdgePixels, restrictedPixels }); - wastedPixels = slabEntryPixels - (usedPixels - edgeUsedPixels); - - - // Overlay actual glyphs on top - ctx.globalAlpha = 0.5; - ctx.drawImage(this._canvas, 0, 0); - ctx.globalAlpha = 1; - - // usedPixels += slabEdgePixels - restrictedPixels; - const efficiency = usedPixels / (usedPixels + wastedPixels + restrictedPixels); - - // Report stats - console.log([ - `Texture atlas stats:`, - ` Total: ${totalPixels}px`, - ` Used: ${usedPixels}px (${((usedPixels / totalPixels) * 100).toFixed(2)}%)`, - ` Wasted: ${wastedPixels}px (${((wastedPixels / totalPixels) * 100).toFixed(2)}%)`, - `Restricted: ${restrictedPixels}px (${((restrictedPixels / totalPixels) * 100).toFixed(2)}%) (hard to allocate)`, - `Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toFixed(2)}%`, - ` Slabs: ${this._slabs.length} of ${Math.floor(this._canvas.width / slabW) * Math.floor(this._canvas.height / slabH)}` - ].join('\n')); - - return canvas.convertToBlob(); - } -} - -interface ITextureAtlasSlab { - x: number; - y: number; - entryH: number; - entryW: number; - count: number; -} - -export interface ITextureAtlasSlabUnusedRect { - x: number; - y: number; - w: number; - h: number; -} - -function addEntryToMapArray(map: Map, key: K, entry: V) { - let list = map.get(key); - if (!list) { - list = []; - map.set(key, list); - } - list.push(entry); -} - -// #endregion diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts new file mode 100644 index 00000000000..1fdaa0679aa --- /dev/null +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, strictEqual } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasAllocator'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; + +suite('TextureAtlasShelfAllocator', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const blackInt = 0x000000FF; + const blackArr = [0x00, 0x00, 0x00, 0xFF]; + + const pixel1x1 = createRasterizedGlyph(1, 1, [...blackArr]); + const pixel2x1 = createRasterizedGlyph(2, 1, [...blackArr, ...blackArr]); + const pixel1x2 = createRasterizedGlyph(1, 2, [...blackArr, ...blackArr]); + + let canvas: OffscreenCanvas; + let ctx: OffscreenCanvasRenderingContext2D; + let allocator: TextureAtlasShelfAllocator; + + let lastUniqueGlyph: string; + function getUniqueGlyphId(): [string, number] { + if (!lastUniqueGlyph) { + lastUniqueGlyph = 'a'; + } else { + lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); + } + return [lastUniqueGlyph, blackInt]; + } + + setup(() => { + canvas = new OffscreenCanvas(5, 5); + ctx = ensureNonNullable(canvas.getContext('2d')); + allocator = new TextureAtlasShelfAllocator(canvas, ctx); + }); + + test('single allocation', () => { + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { + index: 0, + x: 0, y: 0, + w: 1, h: 1, + originOffsetX: 0, originOffsetY: 0, + }); + }); + test('wrapping', () => { + // 1oooo + // ooooo + // ooooo + // ooooo + // ooooo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { + index: 0, + x: 0, y: 0, + w: 1, h: 1, + originOffsetX: 0, originOffsetY: 0, + }); + // 12ooo + // o2ooo + // ooooo + // ooooo + // ooooo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x2), { + index: 1, + x: 1, y: 0, + w: 1, h: 2, + originOffsetX: 0, originOffsetY: 0, + }); + // 1233o + // o2ooo + // ooooo + // ooooo + // ooooo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { + index: 2, + x: 2, y: 0, + w: 2, h: 1, + originOffsetX: 0, originOffsetY: 0, + }); + // 1233x + // x2xxx + // 44ooo + // ooooo + // ooooo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { + index: 3, + x: 0, y: 2, + w: 2, h: 1, + originOffsetX: 0, originOffsetY: 0, + }, 'should wrap to next line as there\'s no room left'); + // 1233x + // x2xxx + // 4455o + // ooooo + // ooooo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { + index: 4, + x: 2, y: 2, + w: 2, h: 1, + originOffsetX: 0, originOffsetY: 0, + }); + // 1233x + // x2xxx + // 44556 + // ooooo + // ooooo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { + index: 5, + x: 4, y: 2, + w: 1, h: 1, + originOffsetX: 0, originOffsetY: 0, + }); + // 1233x + // x2xxx + // 44556 + // 7oooo + // ooooo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { + index: 6, + x: 0, y: 3, + w: 1, h: 1, + originOffsetX: 0, originOffsetY: 0, + }); + }); +}); + +function createRasterizedGlyph(w: number, h: number, data: ArrayLike): IRasterizedGlyph { + strictEqual(w * h * 4, data.length); + const source = new OffscreenCanvas(w, h); + const imageData = new ImageData(w, h); + imageData.data.set(data); + ensureNonNullable(source.getContext('2d')).putImageData(imageData, 0, 0); + return { + source, + boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, + originOffset: { x: 0, y: 0 }, + }; +} From c10e865bd96dcb532121c164c496987207ee012f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:31:42 -0700 Subject: [PATCH 068/286] Return undefined from shelf allocator when there's no room --- .../view/gpu/atlas/textureAtlasAllocator.ts | 21 +++++--- .../view/gpu/atlas/textureAtlasPage.ts | 3 +- .../gpu/atlas/textureAtlasAllocator.test.ts | 50 +++++++++++++------ 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts index 3d34fa29d54..aa1c0c74421 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts @@ -11,7 +11,14 @@ import { TwoKeyMap } from 'vs/base/common/map'; export interface ITextureAtlasAllocator { readonly glyphMap: TwoKeyMap; - allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph; + /** + * Allocates a rasterized glyph to the canvas, drawing it and returning information on its + * position in the canvas. This will return undefined if the glyph does not fit on the canvas. + */ + allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined; + /** + * Gets a usage preview of the atlas for debugging purposes. + */ getUsagePreview(): Promise; } @@ -28,9 +35,6 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { h: 0 }; - // TODO: Allow for multiple active rows - // public readonly fixedRows: ICharAtlasActiveRow[] = []; - readonly glyphMap: TwoKeyMap = new TwoKeyMap(); private _nextIndex = 0; @@ -41,15 +45,18 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { ) { } - public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { - // Finalize row if it doesn't fix + public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined { + // Finalize and increment row if it doesn't fix horizontally if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) { this._currentRow.x = 0; this._currentRow.y += this._currentRow.h; this._currentRow.h = 1; } - // TODO: Handle end of atlas page + // Return undefined if there isn't any room left + if (this._currentRow.y + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1 > this._canvas.height) { + return undefined; + } // Draw glyph const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index e8e0e7a03d6..26bde254865 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -79,7 +79,8 @@ export class TextureAtlasPage extends Disposable { private _createGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg]); - const glyph = this._allocator.allocate(chars, tokenFg, rasterizedGlyph); + // TODO: Handle undefined allocate result + const glyph = this._allocator.allocate(chars, tokenFg, rasterizedGlyph)!; this._glyphMap.set(chars, tokenFg, glyph); this._glyphInOrderSet.add(glyph); this.hasChanges = true; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 1fdaa0679aa..bef2fba9f87 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -19,11 +19,7 @@ suite('TextureAtlasShelfAllocator', () => { const pixel2x1 = createRasterizedGlyph(2, 1, [...blackArr, ...blackArr]); const pixel1x2 = createRasterizedGlyph(1, 2, [...blackArr, ...blackArr]); - let canvas: OffscreenCanvas; - let ctx: OffscreenCanvasRenderingContext2D; - let allocator: TextureAtlasShelfAllocator; - - let lastUniqueGlyph: string; + let lastUniqueGlyph: string | undefined; function getUniqueGlyphId(): [string, number] { if (!lastUniqueGlyph) { lastUniqueGlyph = 'a'; @@ -33,13 +29,14 @@ suite('TextureAtlasShelfAllocator', () => { return [lastUniqueGlyph, blackInt]; } - setup(() => { - canvas = new OffscreenCanvas(5, 5); - ctx = ensureNonNullable(canvas.getContext('2d')); - allocator = new TextureAtlasShelfAllocator(canvas, ctx); + suiteSetup(() => { + lastUniqueGlyph = undefined; }); test('single allocation', () => { + const { allocator } = initAllocator(2, 2); + // 1o + // oo deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { index: 0, x: 0, y: 0, @@ -48,11 +45,11 @@ suite('TextureAtlasShelfAllocator', () => { }); }); test('wrapping', () => { + const { allocator } = initAllocator(5, 4); // 1oooo // ooooo // ooooo // ooooo - // ooooo deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { index: 0, x: 0, y: 0, @@ -63,7 +60,6 @@ suite('TextureAtlasShelfAllocator', () => { // o2ooo // ooooo // ooooo - // ooooo deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x2), { index: 1, x: 1, y: 0, @@ -74,7 +70,6 @@ suite('TextureAtlasShelfAllocator', () => { // o2ooo // ooooo // ooooo - // ooooo deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { index: 2, x: 2, y: 0, @@ -85,7 +80,6 @@ suite('TextureAtlasShelfAllocator', () => { // x2xxx // 44ooo // ooooo - // ooooo deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { index: 3, x: 0, y: 2, @@ -96,7 +90,6 @@ suite('TextureAtlasShelfAllocator', () => { // x2xxx // 4455o // ooooo - // ooooo deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { index: 4, x: 2, y: 2, @@ -107,7 +100,6 @@ suite('TextureAtlasShelfAllocator', () => { // x2xxx // 44556 // ooooo - // ooooo deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { index: 5, x: 4, y: 2, @@ -118,16 +110,42 @@ suite('TextureAtlasShelfAllocator', () => { // x2xxx // 44556 // 7oooo - // ooooo deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { index: 6, x: 0, y: 3, w: 1, h: 1, originOffsetX: 0, originOffsetY: 0, + }, 'should wrap to next line as there\'s no room left'); + }); + test('full', () => { + const { allocator } = initAllocator(3, 2); + // 1oo + // 1oo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x2), { + index: 0, + x: 0, y: 0, + w: 1, h: 2, + originOffsetX: 0, originOffsetY: 0, }); + // 122 + // 1oo + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { + index: 1, + x: 1, y: 0, + w: 2, h: 1, + originOffsetX: 0, originOffsetY: 0, + }); + deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), undefined, 'should return undefined when the canvas is full'); }); }); +function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasShelfAllocator } { + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + const allocator = new TextureAtlasShelfAllocator(canvas, ctx); + return { canvas, ctx, allocator }; +} + function createRasterizedGlyph(w: number, h: number, data: ArrayLike): IRasterizedGlyph { strictEqual(w * h * 4, data.length); const source = new OffscreenCanvas(w, h); From e28976abeb7f450399f0c72783aa62685b0b3ad0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:32:29 -0700 Subject: [PATCH 069/286] Move functions/consts to top --- .../gpu/atlas/textureAtlasAllocator.test.ts | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index bef2fba9f87..6bfa6cb0ce9 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -9,16 +9,36 @@ import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/tex import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +const blackInt = 0x000000FF; +const blackArr = [0x00, 0x00, 0x00, 0xFF]; + +const pixel1x1 = createRasterizedGlyph(1, 1, [...blackArr]); +const pixel2x1 = createRasterizedGlyph(2, 1, [...blackArr, ...blackArr]); +const pixel1x2 = createRasterizedGlyph(1, 2, [...blackArr, ...blackArr]); + +function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasShelfAllocator } { + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + const allocator = new TextureAtlasShelfAllocator(canvas, ctx); + return { canvas, ctx, allocator }; +} + +function createRasterizedGlyph(w: number, h: number, data: ArrayLike): IRasterizedGlyph { + strictEqual(w * h * 4, data.length); + const source = new OffscreenCanvas(w, h); + const imageData = new ImageData(w, h); + imageData.data.set(data); + ensureNonNullable(source.getContext('2d')).putImageData(imageData, 0, 0); + return { + source, + boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, + originOffset: { x: 0, y: 0 }, + }; +} + suite('TextureAtlasShelfAllocator', () => { ensureNoDisposablesAreLeakedInTestSuite(); - const blackInt = 0x000000FF; - const blackArr = [0x00, 0x00, 0x00, 0xFF]; - - const pixel1x1 = createRasterizedGlyph(1, 1, [...blackArr]); - const pixel2x1 = createRasterizedGlyph(2, 1, [...blackArr, ...blackArr]); - const pixel1x2 = createRasterizedGlyph(1, 2, [...blackArr, ...blackArr]); - let lastUniqueGlyph: string | undefined; function getUniqueGlyphId(): [string, number] { if (!lastUniqueGlyph) { @@ -138,23 +158,3 @@ suite('TextureAtlasShelfAllocator', () => { deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), undefined, 'should return undefined when the canvas is full'); }); }); - -function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasShelfAllocator } { - const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - const allocator = new TextureAtlasShelfAllocator(canvas, ctx); - return { canvas, ctx, allocator }; -} - -function createRasterizedGlyph(w: number, h: number, data: ArrayLike): IRasterizedGlyph { - strictEqual(w * h * 4, data.length); - const source = new OffscreenCanvas(w, h); - const imageData = new ImageData(w, h); - imageData.data.set(data); - ensureNonNullable(source.getContext('2d')).putImageData(imageData, 0, 0); - return { - source, - boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, - originOffset: { x: 0, y: 0 }, - }; -} From 6efa86ed8f50fa7e58f39f252b2bb1c1c9f0bb22 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:02:49 -0700 Subject: [PATCH 070/286] Some slab allocator tests --- .../view/gpu/atlas/textureAtlasAllocator.ts | 44 ++-- .../gpu/atlas/textureAtlasAllocator.test.ts | 223 +++++++++--------- 2 files changed, 135 insertions(+), 132 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts index aa1c0c74421..2fd2b3fef65 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts @@ -150,6 +150,11 @@ interface ITextureAtlasShelf { // #region Slab allocator +export interface TextureAtlasSlabAllocatorOptions { + slabW?: number; + slabH?: number; +} + /** * The slab allocator is a more complex allocator that places glyphs in square slabs of a fixed * size. Slabs are defined by a small range of glyphs sizes they can house, this places like-sized @@ -171,12 +176,25 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { readonly glyphMap: TwoKeyMap = new TwoKeyMap(); + private readonly _slabW: number; + private readonly _slabH: number; + private readonly _slabsPerRow: number; private _nextIndex = 0; constructor( private readonly _canvas: OffscreenCanvas, private readonly _ctx: OffscreenCanvasRenderingContext2D, + options?: TextureAtlasSlabAllocatorOptions ) { + this._slabW = Math.min( + options?.slabW ?? (64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1)), + this._canvas.width + ); + this._slabH = Math.min( + options?.slabH ?? this._slabW, + this._canvas.height + ); + this._slabsPerRow = Math.floor(this._canvas.width / this._slabW); } public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { @@ -209,16 +227,12 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { h: glyphHeight, }; - const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); // this._canvas.width / 8; - const slabH = slabW; // this._canvas.height / 8; - const slabsPerRow = Math.floor(this._canvas.width / slabW); - // Get any existing slab let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h); // Check if the slab is full if (slab) { - const glyphsPerSlab = Math.floor(slabW / slab.entryW) * Math.floor(slabH / slab.entryH); + const glyphsPerSlab = Math.floor(this._slabW / slab.entryW) * Math.floor(this._slabH / slab.entryH); if (slab.count >= glyphsPerSlab) { slab = undefined; } @@ -340,9 +354,11 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Create a new slab if (dx === undefined || dy === undefined) { if (!slab) { + // TODO: Return undefined if there isn't any room left + slab = { - x: Math.floor(this._slabs.length % slabsPerRow) * slabW, - y: Math.floor(this._slabs.length / slabsPerRow) * slabH, + x: Math.floor(this._slabs.length % this._slabsPerRow) * this._slabW, + y: Math.floor(this._slabs.length / this._slabsPerRow) * this._slabH, entryW: desiredSlabSize.w, entryH: desiredSlabSize.h, count: 0 @@ -355,21 +371,21 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // |-------------+----+ // | | <- Unused H region // +------------------+ - const unusedW = slabW % slab.entryW; - const unusedH = slabH % slab.entryH; + const unusedW = this._slabW % slab.entryW; + const unusedH = this._slabH % slab.entryH; if (unusedW) { addEntryToMapArray(this._openRegionsByWidth, unusedW, { - x: slab.x + slabW - unusedW, + x: slab.x + this._slabW - unusedW, w: unusedW, y: slab.y, - h: slabH - (unusedH ?? 0) + h: this._slabH - (unusedH ?? 0) }); } if (unusedH) { addEntryToMapArray(this._openRegionsByHeight, unusedH, { x: slab.x, - w: slabW, - y: slab.y + slabH - unusedH, + w: this._slabW, + y: slab.y + this._slabH - unusedH, h: unusedH }); } @@ -377,7 +393,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); } - const glyphsPerRow = Math.floor(slabW / slab.entryW); + const glyphsPerRow = Math.floor(this._slabW / slab.entryW); dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW; dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 6bfa6cb0ce9..0468a20aecf 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -5,7 +5,7 @@ import { deepStrictEqual, strictEqual } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasAllocator'; +import { TextureAtlasShelfAllocator, TextureAtlasSlabAllocator, type ITextureAtlasAllocator, type TextureAtlasSlabAllocatorOptions } from 'vs/editor/browser/view/gpu/atlas/textureAtlasAllocator'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -16,13 +16,6 @@ const pixel1x1 = createRasterizedGlyph(1, 1, [...blackArr]); const pixel2x1 = createRasterizedGlyph(2, 1, [...blackArr, ...blackArr]); const pixel1x2 = createRasterizedGlyph(1, 2, [...blackArr, ...blackArr]); -function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasShelfAllocator } { - const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - const allocator = new TextureAtlasShelfAllocator(canvas, ctx); - return { canvas, ctx, allocator }; -} - function createRasterizedGlyph(w: number, h: number, data: ArrayLike): IRasterizedGlyph { strictEqual(w * h * 4, data.length); const source = new OffscreenCanvas(w, h); @@ -36,125 +29,119 @@ function createRasterizedGlyph(w: number, h: number, data: ArrayLike): I }; } -suite('TextureAtlasShelfAllocator', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - let lastUniqueGlyph: string | undefined; - function getUniqueGlyphId(): [string, number] { - if (!lastUniqueGlyph) { - lastUniqueGlyph = 'a'; - } else { - lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); - } - return [lastUniqueGlyph, blackInt]; +let lastUniqueGlyph: string | undefined; +function getUniqueGlyphId(): [string, number] { + if (!lastUniqueGlyph) { + lastUniqueGlyph = 'a'; + } else { + lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); } + return [lastUniqueGlyph, blackInt]; +} + +function allocateAndAssert(allocator: ITextureAtlasAllocator, rasterizedGlyph: IRasterizedGlyph, expected: { x: number; y: number; w: number; h: number } | undefined): void { + const actual = allocator.allocate(...getUniqueGlyphId(), rasterizedGlyph); + if (!actual) { + strictEqual(actual, expected); + return; + } + deepStrictEqual({ + x: actual.x, + y: actual.y, + w: actual.w, + h: actual.h, + }, expected); +} + +suite('TextureAtlasAllocator', () => { + ensureNoDisposablesAreLeakedInTestSuite(); suiteSetup(() => { lastUniqueGlyph = undefined; }); - test('single allocation', () => { - const { allocator } = initAllocator(2, 2); - // 1o - // oo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { - index: 0, - x: 0, y: 0, - w: 1, h: 1, - originOffsetX: 0, originOffsetY: 0, + suite('TextureAtlasShelfAllocator', () => { + function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasShelfAllocator } { + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + const allocator = new TextureAtlasShelfAllocator(canvas, ctx); + return { canvas, ctx, allocator }; + } + + test('single allocation', () => { + const { allocator } = initAllocator(2, 2); + // 1o + // oo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); }); - }); - test('wrapping', () => { - const { allocator } = initAllocator(5, 4); - // 1oooo - // ooooo - // ooooo - // ooooo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { - index: 0, - x: 0, y: 0, - w: 1, h: 1, - originOffsetX: 0, originOffsetY: 0, + test('wrapping', () => { + const { allocator } = initAllocator(5, 4); + + // 1233o + // o2ooo + // ooooo + // ooooo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x2, { x: 1, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel2x1, { x: 2, y: 0, w: 2, h: 1 }); + + // 1233x + // x2xxx + // 44556 + // ooooo + allocateAndAssert(allocator, pixel2x1, { x: 0, y: 2, w: 2, h: 1 }); + allocateAndAssert(allocator, pixel2x1, { x: 2, y: 2, w: 2, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 4, y: 2, w: 1, h: 1 }); + + // 1233x + // x2xxx + // 44556 + // 7oooo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 3, w: 1, h: 1 }); }); - // 12ooo - // o2ooo - // ooooo - // ooooo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x2), { - index: 1, - x: 1, y: 0, - w: 1, h: 2, - originOffsetX: 0, originOffsetY: 0, + test('full', () => { + const { allocator } = initAllocator(3, 2); + // 122 + // 1oo + allocateAndAssert(allocator, pixel1x2, { x: 0, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel2x1, { x: 1, y: 0, w: 2, h: 1 }); + allocateAndAssert(allocator, pixel1x1, undefined); }); - // 1233o - // o2ooo - // ooooo - // ooooo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { - index: 2, - x: 2, y: 0, - w: 2, h: 1, - originOffsetX: 0, originOffsetY: 0, + + suite('TextureAtlasSlabAllocator', () => { + function initAllocator(w: number, h: number, options?: TextureAtlasSlabAllocatorOptions): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasSlabAllocator } { + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + const allocator = new TextureAtlasSlabAllocator(canvas, ctx, options); + return { canvas, ctx, allocator }; + } + + test('single allocation', () => { + const { allocator } = initAllocator(2, 2); + // 1o + // oo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + }); + + test('allocate 1x1 to multiple slabs until full', () => { + const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); + + // 12│oo + // 34│oo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 1, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 1, w: 1, h: 1 }); + + // 12│56 + // 34│78 + allocateAndAssert(allocator, pixel1x1, { x: 2, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 3, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 2, y: 1, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 3, y: 1, w: 1, h: 1 }); + + allocateAndAssert(allocator, pixel1x1, undefined); + }); }); - // 1233x - // x2xxx - // 44ooo - // ooooo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { - index: 3, - x: 0, y: 2, - w: 2, h: 1, - originOffsetX: 0, originOffsetY: 0, - }, 'should wrap to next line as there\'s no room left'); - // 1233x - // x2xxx - // 4455o - // ooooo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { - index: 4, - x: 2, y: 2, - w: 2, h: 1, - originOffsetX: 0, originOffsetY: 0, - }); - // 1233x - // x2xxx - // 44556 - // ooooo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { - index: 5, - x: 4, y: 2, - w: 1, h: 1, - originOffsetX: 0, originOffsetY: 0, - }); - // 1233x - // x2xxx - // 44556 - // 7oooo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), { - index: 6, - x: 0, y: 3, - w: 1, h: 1, - originOffsetX: 0, originOffsetY: 0, - }, 'should wrap to next line as there\'s no room left'); - }); - test('full', () => { - const { allocator } = initAllocator(3, 2); - // 1oo - // 1oo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x2), { - index: 0, - x: 0, y: 0, - w: 1, h: 2, - originOffsetX: 0, originOffsetY: 0, - }); - // 122 - // 1oo - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel2x1), { - index: 1, - x: 1, y: 0, - w: 2, h: 1, - originOffsetX: 0, originOffsetY: 0, - }); - deepStrictEqual(allocator.allocate(...getUniqueGlyphId(), pixel1x1), undefined, 'should return undefined when the canvas is full'); }); }); From f6290007ddf5a4b24a83eeea07daaf3dd0bd3884 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:06:43 -0700 Subject: [PATCH 071/286] More tests --- .../gpu/atlas/textureAtlasAllocator.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 0468a20aecf..e85281fe55e 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -123,6 +123,15 @@ suite('TextureAtlasAllocator', () => { allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); }); + test('single slab full', () => { + const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); + + // 1 + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + + allocateAndAssert(allocator, pixel1x1, undefined); + }); + test('allocate 1x1 to multiple slabs until full', () => { const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); @@ -142,6 +151,30 @@ suite('TextureAtlasAllocator', () => { allocateAndAssert(allocator, pixel1x1, undefined); }); + + test('glyph too large for canvas', () => { + const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); + allocateAndAssert(allocator, pixel2x1, undefined); + }); + + test('glyph too large for slab', () => { + const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 1 }); + allocateAndAssert(allocator, pixel2x1, undefined); + }); + + test('separate slabs for different sized glyphs', () => { + const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); + + // 10│2o + // 00│2o + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x2, { x: 2, y: 0, w: 1, h: 2 }); + + // 14│23 + // 00│23 + allocateAndAssert(allocator, pixel1x2, { x: 3, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 0, w: 1, h: 1 }); + }); }); }); }); From 93d812769474c257d140d197d128be320cd4628f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:08:59 -0700 Subject: [PATCH 072/286] Fix blocks --- .../gpu/atlas/textureAtlasAllocator.test.ts | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index e85281fe55e..8dd1731c788 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -107,74 +107,74 @@ suite('TextureAtlasAllocator', () => { allocateAndAssert(allocator, pixel2x1, { x: 1, y: 0, w: 2, h: 1 }); allocateAndAssert(allocator, pixel1x1, undefined); }); + }); - suite('TextureAtlasSlabAllocator', () => { - function initAllocator(w: number, h: number, options?: TextureAtlasSlabAllocatorOptions): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasSlabAllocator } { - const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - const allocator = new TextureAtlasSlabAllocator(canvas, ctx, options); - return { canvas, ctx, allocator }; - } + suite('TextureAtlasSlabAllocator', () => { + function initAllocator(w: number, h: number, options?: TextureAtlasSlabAllocatorOptions): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasSlabAllocator } { + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + const allocator = new TextureAtlasSlabAllocator(canvas, ctx, options); + return { canvas, ctx, allocator }; + } - test('single allocation', () => { - const { allocator } = initAllocator(2, 2); - // 1o - // oo - allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); - }); + test('single allocation', () => { + const { allocator } = initAllocator(2, 2); + // 1o + // oo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + }); - test('single slab full', () => { - const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); + test('single slab full', () => { + const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); - // 1 - allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + // 1 + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x1, undefined); - }); + allocateAndAssert(allocator, pixel1x1, undefined); + }); - test('allocate 1x1 to multiple slabs until full', () => { - const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); + test('allocate 1x1 to multiple slabs until full', () => { + const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); - // 12│oo - // 34│oo - allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x1, { x: 1, y: 0, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x1, { x: 0, y: 1, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x1, { x: 1, y: 1, w: 1, h: 1 }); + // 12│oo + // 34│oo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 1, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 1, w: 1, h: 1 }); - // 12│56 - // 34│78 - allocateAndAssert(allocator, pixel1x1, { x: 2, y: 0, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x1, { x: 3, y: 0, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x1, { x: 2, y: 1, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x1, { x: 3, y: 1, w: 1, h: 1 }); + // 12│56 + // 34│78 + allocateAndAssert(allocator, pixel1x1, { x: 2, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 3, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 2, y: 1, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 3, y: 1, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x1, undefined); - }); + allocateAndAssert(allocator, pixel1x1, undefined); + }); - test('glyph too large for canvas', () => { - const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); - allocateAndAssert(allocator, pixel2x1, undefined); - }); + test('glyph too large for canvas', () => { + const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); + allocateAndAssert(allocator, pixel2x1, undefined); + }); - test('glyph too large for slab', () => { - const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 1 }); - allocateAndAssert(allocator, pixel2x1, undefined); - }); + test('glyph too large for slab', () => { + const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 1 }); + allocateAndAssert(allocator, pixel2x1, undefined); + }); - test('separate slabs for different sized glyphs', () => { - const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); + test('separate slabs for different sized glyphs', () => { + const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); - // 10│2o - // 00│2o - allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); - allocateAndAssert(allocator, pixel1x2, { x: 2, y: 0, w: 1, h: 2 }); + // 10│2o + // 00│2o + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x2, { x: 2, y: 0, w: 1, h: 2 }); - // 14│23 - // 00│23 - allocateAndAssert(allocator, pixel1x2, { x: 3, y: 0, w: 1, h: 2 }); - allocateAndAssert(allocator, pixel1x1, { x: 1, y: 0, w: 1, h: 1 }); - }); + // 14│23 + // 00│23 + allocateAndAssert(allocator, pixel1x2, { x: 3, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 0, w: 1, h: 1 }); }); }); }); From 4a95066c13781b9b6887cba9840e8a7921d0de9c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:46:20 -0700 Subject: [PATCH 073/286] More failing test cases --- .../view/gpu/atlas/textureAtlasAllocator.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 8dd1731c788..e8c3cd39892 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -124,7 +124,7 @@ suite('TextureAtlasAllocator', () => { allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); }); - test('single slab full', () => { + test('single slab single glyph full', () => { const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); // 1 @@ -133,6 +133,16 @@ suite('TextureAtlasAllocator', () => { allocateAndAssert(allocator, pixel1x1, undefined); }); + test('single slab multiple glyph full', () => { + const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 2 }); + + // 1 + // 1 + allocateAndAssert(allocator, pixel1x2, { x: 0, y: 0, w: 1, h: 2 }); + + allocateAndAssert(allocator, pixel1x2, undefined); + }); + test('allocate 1x1 to multiple slabs until full', () => { const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); From a14ef1f5b4473d4ca97c7ad6cdee1c02f4ccdb97 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:52:25 -0700 Subject: [PATCH 074/286] Pass allocator tests --- .../browser/view/gpu/atlas/textureAtlasAllocator.ts | 12 +++++++++++- .../view/gpu/atlas/textureAtlasAllocator.test.ts | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts index 2fd2b3fef65..59b2fdb000c 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts @@ -179,6 +179,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private readonly _slabW: number; private readonly _slabH: number; private readonly _slabsPerRow: number; + private readonly _slabsPerColumn: number; private _nextIndex = 0; constructor( @@ -195,9 +196,10 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { this._canvas.height ); this._slabsPerRow = Math.floor(this._canvas.width / this._slabW); + this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH); } - public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph { + public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined { // Find ideal slab, creating it if there is none suitable const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; @@ -238,6 +240,11 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } } + // The glyph does not fit into a slab + if (glyphWidth > this._slabW || glyphHeight > this._slabH) { + return undefined; + } + let dx: number | undefined; let dy: number | undefined; @@ -355,6 +362,9 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { if (dx === undefined || dy === undefined) { if (!slab) { // TODO: Return undefined if there isn't any room left + if (this._slabs.length >= this._slabsPerRow * this._slabsPerColumn) { + return undefined; + } slab = { x: Math.floor(this._slabs.length % this._slabsPerRow) * this._slabW, diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index e8c3cd39892..94f068c9422 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -134,11 +134,12 @@ suite('TextureAtlasAllocator', () => { }); test('single slab multiple glyph full', () => { - const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 2 }); + const { allocator } = initAllocator(2, 2, { slabW: 2, slabH: 2 }); // 1 // 1 allocateAndAssert(allocator, pixel1x2, { x: 0, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel1x2, { x: 1, y: 0, w: 1, h: 2 }); allocateAndAssert(allocator, pixel1x2, undefined); }); From 386b8cf4f4f33d40239eb90afef2507888f476f3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:02:49 -0700 Subject: [PATCH 075/286] Move allocators into own files, move interfaces around --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 37 +++++ .../browser/view/gpu/atlas/textureAtlas.ts | 18 +-- .../view/gpu/atlas/textureAtlasPage.ts | 20 +-- .../gpu/atlas/textureAtlasShelfAllocator.ts | 131 +++++++++++++++ ...ocator.ts => textureAtlasSlabAllocator.ts} | 152 +----------------- .../gpu/atlas/textureAtlasAllocator.test.ts | 4 +- 6 files changed, 178 insertions(+), 184 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/atlas/atlas.ts create mode 100644 src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts rename src/vs/editor/browser/view/gpu/atlas/{textureAtlasAllocator.ts => textureAtlasSlabAllocator.ts} (73%) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts new file mode 100644 index 00000000000..8074c1d48c0 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { TwoKeyMap } from 'vs/base/common/map'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; + +export interface ITextureAtlasGlyph { + index: number; + x: number; + y: number; + w: number; + h: number; + originOffsetX: number; + originOffsetY: number; +} + +export interface IBoundingBox { + left: number; + top: number; + right: number; + bottom: number; +} + +export interface ITextureAtlasAllocator { + readonly glyphMap: TwoKeyMap; + /** + * Allocates a rasterized glyph to the canvas, drawing it and returning information on its + * position in the canvas. This will return undefined if the glyph does not fit on the canvas. + */ + allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined; + /** + * Gets a usage preview of the atlas for debugging purposes. + */ + getUsagePreview(): Promise; +} diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 3dcd7f9d718..04704844a1b 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -11,6 +11,7 @@ import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; // DEBUG: This helper can be used to draw image data to the console, it's commented out as we don't // want to ship it, but this is very useful for investigating texture atlas issues. @@ -136,20 +137,3 @@ export class TextureAtlas extends Disposable { } } } - -export interface ITextureAtlasGlyph { - index: number; - x: number; - y: number; - w: number; - h: number; - originOffsetX: number; - originOffsetY: number; -} - -export interface IBoundingBox { - left: number; - top: number; - right: number; - bottom: number; -} diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 26bde254865..b84efa69a80 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -8,10 +8,11 @@ import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { ITextureAtlasAllocator, TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasAllocator'; +import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TwoKeyMap } from 'vs/base/common/map'; +import type { ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; export class TextureAtlasPage extends Disposable { @@ -101,20 +102,3 @@ export class TextureAtlasPage extends Disposable { return this._allocator.getUsagePreview(); } } - -export interface ITextureAtlasGlyph { - index: number; - x: number; - y: number; - w: number; - h: number; - originOffsetX: number; - originOffsetY: number; -} - -export interface IBoundingBox { - left: number; - top: number; - right: number; - bottom: number; -} diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts new file mode 100644 index 00000000000..2f54a77c4e3 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TwoKeyMap } from 'vs/base/common/map'; +import type { ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; + +/** + * The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the + * current row is full. Due to its simplicity, it can waste space but it is very fast. + */ +export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { + private _currentRow: ITextureAtlasShelf = { + x: 0, + y: 0, + h: 0 + }; + + readonly glyphMap: TwoKeyMap = new TwoKeyMap(); + + private _nextIndex = 0; + + constructor( + private readonly _canvas: OffscreenCanvas, + private readonly _ctx: OffscreenCanvasRenderingContext2D, + ) { + } + + public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined { + // Finalize and increment row if it doesn't fix horizontally + if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) { + this._currentRow.x = 0; + this._currentRow.y += this._currentRow.h; + this._currentRow.h = 1; + } + + // Return undefined if there isn't any room left + if (this._currentRow.y + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1 > this._canvas.height) { + return undefined; + } + + // Draw glyph + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; + this._ctx.drawImage( + rasterizedGlyph.source, + // source + rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.top, + glyphWidth, + glyphHeight, + // destination + this._currentRow.x, + this._currentRow.y, + glyphWidth, + glyphHeight + ); + + // Create glyph object + const glyph: ITextureAtlasGlyph = { + index: this._nextIndex++, + x: this._currentRow.x, + y: this._currentRow.y, + w: glyphWidth, + h: glyphHeight, + originOffsetX: rasterizedGlyph.originOffset.x, + originOffsetY: rasterizedGlyph.originOffset.y + }; + + // Shift current row + this._currentRow.x += glyphWidth; + this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); + + // Set the glyph + this.glyphMap.set(chars, tokenFg, glyph); + + return glyph; + } + + public getUsagePreview(): Promise { + const w = this._canvas.width; + const h = this._canvas.height; + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.fillStyle = '#808080'; + ctx.fillRect(0, 0, w, h); + + let usedPixels = 0; + let wastedPixels = 0; + const totalPixels = w * h; + + const rowHeight: Map = new Map(); // y -> h + const rowWidth: Map = new Map(); // y -> w + for (const g of this.glyphMap.values()) { + rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); + rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); + } + for (const g of this.glyphMap.values()) { + usedPixels += g.w * g.h; + wastedPixels += g.w * (rowHeight.get(g.y)! - g.h); + ctx.fillStyle = '#4040FF'; + ctx.fillRect(g.x, g.y, g.w, g.h); + ctx.fillStyle = '#FF0000'; + ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); + } + for (const [rowY, rowW] of rowWidth.entries()) { + if (rowY !== this._currentRow.y) { + ctx.fillStyle = '#FF0000'; + ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); + wastedPixels += (w - rowW) * rowHeight.get(rowY)!; + } + } + console.log([ + `Texture atlas stats:`, + ` Total: ${totalPixels}`, + ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, + ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, + `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, + ].join('\n')); + return canvas.convertToBlob(); + } +} + +interface ITextureAtlasShelf { + x: number; + y: number; + h: number; +} diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts similarity index 73% rename from src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts rename to src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index 59b2fdb000c..e2db0f00871 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -4,151 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from 'vs/base/browser/dom'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { TwoKeyMap } from 'vs/base/common/map'; - -export interface ITextureAtlasAllocator { - readonly glyphMap: TwoKeyMap; - /** - * Allocates a rasterized glyph to the canvas, drawing it and returning information on its - * position in the canvas. This will return undefined if the glyph does not fit on the canvas. - */ - allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined; - /** - * Gets a usage preview of the atlas for debugging purposes. - */ - getUsagePreview(): Promise; -} - -// #region Shelf allocator - -/** - * The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the - * current row is full. Due to its simplicity, it can waste space but it is very fast. - */ -export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { - private _currentRow: ITextureAtlasShelf = { - x: 0, - y: 0, - h: 0 - }; - - readonly glyphMap: TwoKeyMap = new TwoKeyMap(); - - private _nextIndex = 0; - - constructor( - private readonly _canvas: OffscreenCanvas, - private readonly _ctx: OffscreenCanvasRenderingContext2D, - ) { - } - - public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined { - // Finalize and increment row if it doesn't fix horizontally - if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) { - this._currentRow.x = 0; - this._currentRow.y += this._currentRow.h; - this._currentRow.h = 1; - } - - // Return undefined if there isn't any room left - if (this._currentRow.y + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1 > this._canvas.height) { - return undefined; - } - - // Draw glyph - const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; - const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; - this._ctx.drawImage( - rasterizedGlyph.source, - // source - rasterizedGlyph.boundingBox.left, - rasterizedGlyph.boundingBox.top, - glyphWidth, - glyphHeight, - // destination - this._currentRow.x, - this._currentRow.y, - glyphWidth, - glyphHeight - ); - - // Create glyph object - const glyph: ITextureAtlasGlyph = { - index: this._nextIndex++, - x: this._currentRow.x, - y: this._currentRow.y, - w: glyphWidth, - h: glyphHeight, - originOffsetX: rasterizedGlyph.originOffset.x, - originOffsetY: rasterizedGlyph.originOffset.y - }; - - // Shift current row - this._currentRow.x += glyphWidth; - this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); - - // Set the glyph - this.glyphMap.set(chars, tokenFg, glyph); - - return glyph; - } - - public getUsagePreview(): Promise { - const w = this._canvas.width; - const h = this._canvas.height; - const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - ctx.fillStyle = '#808080'; - ctx.fillRect(0, 0, w, h); - - let usedPixels = 0; - let wastedPixels = 0; - const totalPixels = w * h; - - const rowHeight: Map = new Map(); // y -> h - const rowWidth: Map = new Map(); // y -> w - for (const g of this.glyphMap.values()) { - rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); - rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); - } - for (const g of this.glyphMap.values()) { - usedPixels += g.w * g.h; - wastedPixels += g.w * (rowHeight.get(g.y)! - g.h); - ctx.fillStyle = '#4040FF'; - ctx.fillRect(g.x, g.y, g.w, g.h); - ctx.fillStyle = '#FF0000'; - ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); - } - for (const [rowY, rowW] of rowWidth.entries()) { - if (rowY !== this._currentRow.y) { - ctx.fillStyle = '#FF0000'; - ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); - wastedPixels += (w - rowW) * rowHeight.get(rowY)!; - } - } - console.log([ - `Texture atlas stats:`, - ` Total: ${totalPixels}`, - ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, - ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, - `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, - ].join('\n')); - return canvas.convertToBlob(); - } -} - -interface ITextureAtlasShelf { - x: number; - y: number; - h: number; -} - -// #endregion - -// #region Slab allocator +import type { ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; export interface TextureAtlasSlabAllocatorOptions { slabW?: number; @@ -412,7 +271,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } // Draw glyph - // TODO: Prefer putImageData as it doesn't do blending or scaling this._ctx.drawImage( rasterizedGlyph.source, // source @@ -535,7 +393,7 @@ interface ITextureAtlasSlab { count: number; } -export interface ITextureAtlasSlabUnusedRect { +interface ITextureAtlasSlabUnusedRect { x: number; y: number; w: number; @@ -550,5 +408,3 @@ function addEntryToMapArray(map: Map, key: K, entry: V) { } list.push(entry); } - -// #endregion diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 94f068c9422..213095341db 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -5,7 +5,9 @@ import { deepStrictEqual, strictEqual } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { TextureAtlasShelfAllocator, TextureAtlasSlabAllocator, type ITextureAtlasAllocator, type TextureAtlasSlabAllocatorOptions } from 'vs/editor/browser/view/gpu/atlas/textureAtlasAllocator'; +import type { ITextureAtlasAllocator } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; +import { TextureAtlasSlabAllocator, TextureAtlasSlabAllocatorOptions } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; From 785aaa3fdf3f80b9c7147777b4e28866f32d1e58 Mon Sep 17 00:00:00 2001 From: Ritam Date: Thu, 15 Aug 2024 18:55:06 +0530 Subject: [PATCH 076/286] refactor: moved the cli serve-web cache seeding to ConnectionManager initialization from get_latest_release --- cli/src/commands/serve_web.rs | 55 +++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 96e9c08548d..3528bcb7ab6 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -538,18 +538,47 @@ impl ConnectionManager { pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { let base_path = normalize_base_path(args.server_base_path.as_deref().unwrap_or_default()); + let cache = DownloadCache::load(ctx.paths.web_server_storage()); + let latest_version: tokio::sync::Mutex>; + let target_kind = TargetKind::Web; + + //Set the instant to now minus the RELEASE_CACHE_SECS + //This allows the service to skip use of the cache for the first run + let instant = Instant::now() - Duration::from_secs(RELEASE_CACHE_SECS); + + let quality = VSCODE_CLI_QUALITY + .map_or(Quality::Stable, |q| { + match Quality::try_from(q) { + Ok(q) => q, + Err(_) => Quality::Stable + } + }); + + if let Some(latest_commit) = cache.get().first() { + let release = Release { + name: String::from("0.0.0"), // Version information not stored on cache + commit: latest_commit.clone(), + platform, + target: target_kind, + quality + }; + latest_version = tokio::sync::Mutex::new(Some((instant, release))); + } else { + latest_version = tokio::sync::Mutex::default(); + } + Arc::new(Self { platform, args, base_path, log: ctx.log.clone(), - cache: DownloadCache::load(ctx.paths.web_server_storage()), + cache, update_service: UpdateService::new( ctx.log.clone(), Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())), ), state: ConnectionStateMap::default(), - latest_version: tokio::sync::Mutex::default(), + latest_version, }) } @@ -591,30 +620,12 @@ impl ConnectionManager { .map_err(|e| CodeError::UpdateCheckFailed(e.to_string())); // If the update service is unavailable and we have stale data, use that - if let (Err(e), Some((_, previous))) = (&release, &*latest) { + if let (Err(e), Some((_, previous))) = (&release, latest.clone()) { warning!(self.log, "error getting latest release, using stale: {}", e); + *latest = Some((now, previous.clone())); return Ok(previous.clone()); } - // If the update service is unavailable and we have cached data, use that - if let Err(e) = &release { - warning!(self.log, "error getting latest release: {}", e); - if let Some(latest_commit) = self.cache.get().first() { - warning!(self.log, "using latest release available from cache"); - let release = Release { - name: String::from("0.0.0"), // Version information not stored on cache - commit: latest_commit.clone(), - platform: self.platform, - target: target_kind, - quality - }; - - *latest = Some((now, release.clone())); - - return Ok(release) - } - } - let release = release?; debug!(self.log, "refreshed latest release: {}", release); *latest = Some((now, release.clone())); From edf152b215a8391c8734a5966270327d35b88634 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 06:28:30 -0700 Subject: [PATCH 077/286] Disposable leaks, basic texture atlas unit tests --- .../browser/view/gpu/atlas/textureAtlas.ts | 4 +- src/vs/editor/test/browser/testCodeEditor.ts | 2 +- .../view/gpu/atlas/textureAtlas.test.ts | 101 ++++++++++++++++++ .../gpu/atlas/textureAtlasAllocator.test.ts | 2 +- 4 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 04704844a1b..b8fb9e32d2d 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -54,7 +54,7 @@ export class TextureAtlas extends Disposable { private readonly _glyphRasterizer: GlyphRasterizer; private _colorMap!: string[]; - private readonly _warmUpTask: MutableDisposable = new MutableDisposable(); + private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); public get source(): OffscreenCanvas { return this._page.source; @@ -89,7 +89,7 @@ export class TextureAtlas extends Disposable { this._warmUpAtlas(); })); - this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); + this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, style.fontFamily)); this._page = this._register(this._instantiationService.createInstance(TextureAtlasPage, parentDomNode, pageSize, maxTextureSize, this._glyphRasterizer)); } diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index 5fcd0c1bd4c..969c58c33a1 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -177,7 +177,7 @@ function _withTestCodeEditor(arg: ITextModel | string | string[] | ITextBufferFa disposables.dispose(); } -export function createCodeEditorServices(disposables: DisposableStore, services: ServiceCollection = new ServiceCollection()): TestInstantiationService { +export function createCodeEditorServices(disposables: Pick, services: ServiceCollection = new ServiceCollection()): TestInstantiationService { const serviceIdentifiers: ServiceIdentifier[] = []; const define = (id: ServiceIdentifier, ctor: new (...args: any[]) => T) => { if (!services.has(id)) { diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts new file mode 100644 index 00000000000..65534104a38 --- /dev/null +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ok } from 'assert'; +import { getActiveWindow } from 'vs/base/browser/dom'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { isNumber } from 'vs/base/common/types'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { createCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; +import type { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +const blackInt = 0x000000FF; + +let lastUniqueGlyph: string | undefined; +function getUniqueGlyphId(): [chars: string, tokenFg: number] { + if (!lastUniqueGlyph) { + lastUniqueGlyph = 'a'; + } else { + lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); + } + return [lastUniqueGlyph, blackInt]; +} + +function assertIsValidGlyph(glyph: ITextureAtlasGlyph, atlas: TextureAtlas) { + // (x,y) are valid coordinates + ok(isNumber(glyph.x)); + ok(glyph.x >= 0); + ok(glyph.x < atlas.source.width); + ok(isNumber(glyph.y)); + ok(glyph.y >= 0); + ok(glyph.y < atlas.source.height); + + // (w,h) are valid dimensions + ok(isNumber(glyph.w)); + ok(glyph.w > 0); + ok(glyph.w < atlas.source.width); + ok(isNumber(glyph.h)); + ok(glyph.h > 0); + ok(glyph.h < atlas.source.height); + + // (originOffsetX, originOffsetY) are valid offsets + ok(isNumber(glyph.originOffsetX)); + ok(isNumber(glyph.originOffsetY)); + + // (x,y) + (w,h) are within the bounds of the atlas + ok(glyph.x + glyph.w <= atlas.source.width); + ok(glyph.y + glyph.h <= atlas.source.height); + + // Each of the glyph's outer pixel edges contain at least 1 non-transparent pixel + const ctx = ensureNonNullable(atlas.source.getContext('2d')); + const edges = [ + ctx.getImageData(glyph.x, glyph.y, glyph.w, 1).data, + ctx.getImageData(glyph.x, glyph.y + glyph.h - 1, glyph.w, 1).data, + ctx.getImageData(glyph.x, glyph.y, 1, glyph.h).data, + ctx.getImageData(glyph.x + glyph.w - 1, glyph.y, 1, glyph.h).data, + ]; + for (const edge of edges) { + ok(edge.some(color => (color & 0xFF) !== 0)); + } +} + +suite('TextureAtlas', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + suiteSetup(() => { + lastUniqueGlyph = undefined; + }); + + let instantiationService: IInstantiationService; + let parentElement: HTMLElement; + + setup(() => { + instantiationService = createCodeEditorServices(store); + + const doc = getActiveWindow().document; + parentElement = doc.createElement('div'); + doc.body.appendChild(parentElement); + store.add(toDisposable(() => parentElement.remove())); + }); + + test('get single glyph', () => { + const atlas = store.add(instantiationService.createInstance(TextureAtlas, parentElement, 512, 1024)); + assertIsValidGlyph(atlas.getGlyph(...getUniqueGlyphId()), atlas); + }); + + test('get multiple glyphs', () => { + const atlas = store.add(instantiationService.createInstance(TextureAtlas, parentElement, 512, 1024)); + for (let i = 0; i < 10; i++) { + assertIsValidGlyph(atlas.getGlyph(...getUniqueGlyphId()), atlas); + } + }); + + test.skip('adding glyph to full page creates new page', () => { + throw new Error('NYI'); // TODO: Implement + }); +}); diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 213095341db..4b6096a39c6 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -32,7 +32,7 @@ function createRasterizedGlyph(w: number, h: number, data: ArrayLike): I } let lastUniqueGlyph: string | undefined; -function getUniqueGlyphId(): [string, number] { +function getUniqueGlyphId(): [chars: string, tokenFg: number] { if (!lastUniqueGlyph) { lastUniqueGlyph = 'a'; } else { From d11d4d73315bddf0b528cfd6f9e5d4047cb16a3c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 08:18:13 -0700 Subject: [PATCH 078/286] Work towards multiple textures --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 1 + .../browser/view/gpu/atlas/textureAtlas.ts | 71 +++++----- .../view/gpu/atlas/textureAtlasPage.ts | 31 ++--- .../gpu/atlas/textureAtlasShelfAllocator.ts | 9 +- .../gpu/atlas/textureAtlasSlabAllocator.ts | 10 +- .../editor/browser/view/gpu/gpuViewLayer.ts | 131 ++++++++++++------ .../view/gpu/atlas/textureAtlas.test.ts | 4 +- .../gpu/atlas/textureAtlasAllocator.test.ts | 14 +- 8 files changed, 159 insertions(+), 112 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index 8074c1d48c0..3ce0658b781 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -7,6 +7,7 @@ import type { TwoKeyMap } from 'vs/base/common/map'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; export interface ITextureAtlasGlyph { + textureIndex: number; index: number; x: number; y: number; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index b8fb9e32d2d..67dc111e208 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -6,47 +6,15 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; -import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; - -// DEBUG: This helper can be used to draw image data to the console, it's commented out as we don't -// want to ship it, but this is very useful for investigating texture atlas issues. -// (console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => { -// function getBox(width: number, height: number) { -// return { -// string: '+', -// style: 'font-size: 1px; padding: ' + Math.floor(height / 2) + 'px ' + Math.floor(width / 2) + 'px; line-height: ' + height + 'px;' -// }; -// } -// if (source instanceof HTMLCanvasElement) { -// source = source.getContext('2d')?.getImageData(0, 0, source.width, source.height)!; -// } -// const canvas = document.createElement('canvas'); -// canvas.width = source.width; -// canvas.height = source.height; -// const ctx = canvas.getContext('2d')!; -// ctx.putImageData(source, 0, 0); - -// const sw = source.width * scale; -// const sh = source.height * scale; -// const dim = getBox(sw, sh); -// console.log( -// `Image: ${source.width} x ${source.height}\n%c${dim.string}`, -// `${dim.style}background: url(${canvas.toDataURL()}); background-size: ${sw}px ${sh}px; background-repeat: no-repeat; color: transparent;` -// ); -// console.groupCollapsed('Zoomed'); -// console.log( -// `%c${dim.string}`, -// `${getBox(sw * 10, sh * 10).style}background: url(${canvas.toDataURL()}); background-size: ${sw * 10}px ${sh * 10}px; background-repeat: no-repeat; color: transparent; image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;` -// ); -// console.groupEnd(); -// }; export class TextureAtlas extends Disposable { + // TODO: Expose all page glyphs - the glyphs will need a textureId association public get glyphs(): IterableIterator { return this._page.glyphs; } @@ -67,18 +35,34 @@ export class TextureAtlas extends Disposable { this._page.hasChanges = value; } + /** + * The scratch texture atlas page is a relatively small texture where glyphs that are required + * immediately are drawn to which reduces the latency of their first draw. In some future idle + * callback, the glyph will be transferred into one of the main pages. + */ + private readonly _scratchPage: TextureAtlasPage; + // TODO: Generically get pages externally - gpu shouldn't care about the details of the pages + public get scratchSource(): OffscreenCanvas { + return this._scratchPage.source; + } + + /** + * The main texture atlas pages which are both larger textures and more efficiently packed + * relative to the scratch page. The idea is the main pages are drawn to and uploaded to the GPU + * much less frequently so as to not drop frames. + */ private readonly _page: TextureAtlasPage; - // TODO: Should pull in the font size from config instead of random dom node constructor( parentDomNode: HTMLElement, - pageSize: number, - maxTextureSize: number, + /** The maximum texture size supported by the GPU. */ + private readonly _maxTextureSize: number, @IThemeService private readonly _themeService: IThemeService, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); + // TODO: Should pull in the font size from config instead of random dom node const activeWindow = getActiveWindow(); const style = activeWindow.getComputedStyle(parentDomNode); const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); @@ -91,7 +75,16 @@ export class TextureAtlas extends Disposable { this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, style.fontFamily)); - this._page = this._register(this._instantiationService.createInstance(TextureAtlasPage, parentDomNode, pageSize, maxTextureSize, this._glyphRasterizer)); + const dprFactor = Math.max(1, Math.floor(activeWindow.devicePixelRatio)); + + // TODO: Hook up scratch page to renderer + const scratchPageSize = Math.min(512 * dprFactor, this._maxTextureSize); + // TODO: General way of assigning texture identifier + // TODO: Identify texture via a name, the texture index should be only known to the GPU code + this._scratchPage = this._register(this._instantiationService.createInstance(TextureAtlasPage, 0, scratchPageSize, 'shelf', this._glyphRasterizer)); + + const pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); + this._page = this._register(this._instantiationService.createInstance(TextureAtlasPage, 1, pageSize, 'slab', this._glyphRasterizer)); } // TODO: Color, style etc. diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index b84efa69a80..849baa52fc5 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -3,21 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TwoKeyMap } from 'vs/base/common/map'; import type { ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; +import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; +import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TextureAtlasPage extends Disposable { private readonly _canvas: OffscreenCanvas; - private readonly _ctx: OffscreenCanvasRenderingContext2D; private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); // HACK: This is an ordered set of glyphs to be passed to the GPU since currently the shader @@ -39,9 +37,9 @@ export class TextureAtlasPage extends Disposable { // TODO: Should pull in the font size from config instead of random dom node constructor( - parentDomNode: HTMLElement, + textureIndex: number, pageSize: number, - maxTextureSize: number, + allocatorType: 'shelf' | 'slab', private readonly _glyphRasterizer: GlyphRasterizer, @ILogService private readonly _logService: ILogService, @IThemeService private readonly _themeService: IThemeService, @@ -49,23 +47,17 @@ export class TextureAtlasPage extends Disposable { super(); this._canvas = new OffscreenCanvas(pageSize, pageSize); - this._ctx = ensureNonNullable(this._canvas.getContext('2d', { - willReadFrequently: true - })); - const activeWindow = getActiveWindow(); - const style = activeWindow.getComputedStyle(parentDomNode); - const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); - this._ctx.font = `${fontSize}px ${style.fontFamily}`; + switch (allocatorType) { + case 'shelf': this._allocator = new TextureAtlasShelfAllocator(this._canvas, textureIndex); break; + case 'slab': this._allocator = new TextureAtlasSlabAllocator(this._canvas, textureIndex); break; + } this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { // TODO: Clear entire atlas on theme change this._colorMap = this._themeService.getColorTheme().tokenColorMap; })); - // this._allocator = new TextureAtlasShelfAllocator(this._canvas, this._ctx); - this._allocator = new TextureAtlasSlabAllocator(this._canvas, this._ctx); - // Reduce impact of a memory leak if this object is not released this._register(toDisposable(() => { this._canvas.width = 1; @@ -99,6 +91,7 @@ export class TextureAtlasPage extends Disposable { } getUsagePreview(): Promise { + // TODO: Standardize usage stats and make them loggable return this._allocator.getUsagePreview(); } } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts index 2f54a77c4e3..534e163fd2a 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts @@ -13,6 +13,9 @@ import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRa * current row is full. Due to its simplicity, it can waste space but it is very fast. */ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { + + private readonly _ctx: OffscreenCanvasRenderingContext2D; + private _currentRow: ITextureAtlasShelf = { x: 0, y: 0, @@ -25,8 +28,11 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { constructor( private readonly _canvas: OffscreenCanvas, - private readonly _ctx: OffscreenCanvasRenderingContext2D, + private readonly _textureIndex: number, ) { + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); } public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined { @@ -61,6 +67,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { // Create glyph object const glyph: ITextureAtlasGlyph = { + textureIndex: this._textureIndex, index: this._nextIndex++, x: this._currentRow.x, y: this._currentRow.y, diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index e2db0f00871..e2956498e5c 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -24,6 +24,9 @@ export interface TextureAtlasSlabAllocatorOptions { * waste a lot of space in their own slab. */ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { + + private readonly _ctx: OffscreenCanvasRenderingContext2D; + // TODO: Is there a better way to index slabs other than an unsorted list? private _slabs: ITextureAtlasSlab[] = []; private _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); @@ -43,9 +46,13 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { constructor( private readonly _canvas: OffscreenCanvas, - private readonly _ctx: OffscreenCanvasRenderingContext2D, + private readonly _textureIndex: number, options?: TextureAtlasSlabAllocatorOptions ) { + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); + this._slabW = Math.min( options?.slabW ?? (64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1)), this._canvas.width @@ -287,6 +294,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Create glyph object const glyph: ITextureAtlasGlyph = { + textureIndex: this._textureIndex, index: this._nextIndex++, x: dx, y: dy, diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 1f7d8a3513b..948159303d1 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -7,7 +7,8 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { VSBuffer } from 'vs/base/common/buffer'; import { debounce } from 'vs/base/common/decorators'; import { URI } from 'vs/base/common/uri'; -import { TextureAtlas, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -39,13 +40,15 @@ const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float const enum BindingId { // TODO: Improve names - SpriteInfo = 0, - DynamicUnitInfo = 1, - TextureSampler = 2, - Texture = 3, - Uniforms = 4, - TextureInfoUniform = 5, - ScrollOffset = 6, + SpriteInfo0, + SpriteInfo1, + DynamicUnitInfo, + TextureSampler, + Texture0, + Texture1, + Uniforms, + TextureInfoUniform, + ScrollOffset, } export class GpuViewLayerRenderer { @@ -67,8 +70,10 @@ export class GpuViewLayerRenderer { private _squareVertices!: { vertexData: Float32Array; numVertices: number }; private static _textureAtlas: TextureAtlas; - private _spriteInfoStorageBuffer!: GPUBuffer; - private _textureAtlasGpuTexture!: GPUTexture; + private _spriteInfoStorageBuffer0!: GPUBuffer; + private _spriteInfoStorageBuffer1!: GPUBuffer; + private _textureAtlasGpuTexture0!: GPUTexture; + private _textureAtlasGpuTexture1!: GPUTexture; private _initialized = false; @@ -111,8 +116,7 @@ export class GpuViewLayerRenderer { // Create texture atlas if (!GpuViewLayerRenderer._textureAtlas) { - const pageSize = this._device.limits.maxTextureDimension2D; - GpuViewLayerRenderer._textureAtlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, pageSize, this._device.limits.maxTextureDimension2D); + GpuViewLayerRenderer._textureAtlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, this._device.limits.maxTextureDimension2D); } const textureAtlas = GpuViewLayerRenderer._textureAtlas; @@ -206,14 +210,24 @@ export class GpuViewLayerRenderer { /////////////////// // Static buffer // /////////////////// - this._spriteInfoStorageBuffer = this._device.createBuffer({ + this._spriteInfoStorageBuffer0 = this._device.createBuffer({ label: 'Entity static info buffer', size: spriteInfoStorageBufferByteSize * maxRenderedObjects, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); - - // Upload texture bitmap from atlas - this._textureAtlasGpuTexture = this._device.createTexture({ + this._textureAtlasGpuTexture0 = this._device.createTexture({ + format: 'rgba8unorm', + size: { width: textureAtlas.scratchSource.width, height: textureAtlas.scratchSource.height }, + usage: GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + this._spriteInfoStorageBuffer1 = this._device.createBuffer({ + label: 'Entity static info buffer', + size: spriteInfoStorageBufferByteSize * maxRenderedObjects, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }); + this._textureAtlasGpuTexture1 = this._device.createTexture({ format: 'rgba8unorm', size: { width: textureAtlas.source.width, height: textureAtlas.source.height }, usage: GPUTextureUsage.TEXTURE_BINDING | @@ -240,9 +254,12 @@ export class GpuViewLayerRenderer { label: 'ViewLayer bind group', layout: this._pipeline.getBindGroupLayout(0), entries: [ - { binding: BindingId.SpriteInfo, resource: { buffer: this._spriteInfoStorageBuffer } }, + // Pass in generically as array? + { binding: BindingId.SpriteInfo0, resource: { buffer: this._spriteInfoStorageBuffer0 } }, + { binding: BindingId.SpriteInfo1, resource: { buffer: this._spriteInfoStorageBuffer1 } }, { binding: BindingId.TextureSampler, resource: sampler }, - { binding: BindingId.Texture, resource: this._textureAtlasGpuTexture.createView() }, + { binding: BindingId.Texture0, resource: this._textureAtlasGpuTexture0.createView() }, + { binding: BindingId.Texture1, resource: this._textureAtlasGpuTexture1.createView() }, { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, ...this._renderStrategy.bindGroupEntries @@ -298,24 +315,50 @@ export class GpuViewLayerRenderer { return; } GpuViewLayerRenderer._textureAtlas.hasChanges = false; - // TODO: Dynamically set buffer size - const bufferSize = spriteInfoStorageBufferByteSize * 10000; - const values = new Float32Array(bufferSize / 4); - let entryOffset = 0; - for (const glyph of GpuViewLayerRenderer._textureAtlas.glyphs) { - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; - entryOffset += SpriteInfoStorageBufferInfo.Size; + + { + // TODO: Dynamically set buffer size + const bufferSize = spriteInfoStorageBufferByteSize * 10000; + const values = new Float32Array(bufferSize / 4); + let entryOffset = 0; + for (const glyph of GpuViewLayerRenderer._textureAtlas.glyphs) { + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; + entryOffset += SpriteInfoStorageBufferInfo.Size; + } + this._device.queue.writeBuffer(this._spriteInfoStorageBuffer0, 0, values); + } + + this._device.queue.copyExternalImageToTexture( + { source: GpuViewLayerRenderer._textureAtlas.scratchSource }, + { texture: this._textureAtlasGpuTexture0 }, + { width: GpuViewLayerRenderer._textureAtlas.scratchSource.width, height: GpuViewLayerRenderer._textureAtlas.scratchSource.height }, + ); + + // TODO: Dynamically set buffer size + { + const bufferSize = spriteInfoStorageBufferByteSize * 10000; + const values = new Float32Array(bufferSize / 4); + let entryOffset = 0; + for (const glyph of GpuViewLayerRenderer._textureAtlas.glyphs) { + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; + values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; + entryOffset += SpriteInfoStorageBufferInfo.Size; + } + this._device.queue.writeBuffer(this._spriteInfoStorageBuffer1, 0, values); } - this._device.queue.writeBuffer(this._spriteInfoStorageBuffer, 0, values); this._device.queue.copyExternalImageToTexture( { source: GpuViewLayerRenderer._textureAtlas.source }, - { texture: this._textureAtlasGpuTexture }, + { texture: this._textureAtlasGpuTexture1 }, { width: GpuViewLayerRenderer._textureAtlas.source.width, height: GpuViewLayerRenderer._textureAtlas.source.height }, ); @@ -329,11 +372,11 @@ export class GpuViewLayerRenderer { if (folders.length > 0) { await Promise.all([ fileService.writeFile( - URI.joinPath(folders[0].uri, 'atlas_scratch_actual.png'), + URI.joinPath(folders[0].uri, 'atlas_page1_actual.png'), VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer())) ), fileService.writeFile( - URI.joinPath(folders[0].uri, 'atlas_scratch_usage.png'), + URI.joinPath(folders[0].uri, 'atlas_page1_usage.png'), VSBuffer.wrap(new Uint8Array(await ((await GpuViewLayerRenderer._textureAtlas.getUsagePreview()).arrayBuffer()))) ) ]); @@ -419,8 +462,8 @@ struct Vertex { struct DynamicUnitInfo { position: vec2f, unused1: vec2f, - textureIndex: f32, - unused2: f32 + glyphIndex: f32, + textureIndex: f32 }; struct ScrollOffset { @@ -435,7 +478,8 @@ struct VSOutput { @group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; @group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; -@group(0) @binding(${BindingId.SpriteInfo}) var spriteInfo: array; +@group(0) @binding(${BindingId.SpriteInfo0}) var spriteInfo0: array; +@group(0) @binding(${BindingId.SpriteInfo1}) var spriteInfo1: array; @group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; @group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; @@ -445,7 +489,8 @@ struct VSOutput { @builtin(vertex_index) vertexIndex : u32 ) -> VSOutput { let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; - let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureIndex)]; + let temp = spriteInfo0[0]; + let spriteInfo = spriteInfo1[u32(dynamicUnitInfo.glyphIndex)]; var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 @@ -468,10 +513,12 @@ struct VSOutput { } @group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; -@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d; +@group(0) @binding(${BindingId.Texture0}) var ourTexture0: texture_2d; +@group(0) @binding(${BindingId.Texture1}) var ourTexture1: texture_2d; @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { - return textureSample(ourTexture, ourSampler, vsOut.texcoord); + let temp = textureSample(ourTexture0, ourSampler, vsOut.texcoord); + return textureSample(ourTexture1, ourSampler, vsOut.texcoord); } `; @@ -665,8 +712,8 @@ class FullFileRenderStrategy implements IRenderStrategy< cellBuffer[cellIndex + 1] = -wgslY; // y cellBuffer[cellIndex + 2] = 0; cellBuffer[cellIndex + 3] = 0; - cellBuffer[cellIndex + 4] = glyph.index; // textureIndex - cellBuffer[cellIndex + 5] = 0; + cellBuffer[cellIndex + 4] = glyph.index; // glyphIndex + cellBuffer[cellIndex + 5] = glyph.textureIndex; // textureIndex } tokenStartIndex = tokenEndIndex; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 65534104a38..203821028c9 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -84,12 +84,12 @@ suite('TextureAtlas', () => { }); test('get single glyph', () => { - const atlas = store.add(instantiationService.createInstance(TextureAtlas, parentElement, 512, 1024)); + const atlas = store.add(instantiationService.createInstance(TextureAtlas, parentElement, 512)); assertIsValidGlyph(atlas.getGlyph(...getUniqueGlyphId()), atlas); }); test('get multiple glyphs', () => { - const atlas = store.add(instantiationService.createInstance(TextureAtlas, parentElement, 512, 1024)); + const atlas = store.add(instantiationService.createInstance(TextureAtlas, parentElement, 512)); for (let i = 0; i < 10; i++) { assertIsValidGlyph(atlas.getGlyph(...getUniqueGlyphId()), atlas); } diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 4b6096a39c6..9269ecef2e0 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -63,11 +63,10 @@ suite('TextureAtlasAllocator', () => { }); suite('TextureAtlasShelfAllocator', () => { - function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasShelfAllocator } { + function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; allocator: TextureAtlasShelfAllocator } { const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - const allocator = new TextureAtlasShelfAllocator(canvas, ctx); - return { canvas, ctx, allocator }; + const allocator = new TextureAtlasShelfAllocator(canvas, 0); + return { canvas, allocator }; } test('single allocation', () => { @@ -112,11 +111,10 @@ suite('TextureAtlasAllocator', () => { }); suite('TextureAtlasSlabAllocator', () => { - function initAllocator(w: number, h: number, options?: TextureAtlasSlabAllocatorOptions): { canvas: OffscreenCanvas; ctx: OffscreenCanvasRenderingContext2D; allocator: TextureAtlasSlabAllocator } { + function initAllocator(w: number, h: number, options?: TextureAtlasSlabAllocatorOptions): { canvas: OffscreenCanvas; allocator: TextureAtlasSlabAllocator } { const canvas = new OffscreenCanvas(w, h); - const ctx = ensureNonNullable(canvas.getContext('2d')); - const allocator = new TextureAtlasSlabAllocator(canvas, ctx, options); - return { canvas, ctx, allocator }; + const allocator = new TextureAtlasSlabAllocator(canvas, 0, options); + return { canvas, allocator }; } test('single allocation', () => { From c1232b94e59444ae4a25ed5e20c549af096dd7c6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:19:49 -0700 Subject: [PATCH 079/286] Multiple texture layers --- .../browser/view/gpu/atlas/textureAtlas.ts | 21 ++++-- .../editor/browser/view/gpu/gpuViewLayer.ts | 66 ++++++++++--------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 67dc111e208..39599ef874e 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -15,6 +15,9 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TextureAtlas extends Disposable { // TODO: Expose all page glyphs - the glyphs will need a textureId association + public get scratchGlyphs(): IterableIterator { + return this._scratchPage.glyphs; + } public get glyphs(): IterableIterator { return this._page.glyphs; } @@ -29,9 +32,10 @@ export class TextureAtlas extends Disposable { } public get hasChanges(): boolean { - return this._page.hasChanges; + return this._scratchPage.hasChanges || this._page.hasChanges; } public set hasChanges(value: boolean) { + this._scratchPage.hasChanges = value; this._page.hasChanges = value; } @@ -77,8 +81,9 @@ export class TextureAtlas extends Disposable { const dprFactor = Math.max(1, Math.floor(activeWindow.devicePixelRatio)); + // TODO: Scratch should be smaller // TODO: Hook up scratch page to renderer - const scratchPageSize = Math.min(512 * dprFactor, this._maxTextureSize); + const scratchPageSize = Math.min(1024 * dprFactor, this._maxTextureSize); // TODO: General way of assigning texture identifier // TODO: Identify texture via a name, the texture index should be only known to the GPU code this._scratchPage = this._register(this._instantiationService.createInstance(TextureAtlasPage, 0, scratchPageSize, 'shelf', this._glyphRasterizer)); @@ -89,11 +94,19 @@ export class TextureAtlas extends Disposable { // TODO: Color, style etc. public getGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { + // HACK: Testing multiple pages + // if (Math.random() < 0.5) { + // return this._scratchPage.getGlyph(chars, tokenFg); + // } return this._page.getGlyph(chars, tokenFg); + // return this._scratchPage.getGlyph(chars, tokenFg); } - public getUsagePreview(): Promise { - return this._page.getUsagePreview(); + public getUsagePreview(): Promise { + return Promise.all([ + this._scratchPage.getUsagePreview(), + this._page.getUsagePreview(), + ]); } /** diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 948159303d1..8d5c2b796da 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -44,8 +44,7 @@ const enum BindingId { SpriteInfo1, DynamicUnitInfo, TextureSampler, - Texture0, - Texture1, + Texture, Uniforms, TextureInfoUniform, ScrollOffset, @@ -72,8 +71,7 @@ export class GpuViewLayerRenderer { private static _textureAtlas: TextureAtlas; private _spriteInfoStorageBuffer0!: GPUBuffer; private _spriteInfoStorageBuffer1!: GPUBuffer; - private _textureAtlasGpuTexture0!: GPUTexture; - private _textureAtlasGpuTexture1!: GPUTexture; + private _textureAtlasGpuTexture!: GPUTexture; private _initialized = false; @@ -215,21 +213,16 @@ export class GpuViewLayerRenderer { size: spriteInfoStorageBufferByteSize * maxRenderedObjects, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); - this._textureAtlasGpuTexture0 = this._device.createTexture({ - format: 'rgba8unorm', - size: { width: textureAtlas.scratchSource.width, height: textureAtlas.scratchSource.height }, - usage: GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT, - }); this._spriteInfoStorageBuffer1 = this._device.createBuffer({ label: 'Entity static info buffer', size: spriteInfoStorageBufferByteSize * maxRenderedObjects, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); - this._textureAtlasGpuTexture1 = this._device.createTexture({ + this._textureAtlasGpuTexture = this._device.createTexture({ format: 'rgba8unorm', - size: { width: textureAtlas.source.width, height: textureAtlas.source.height }, + // TODO: These sizes are shared across all layers - this means scratch is wasting texture space + size: { width: textureAtlas.source.width, height: textureAtlas.source.height, depthOrArrayLayers: 2 }, + dimension: '2d', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, @@ -258,8 +251,7 @@ export class GpuViewLayerRenderer { { binding: BindingId.SpriteInfo0, resource: { buffer: this._spriteInfoStorageBuffer0 } }, { binding: BindingId.SpriteInfo1, resource: { buffer: this._spriteInfoStorageBuffer1 } }, { binding: BindingId.TextureSampler, resource: sampler }, - { binding: BindingId.Texture0, resource: this._textureAtlasGpuTexture0.createView() }, - { binding: BindingId.Texture1, resource: this._textureAtlasGpuTexture1.createView() }, + { binding: BindingId.Texture, resource: this._textureAtlasGpuTexture.createView() }, { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, ...this._renderStrategy.bindGroupEntries @@ -321,7 +313,7 @@ export class GpuViewLayerRenderer { const bufferSize = spriteInfoStorageBufferByteSize * 10000; const values = new Float32Array(bufferSize / 4); let entryOffset = 0; - for (const glyph of GpuViewLayerRenderer._textureAtlas.glyphs) { + for (const glyph of GpuViewLayerRenderer._textureAtlas.scratchGlyphs) { values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; @@ -333,12 +325,6 @@ export class GpuViewLayerRenderer { this._device.queue.writeBuffer(this._spriteInfoStorageBuffer0, 0, values); } - this._device.queue.copyExternalImageToTexture( - { source: GpuViewLayerRenderer._textureAtlas.scratchSource }, - { texture: this._textureAtlasGpuTexture0 }, - { width: GpuViewLayerRenderer._textureAtlas.scratchSource.width, height: GpuViewLayerRenderer._textureAtlas.scratchSource.height }, - ); - // TODO: Dynamically set buffer size { const bufferSize = spriteInfoStorageBufferByteSize * 10000; @@ -356,9 +342,15 @@ export class GpuViewLayerRenderer { this._device.queue.writeBuffer(this._spriteInfoStorageBuffer1, 0, values); } + // TODO: Draw only dirty regions + this._device.queue.copyExternalImageToTexture( + { source: GpuViewLayerRenderer._textureAtlas.scratchSource }, + { texture: this._textureAtlasGpuTexture, origin: { x: 0, y: 0, z: 0 } }, + { width: GpuViewLayerRenderer._textureAtlas.scratchSource.width, height: GpuViewLayerRenderer._textureAtlas.scratchSource.height }, + ); this._device.queue.copyExternalImageToTexture( { source: GpuViewLayerRenderer._textureAtlas.source }, - { texture: this._textureAtlasGpuTexture1 }, + { texture: this._textureAtlasGpuTexture, origin: { x: 0, y: 0, z: 1 } }, { width: GpuViewLayerRenderer._textureAtlas.source.width, height: GpuViewLayerRenderer._textureAtlas.source.height }, ); @@ -367,17 +359,27 @@ export class GpuViewLayerRenderer { @debounce(500) private static async _drawToTextureAtlas(fileService: IFileService, workspaceContextService: IWorkspaceContextService) { + const scratchBlob = await GpuViewLayerRenderer._textureAtlas.scratchSource.convertToBlob(); const blob = await GpuViewLayerRenderer._textureAtlas.source.convertToBlob(); const folders = workspaceContextService.getWorkspace().folders; if (folders.length > 0) { + const usagePreviews = await GpuViewLayerRenderer._textureAtlas.getUsagePreview(); await Promise.all([ + fileService.writeFile( + URI.joinPath(folders[0].uri, 'atlas_page0_actual.png'), + VSBuffer.wrap(new Uint8Array(await scratchBlob.arrayBuffer())) + ), + fileService.writeFile( + URI.joinPath(folders[0].uri, 'atlas_page0_usage.png'), + VSBuffer.wrap(new Uint8Array(await usagePreviews[0].arrayBuffer())) + ), fileService.writeFile( URI.joinPath(folders[0].uri, 'atlas_page1_actual.png'), VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer())) ), fileService.writeFile( URI.joinPath(folders[0].uri, 'atlas_page1_usage.png'), - VSBuffer.wrap(new Uint8Array(await ((await GpuViewLayerRenderer._textureAtlas.getUsagePreview()).arrayBuffer()))) + VSBuffer.wrap(new Uint8Array(await usagePreviews[1].arrayBuffer())) ) ]); } @@ -472,6 +474,7 @@ struct ScrollOffset { struct VSOutput { @builtin(position) position: vec4f, + @location(1) layerIndex: f32, @location(0) texcoord: vec2f, }; @@ -489,8 +492,12 @@ struct VSOutput { @builtin(vertex_index) vertexIndex : u32 ) -> VSOutput { let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; - let temp = spriteInfo0[0]; - let spriteInfo = spriteInfo1[u32(dynamicUnitInfo.glyphIndex)]; + var spriteInfo = spriteInfo0[0]; + if (u32(dynamicUnitInfo.textureIndex) == 0) { + spriteInfo = spriteInfo0[u32(dynamicUnitInfo.glyphIndex)]; + } else { + spriteInfo = spriteInfo1[u32(dynamicUnitInfo.glyphIndex)]; + } var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 @@ -500,6 +507,7 @@ struct VSOutput { 1.0 ); + vsOut.layerIndex = dynamicUnitInfo.textureIndex; // Textures are flipped from natural direction on the y-axis, so flip it back vsOut.texcoord = vert.position; vsOut.texcoord = ( @@ -513,12 +521,10 @@ struct VSOutput { } @group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; -@group(0) @binding(${BindingId.Texture0}) var ourTexture0: texture_2d; -@group(0) @binding(${BindingId.Texture1}) var ourTexture1: texture_2d; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d_array; @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { - let temp = textureSample(ourTexture0, ourSampler, vsOut.texcoord); - return textureSample(ourTexture1, ourSampler, vsOut.texcoord); + return textureSample(ourTexture, ourSampler, vsOut.texcoord, u32(vsOut.layerIndex)); } `; From 7c37ba78aad1e0095dd977659568612c184109d3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:54:26 -0700 Subject: [PATCH 080/286] Multiple texture layers progress --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 7 + .../browser/view/gpu/atlas/textureAtlas.ts | 59 ++++---- .../view/gpu/atlas/textureAtlasPage.ts | 4 +- .../editor/browser/view/gpu/gpuViewLayer.ts | 128 +++++++----------- .../view/gpu/atlas/textureAtlas.test.ts | 14 +- 5 files changed, 100 insertions(+), 112 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index 3ce0658b781..7afabefde24 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -36,3 +36,10 @@ export interface ITextureAtlasAllocator { */ getUsagePreview(): Promise; } + +export interface IReadableTextureAtlasPage { + readonly glyphs: IterableIterator; + readonly source: OffscreenCanvas; + readonly hasChanges: boolean; + getUsagePreview(): Promise; +} diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 39599ef874e..2ff2ca2a1f1 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -5,8 +5,8 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; -import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import type { IReadableTextureAtlasPage, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; @@ -18,25 +18,28 @@ export class TextureAtlas extends Disposable { public get scratchGlyphs(): IterableIterator { return this._scratchPage.glyphs; } - public get glyphs(): IterableIterator { - return this._page.glyphs; - } + // public get glyphs(): IterableIterator { + // return this._pages.glyphs; + // } private readonly _glyphRasterizer: GlyphRasterizer; private _colorMap!: string[]; private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); - public get source(): OffscreenCanvas { - return this._page.source; - } + // get source(): OffscreenCanvas { + // return this._pages.source; + // } - public get hasChanges(): boolean { - return this._scratchPage.hasChanges || this._page.hasChanges; + // TODO: Should check changes independently + get hasChanges(): boolean { + // return this._scratchPage.hasChanges || this._pages.hasChanges; + return this._pages.some(e => e.hasChanges); } - public set hasChanges(value: boolean) { - this._scratchPage.hasChanges = value; - this._page.hasChanges = value; + set hasChanges(value: boolean) { + // this._scratchPage.hasChanges = value; + // this._pages.hasChanges = value; + this._pages[0].hasChanges = value; } /** @@ -46,7 +49,7 @@ export class TextureAtlas extends Disposable { */ private readonly _scratchPage: TextureAtlasPage; // TODO: Generically get pages externally - gpu shouldn't care about the details of the pages - public get scratchSource(): OffscreenCanvas { + get scratchSource(): OffscreenCanvas { return this._scratchPage.source; } @@ -55,7 +58,12 @@ export class TextureAtlas extends Disposable { * relative to the scratch page. The idea is the main pages are drawn to and uploaded to the GPU * much less frequently so as to not drop frames. */ - private readonly _page: TextureAtlasPage; + private readonly _pages: TextureAtlasPage[] = []; + get pages(): IReadableTextureAtlasPage[] { + return this._pages; + } + + readonly pageSize: number; constructor( parentDomNode: HTMLElement, @@ -88,25 +96,22 @@ export class TextureAtlas extends Disposable { // TODO: Identify texture via a name, the texture index should be only known to the GPU code this._scratchPage = this._register(this._instantiationService.createInstance(TextureAtlasPage, 0, scratchPageSize, 'shelf', this._glyphRasterizer)); - const pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); - this._page = this._register(this._instantiationService.createInstance(TextureAtlasPage, 1, pageSize, 'slab', this._glyphRasterizer)); + this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); + this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, 'slab', this._glyphRasterizer)); + this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 1, this.pageSize, 'slab', this._glyphRasterizer)); + this._register(toDisposable(() => dispose(this._pages))); } // TODO: Color, style etc. public getGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { - // HACK: Testing multiple pages - // if (Math.random() < 0.5) { - // return this._scratchPage.getGlyph(chars, tokenFg); - // } - return this._page.getGlyph(chars, tokenFg); - // return this._scratchPage.getGlyph(chars, tokenFg); + // HACK: Draw glyphs to different pages to test out multiple textures while there's no overflow logic + const targetPage = chars.match(/[a-z]/i) ? 0 : 1; + return this._pages[targetPage].getGlyph(chars, tokenFg); } public getUsagePreview(): Promise { - return Promise.all([ - this._scratchPage.getUsagePreview(), - this._page.getUsagePreview(), - ]); + // TODO: Include scratch page + return Promise.all(this._pages.map(e => e.getUsagePreview())); } /** diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 849baa52fc5..90181714e49 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -6,14 +6,14 @@ import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { TwoKeyMap } from 'vs/base/common/map'; -import type { ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -export class TextureAtlasPage extends Disposable { +export class TextureAtlasPage extends Disposable implements IReadableTextureAtlasPage { private readonly _canvas: OffscreenCanvas; diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 8d5c2b796da..4c0801c1317 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -40,8 +40,8 @@ const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float const enum BindingId { // TODO: Improve names - SpriteInfo0, - SpriteInfo1, + GlyphInfo0, + GlyphInfo1, DynamicUnitInfo, TextureSampler, Texture, @@ -69,8 +69,7 @@ export class GpuViewLayerRenderer { private _squareVertices!: { vertexData: Float32Array; numVertices: number }; private static _textureAtlas: TextureAtlas; - private _spriteInfoStorageBuffer0!: GPUBuffer; - private _spriteInfoStorageBuffer1!: GPUBuffer; + private readonly _glyphStorageBuffer: GPUBuffer[] = []; private _textureAtlasGpuTexture!: GPUTexture; private _initialized = false; @@ -119,7 +118,6 @@ export class GpuViewLayerRenderer { const textureAtlas = GpuViewLayerRenderer._textureAtlas; - // this._renderStrategy = new NaiveViewportRenderStrategy(this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); this._renderStrategy = this._instantiationService.createInstance(FullFileRenderStrategy, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); const module = this._device.createShaderModule({ @@ -197,8 +195,8 @@ export class GpuViewLayerRenderer { { const uniformValues = new Float32Array(TextureInfoUniformBufferInfo.Size); // TODO: Update on canvas resize - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = textureAtlas.source.width; - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = textureAtlas.source.height; + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = textureAtlas.pageSize; + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = textureAtlas.pageSize; this._device.queue.writeBuffer(textureInfoUniformBuffer, 0, uniformValues); } @@ -208,20 +206,21 @@ export class GpuViewLayerRenderer { /////////////////// // Static buffer // /////////////////// - this._spriteInfoStorageBuffer0 = this._device.createBuffer({ - label: 'Entity static info buffer', + this._glyphStorageBuffer[0] = this._device.createBuffer({ + label: 'Glyph storage buffer', size: spriteInfoStorageBufferByteSize * maxRenderedObjects, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); - this._spriteInfoStorageBuffer1 = this._device.createBuffer({ - label: 'Entity static info buffer', + this._glyphStorageBuffer[1] = this._device.createBuffer({ + label: 'Glyph storage buffer', size: spriteInfoStorageBufferByteSize * maxRenderedObjects, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this._textureAtlasGpuTexture = this._device.createTexture({ + label: 'Atlas texture', format: 'rgba8unorm', - // TODO: These sizes are shared across all layers - this means scratch is wasting texture space - size: { width: textureAtlas.source.width, height: textureAtlas.source.height, depthOrArrayLayers: 2 }, + // TODO: Dynamically grow/shrink layer count + size: { width: textureAtlas.pageSize, height: textureAtlas.pageSize, depthOrArrayLayers: 2 }, dimension: '2d', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | @@ -247,9 +246,9 @@ export class GpuViewLayerRenderer { label: 'ViewLayer bind group', layout: this._pipeline.getBindGroupLayout(0), entries: [ - // Pass in generically as array? - { binding: BindingId.SpriteInfo0, resource: { buffer: this._spriteInfoStorageBuffer0 } }, - { binding: BindingId.SpriteInfo1, resource: { buffer: this._spriteInfoStorageBuffer1 } }, + // TODO: Pass in generically as array? + { binding: BindingId.GlyphInfo0, resource: { buffer: this._glyphStorageBuffer[0] } }, + { binding: BindingId.GlyphInfo1, resource: { buffer: this._glyphStorageBuffer[1] } }, { binding: BindingId.TextureSampler, resource: sampler }, { binding: BindingId.Texture, resource: this._textureAtlasGpuTexture.createView() }, { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, @@ -303,17 +302,19 @@ export class GpuViewLayerRenderer { } private _updateTextureAtlas() { - if (!GpuViewLayerRenderer._textureAtlas.hasChanges) { + const atlas = GpuViewLayerRenderer._textureAtlas; + if (!atlas.hasChanges) { return; } - GpuViewLayerRenderer._textureAtlas.hasChanges = false; + atlas.hasChanges = false; - { + // TODO: Update only dirty pages + for (const [layerIndex, page] of atlas.pages.entries()) { // TODO: Dynamically set buffer size const bufferSize = spriteInfoStorageBufferByteSize * 10000; const values = new Float32Array(bufferSize / 4); let entryOffset = 0; - for (const glyph of GpuViewLayerRenderer._textureAtlas.scratchGlyphs) { + for (const glyph of page.glyphs) { values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; @@ -322,66 +323,38 @@ export class GpuViewLayerRenderer { values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; entryOffset += SpriteInfoStorageBufferInfo.Size; } - this._device.queue.writeBuffer(this._spriteInfoStorageBuffer0, 0, values); + this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); + console.log(`Layer index ${layerIndex}, glyph count ${Array.from(page.glyphs).length}`); + // TODO: Draw only dirty regions + this._device.queue.copyExternalImageToTexture( + { source: page.source }, + { texture: this._textureAtlasGpuTexture, origin: { x: 0, y: 0, z: layerIndex } }, + { width: page.source.width, height: page.source.height }, + ); } - // TODO: Dynamically set buffer size - { - const bufferSize = spriteInfoStorageBufferByteSize * 10000; - const values = new Float32Array(bufferSize / 4); - let entryOffset = 0; - for (const glyph of GpuViewLayerRenderer._textureAtlas.glyphs) { - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; - entryOffset += SpriteInfoStorageBufferInfo.Size; - } - this._device.queue.writeBuffer(this._spriteInfoStorageBuffer1, 0, values); - } - - // TODO: Draw only dirty regions - this._device.queue.copyExternalImageToTexture( - { source: GpuViewLayerRenderer._textureAtlas.scratchSource }, - { texture: this._textureAtlasGpuTexture, origin: { x: 0, y: 0, z: 0 } }, - { width: GpuViewLayerRenderer._textureAtlas.scratchSource.width, height: GpuViewLayerRenderer._textureAtlas.scratchSource.height }, - ); - this._device.queue.copyExternalImageToTexture( - { source: GpuViewLayerRenderer._textureAtlas.source }, - { texture: this._textureAtlasGpuTexture, origin: { x: 0, y: 0, z: 1 } }, - { width: GpuViewLayerRenderer._textureAtlas.source.width, height: GpuViewLayerRenderer._textureAtlas.source.height }, - ); - GpuViewLayerRenderer._drawToTextureAtlas(this._fileService, this._workspaceContextService); } @debounce(500) private static async _drawToTextureAtlas(fileService: IFileService, workspaceContextService: IWorkspaceContextService) { - const scratchBlob = await GpuViewLayerRenderer._textureAtlas.scratchSource.convertToBlob(); - const blob = await GpuViewLayerRenderer._textureAtlas.source.convertToBlob(); const folders = workspaceContextService.getWorkspace().folders; if (folders.length > 0) { - const usagePreviews = await GpuViewLayerRenderer._textureAtlas.getUsagePreview(); - await Promise.all([ - fileService.writeFile( - URI.joinPath(folders[0].uri, 'atlas_page0_actual.png'), - VSBuffer.wrap(new Uint8Array(await scratchBlob.arrayBuffer())) - ), - fileService.writeFile( - URI.joinPath(folders[0].uri, 'atlas_page0_usage.png'), - VSBuffer.wrap(new Uint8Array(await usagePreviews[0].arrayBuffer())) - ), - fileService.writeFile( - URI.joinPath(folders[0].uri, 'atlas_page1_actual.png'), - VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer())) - ), - fileService.writeFile( - URI.joinPath(folders[0].uri, 'atlas_page1_usage.png'), - VSBuffer.wrap(new Uint8Array(await usagePreviews[1].arrayBuffer())) - ) - ]); + const atlas = GpuViewLayerRenderer._textureAtlas; + const promises = []; + for (const [layerIndex, page] of atlas.pages.entries()) { + promises.push(...[ + fileService.writeFile( + URI.joinPath(folders[0].uri, `atlas_page${layerIndex}_usage.png`), + VSBuffer.wrap(new Uint8Array(await (await page.getUsagePreview()).arrayBuffer())) + ), + fileService.writeFile( + URI.joinPath(folders[0].uri, `atlas_page${layerIndex}_actual.png`), + VSBuffer.wrap(new Uint8Array(await (await page.source.convertToBlob()).arrayBuffer())) + ), + ]); + } + await promises; } } @@ -481,8 +454,9 @@ struct VSOutput { @group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; @group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; -@group(0) @binding(${BindingId.SpriteInfo0}) var spriteInfo0: array; -@group(0) @binding(${BindingId.SpriteInfo1}) var spriteInfo1: array; +// TODO: Make this an array of arrays +@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; +@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; @group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; @group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; @@ -492,11 +466,13 @@ struct VSOutput { @builtin(vertex_index) vertexIndex : u32 ) -> VSOutput { let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; - var spriteInfo = spriteInfo0[0]; + // TODO: Is there a nicer way to init this? + var spriteInfo = glyphInfo0[0]; + let glyphIndex = u32(dynamicUnitInfo.glyphIndex); if (u32(dynamicUnitInfo.textureIndex) == 0) { - spriteInfo = spriteInfo0[u32(dynamicUnitInfo.glyphIndex)]; + spriteInfo = glyphInfo0[glyphIndex]; } else { - spriteInfo = spriteInfo1[u32(dynamicUnitInfo.glyphIndex)]; + spriteInfo = glyphInfo1[glyphIndex]; } var vsOut: VSOutput; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 203821028c9..304736c7a2a 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -30,29 +30,29 @@ function assertIsValidGlyph(glyph: ITextureAtlasGlyph, atlas: TextureAtlas) { // (x,y) are valid coordinates ok(isNumber(glyph.x)); ok(glyph.x >= 0); - ok(glyph.x < atlas.source.width); + ok(glyph.x < atlas.pageSize); ok(isNumber(glyph.y)); ok(glyph.y >= 0); - ok(glyph.y < atlas.source.height); + ok(glyph.y < atlas.pageSize); // (w,h) are valid dimensions ok(isNumber(glyph.w)); ok(glyph.w > 0); - ok(glyph.w < atlas.source.width); + ok(glyph.w < atlas.pageSize); ok(isNumber(glyph.h)); ok(glyph.h > 0); - ok(glyph.h < atlas.source.height); + ok(glyph.h < atlas.pageSize); // (originOffsetX, originOffsetY) are valid offsets ok(isNumber(glyph.originOffsetX)); ok(isNumber(glyph.originOffsetY)); // (x,y) + (w,h) are within the bounds of the atlas - ok(glyph.x + glyph.w <= atlas.source.width); - ok(glyph.y + glyph.h <= atlas.source.height); + ok(glyph.x + glyph.w <= atlas.pageSize); + ok(glyph.y + glyph.h <= atlas.pageSize); // Each of the glyph's outer pixel edges contain at least 1 non-transparent pixel - const ctx = ensureNonNullable(atlas.source.getContext('2d')); + const ctx = ensureNonNullable(atlas.pages[glyph.textureIndex].source.getContext('2d')); const edges = [ ctx.getImageData(glyph.x, glyph.y, glyph.w, 1).data, ctx.getImageData(glyph.x, glyph.y + glyph.h - 1, glyph.w, 1).data, From 1cdda04afe94484bb1174848cbcb097fb76fd1e4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:19:29 -0700 Subject: [PATCH 081/286] Comment --- src/vs/base/common/map.ts | 8 ++++++++ src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index e8bc3445c0b..573522b293c 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -876,6 +876,10 @@ export function mapsStrictEqualIgnoreOrder(a: Map, b: Map { private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {}; @@ -906,6 +910,10 @@ export class TwoKeyMap { private _data: TwoKeyMap> = new TwoKeyMap(); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 4c0801c1317..5fe2448cd05 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -324,7 +324,6 @@ export class GpuViewLayerRenderer { entryOffset += SpriteInfoStorageBufferInfo.Size; } this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); - console.log(`Layer index ${layerIndex}, glyph count ${Array.from(page.glyphs).length}`); // TODO: Draw only dirty regions this._device.queue.copyExternalImageToTexture( { source: page.source }, From 750f53c5626f531990d8e2cdcd7ca09ba7228685 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:25:03 -0700 Subject: [PATCH 082/286] Only update pages on gpu when their version changes --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 2 +- .../editor/browser/view/gpu/atlas/textureAtlas.ts | 11 ----------- .../browser/view/gpu/atlas/textureAtlasPage.ts | 12 +++++++++--- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 14 +++++++++----- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index 7afabefde24..3ac74d5bfc9 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -38,8 +38,8 @@ export interface ITextureAtlasAllocator { } export interface IReadableTextureAtlasPage { + readonly version: number; readonly glyphs: IterableIterator; readonly source: OffscreenCanvas; - readonly hasChanges: boolean; getUsagePreview(): Promise; } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 2ff2ca2a1f1..1ad458ee557 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -31,17 +31,6 @@ export class TextureAtlas extends Disposable { // return this._pages.source; // } - // TODO: Should check changes independently - get hasChanges(): boolean { - // return this._scratchPage.hasChanges || this._pages.hasChanges; - return this._pages.some(e => e.hasChanges); - } - set hasChanges(value: boolean) { - // this._scratchPage.hasChanges = value; - // this._pages.hasChanges = value; - this._pages[0].hasChanges = value; - } - /** * The scratch texture atlas page is a relatively small texture where glyphs that are required * immediately are drawn to which reduces the latency of their first draw. In some future idle diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 90181714e49..664ee173fbc 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -14,6 +14,14 @@ import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TextureAtlasPage extends Disposable implements IReadableTextureAtlasPage { + private _version: number = 0; + + /** + * The version of the texture atlas. This is incremented every time the page's texture changes. + */ + public get version(): number { + return this._version; + } private readonly _canvas: OffscreenCanvas; @@ -33,8 +41,6 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla return this._canvas; } - public hasChanges = false; - // TODO: Should pull in the font size from config instead of random dom node constructor( textureIndex: number, @@ -76,7 +82,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla const glyph = this._allocator.allocate(chars, tokenFg, rasterizedGlyph)!; this._glyphMap.set(chars, tokenFg, glyph); this._glyphInOrderSet.add(glyph); - this.hasChanges = true; + this._version++; if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 5fe2448cd05..99e416541fb 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -71,6 +71,7 @@ export class GpuViewLayerRenderer { private static _textureAtlas: TextureAtlas; private readonly _glyphStorageBuffer: GPUBuffer[] = []; private _textureAtlasGpuTexture!: GPUTexture; + private readonly _textureAtlasGpuTextureVersions: number[] = []; private _initialized = false; @@ -216,6 +217,8 @@ export class GpuViewLayerRenderer { size: spriteInfoStorageBufferByteSize * maxRenderedObjects, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); + this._textureAtlasGpuTextureVersions[0] = 0; + this._textureAtlasGpuTextureVersions[1] = 0; this._textureAtlasGpuTexture = this._device.createTexture({ label: 'Atlas texture', format: 'rgba8unorm', @@ -303,13 +306,13 @@ export class GpuViewLayerRenderer { private _updateTextureAtlas() { const atlas = GpuViewLayerRenderer._textureAtlas; - if (!atlas.hasChanges) { - return; - } - atlas.hasChanges = false; - // TODO: Update only dirty pages for (const [layerIndex, page] of atlas.pages.entries()) { + // Skip the update if it's already the latest version + if (page.version === this._textureAtlasGpuTextureVersions[layerIndex]) { + continue; + } + // TODO: Dynamically set buffer size const bufferSize = spriteInfoStorageBufferByteSize * 10000; const values = new Float32Array(bufferSize / 4); @@ -330,6 +333,7 @@ export class GpuViewLayerRenderer { { texture: this._textureAtlasGpuTexture, origin: { x: 0, y: 0, z: layerIndex } }, { width: page.source.width, height: page.source.height }, ); + this._textureAtlasGpuTextureVersions[layerIndex] = page.version; } GpuViewLayerRenderer._drawToTextureAtlas(this._fileService, this._workspaceContextService); From a9a6db42106ac7d7cf944be697481b3c201d98d8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:30:16 -0700 Subject: [PATCH 083/286] Clean up constants/comments --- .../browser/view/gpu/atlas/textureAtlasPage.ts | 6 +++--- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 17 ++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 664ee173fbc..2bada51266d 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -19,7 +19,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla /** * The version of the texture atlas. This is incremented every time the page's texture changes. */ - public get version(): number { + get version(): number { return this._version; } @@ -29,7 +29,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla // HACK: This is an ordered set of glyphs to be passed to the GPU since currently the shader // uses the index of the glyph. This should be improved to derive from _glyphMap private readonly _glyphInOrderSet: Set = new Set(); - public get glyphs(): IterableIterator { + get glyphs(): IterableIterator { return this._glyphInOrderSet.values(); } @@ -37,7 +37,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla private _colorMap!: string[]; - public get source(): OffscreenCanvas { + get source(): OffscreenCanvas { return this._canvas; } diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 99e416541fb..58c28e10481 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -27,7 +27,9 @@ interface IRendererContext { } const enum Constants { - IndicesPerCell = 6 + IndicesPerCell = 6, + + MaxAtlasPageGlyphCount = 10_000, } const enum SpriteInfoStorageBufferInfo { @@ -202,19 +204,17 @@ export class GpuViewLayerRenderer { } - const maxRenderedObjects = 10000; - /////////////////// // Static buffer // /////////////////// this._glyphStorageBuffer[0] = this._device.createBuffer({ label: 'Glyph storage buffer', - size: spriteInfoStorageBufferByteSize * maxRenderedObjects, + size: spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this._glyphStorageBuffer[1] = this._device.createBuffer({ label: 'Glyph storage buffer', - size: spriteInfoStorageBufferByteSize * maxRenderedObjects, + size: spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this._textureAtlasGpuTextureVersions[0] = 0; @@ -314,7 +314,7 @@ export class GpuViewLayerRenderer { } // TODO: Dynamically set buffer size - const bufferSize = spriteInfoStorageBufferByteSize * 10000; + const bufferSize = spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount; const values = new Float32Array(bufferSize / 4); let entryOffset = 0; for (const glyph of page.glyphs) { @@ -348,11 +348,11 @@ export class GpuViewLayerRenderer { for (const [layerIndex, page] of atlas.pages.entries()) { promises.push(...[ fileService.writeFile( - URI.joinPath(folders[0].uri, `atlas_page${layerIndex}_usage.png`), + URI.joinPath(folders[0].uri, `atlasPage${layerIndex}_usage.png`), VSBuffer.wrap(new Uint8Array(await (await page.getUsagePreview()).arrayBuffer())) ), fileService.writeFile( - URI.joinPath(folders[0].uri, `atlas_page${layerIndex}_actual.png`), + URI.joinPath(folders[0].uri, `atlasPage${layerIndex}_actual.png`), VSBuffer.wrap(new Uint8Array(await (await page.source.convertToBlob()).arrayBuffer())) ), ]); @@ -377,7 +377,6 @@ export class GpuViewLayerRenderer { private _render(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { const visibleObjectCount = this._renderStrategy.update(ctx, startLineNumber, stopLineNumber, deltaTop); - // TODO: Only do this when needed this._updateTextureAtlas(); const encoder = this._device.createCommandEncoder(); From c1da6fb545ab5496f59179cd8092350c18bdfba8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:39:23 -0700 Subject: [PATCH 084/286] Clean up hacky types around render pass color attachment and view init --- .../editor/browser/view/gpu/gpuViewLayer.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 58c28e10481..16de574b094 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -63,6 +63,7 @@ export class GpuViewLayerRenderer { private _adapter!: GPUAdapter; private _device!: GPUDevice; private _renderPassDescriptor!: GPURenderPassDescriptor; + private _renderPassColorAttachment!: GPURenderPassColorAttachment; private _bindGroup!: GPUBindGroup; private _pipeline!: GPURenderPipeline; @@ -260,18 +261,14 @@ export class GpuViewLayerRenderer { ], }); + this._renderPassColorAttachment = { + view: null!, // Will be filled at render time + loadOp: 'load', + storeOp: 'store', + }; this._renderPassDescriptor = { label: 'ViewLayer render pass', - colorAttachments: [ - ( - { - // view: <- to be filled out when we render - loadValue: [0, 0, 0, 0], - loadOp: 'load', - storeOp: 'store', - } as Omit - ) as any as GPURenderPassColorAttachment, - ] as any as Iterable, + colorAttachments: [this._renderPassColorAttachment], }; @@ -381,7 +378,7 @@ export class GpuViewLayerRenderer { const encoder = this._device.createCommandEncoder(); - (this._renderPassDescriptor.colorAttachments as any)[0].view = this._gpuCtx.getCurrentTexture().createView(); + this._renderPassColorAttachment.view = this._gpuCtx.getCurrentTexture().createView(); const pass = encoder.beginRenderPass(this._renderPassDescriptor); pass.setPipeline(this._pipeline); pass.setVertexBuffer(0, this._vertexBuffer); From 06d9d3aa36184ae958fc90c7100389c62ecaa5a1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:41:06 -0700 Subject: [PATCH 085/286] Use atlas instead of texture atlas in gpuviewlayer --- .../editor/browser/view/gpu/gpuViewLayer.ts | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 16de574b094..af82fb51eb9 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -71,10 +71,10 @@ export class GpuViewLayerRenderer { private _vertexBuffer!: GPUBuffer; private _squareVertices!: { vertexData: Float32Array; numVertices: number }; - private static _textureAtlas: TextureAtlas; + private static _atlas: TextureAtlas; private readonly _glyphStorageBuffer: GPUBuffer[] = []; - private _textureAtlasGpuTexture!: GPUTexture; - private readonly _textureAtlasGpuTextureVersions: number[] = []; + private _atlasGpuTexture!: GPUTexture; + private readonly _atlasGpuTextureVersions: number[] = []; private _initialized = false; @@ -116,13 +116,13 @@ export class GpuViewLayerRenderer { // Create texture atlas - if (!GpuViewLayerRenderer._textureAtlas) { - GpuViewLayerRenderer._textureAtlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, this._device.limits.maxTextureDimension2D); + if (!GpuViewLayerRenderer._atlas) { + GpuViewLayerRenderer._atlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, this._device.limits.maxTextureDimension2D); } - const textureAtlas = GpuViewLayerRenderer._textureAtlas; + const atlas = GpuViewLayerRenderer._atlas; - this._renderStrategy = this._instantiationService.createInstance(FullFileRenderStrategy, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._textureAtlas); + this._renderStrategy = this._instantiationService.createInstance(FullFileRenderStrategy, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._atlas); const module = this._device.createShaderModule({ label: 'ViewLayer shader module', @@ -199,8 +199,8 @@ export class GpuViewLayerRenderer { { const uniformValues = new Float32Array(TextureInfoUniformBufferInfo.Size); // TODO: Update on canvas resize - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = textureAtlas.pageSize; - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = textureAtlas.pageSize; + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = atlas.pageSize; + uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = atlas.pageSize; this._device.queue.writeBuffer(textureInfoUniformBuffer, 0, uniformValues); } @@ -218,13 +218,13 @@ export class GpuViewLayerRenderer { size: spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); - this._textureAtlasGpuTextureVersions[0] = 0; - this._textureAtlasGpuTextureVersions[1] = 0; - this._textureAtlasGpuTexture = this._device.createTexture({ + this._atlasGpuTextureVersions[0] = 0; + this._atlasGpuTextureVersions[1] = 0; + this._atlasGpuTexture = this._device.createTexture({ label: 'Atlas texture', format: 'rgba8unorm', // TODO: Dynamically grow/shrink layer count - size: { width: textureAtlas.pageSize, height: textureAtlas.pageSize, depthOrArrayLayers: 2 }, + size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: 2 }, dimension: '2d', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | @@ -232,7 +232,7 @@ export class GpuViewLayerRenderer { }); - this._updateTextureAtlas(); + this._updateAtlas(); @@ -254,7 +254,7 @@ export class GpuViewLayerRenderer { { binding: BindingId.GlyphInfo0, resource: { buffer: this._glyphStorageBuffer[0] } }, { binding: BindingId.GlyphInfo1, resource: { buffer: this._glyphStorageBuffer[1] } }, { binding: BindingId.TextureSampler, resource: sampler }, - { binding: BindingId.Texture, resource: this._textureAtlasGpuTexture.createView() }, + { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, ...this._renderStrategy.bindGroupEntries @@ -301,12 +301,12 @@ export class GpuViewLayerRenderer { this.viewportData = viewportData; } - private _updateTextureAtlas() { - const atlas = GpuViewLayerRenderer._textureAtlas; + private _updateAtlas() { + const atlas = GpuViewLayerRenderer._atlas; for (const [layerIndex, page] of atlas.pages.entries()) { // Skip the update if it's already the latest version - if (page.version === this._textureAtlasGpuTextureVersions[layerIndex]) { + if (page.version === this._atlasGpuTextureVersions[layerIndex]) { continue; } @@ -327,20 +327,20 @@ export class GpuViewLayerRenderer { // TODO: Draw only dirty regions this._device.queue.copyExternalImageToTexture( { source: page.source }, - { texture: this._textureAtlasGpuTexture, origin: { x: 0, y: 0, z: layerIndex } }, + { texture: this._atlasGpuTexture, origin: { x: 0, y: 0, z: layerIndex } }, { width: page.source.width, height: page.source.height }, ); - this._textureAtlasGpuTextureVersions[layerIndex] = page.version; + this._atlasGpuTextureVersions[layerIndex] = page.version; } - GpuViewLayerRenderer._drawToTextureAtlas(this._fileService, this._workspaceContextService); + GpuViewLayerRenderer._drawToAtlas(this._fileService, this._workspaceContextService); } @debounce(500) - private static async _drawToTextureAtlas(fileService: IFileService, workspaceContextService: IWorkspaceContextService) { + private static async _drawToAtlas(fileService: IFileService, workspaceContextService: IWorkspaceContextService) { const folders = workspaceContextService.getWorkspace().folders; if (folders.length > 0) { - const atlas = GpuViewLayerRenderer._textureAtlas; + const atlas = GpuViewLayerRenderer._atlas; const promises = []; for (const [layerIndex, page] of atlas.pages.entries()) { promises.push(...[ @@ -374,7 +374,7 @@ export class GpuViewLayerRenderer { private _render(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { const visibleObjectCount = this._renderStrategy.update(ctx, startLineNumber, stopLineNumber, deltaTop); - this._updateTextureAtlas(); + this._updateAtlas(); const encoder = this._device.createCommandEncoder(); @@ -530,7 +530,7 @@ class FullFileRenderStrategy implements IRenderStrategy< private readonly _device: GPUDevice, private readonly _canvas: HTMLCanvasElement, private readonly _viewportData: ViewportData, - private readonly _textureAtlas: TextureAtlas, + private readonly _atlas: TextureAtlas, @IThemeService private readonly _themeService: IThemeService, ) { // TODO: Detect when lines have been tokenized and clear _upToDateLines @@ -679,7 +679,7 @@ class FullFileRenderStrategy implements IRenderStrategy< continue; } - glyph = this._textureAtlas.getGlyph(chars, tokenFg); + glyph = this._atlas.getGlyph(chars, tokenFg); screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); From ccb2817632da01cc1e169459cf72b61fad4d787d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:48:06 -0700 Subject: [PATCH 086/286] Give labels to all webgpu objects --- .../editor/browser/view/gpu/gpuViewLayer.ts | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index af82fb51eb9..d8ea5a269c0 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -48,7 +48,7 @@ const enum BindingId { TextureSampler, Texture, Uniforms, - TextureInfoUniform, + AtlasInfoUniform, ScrollOffset, } @@ -125,12 +125,12 @@ export class GpuViewLayerRenderer { this._renderStrategy = this._instantiationService.createInstance(FullFileRenderStrategy, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._atlas); const module = this._device.createShaderModule({ - label: 'ViewLayer shader module', + label: 'Monaco shader module', code: this._renderStrategy.wgsl, }); this._pipeline = this._device.createRenderPipeline({ - label: 'ViewLayer render pipeline', + label: 'Monaco render pipeline', layout: 'auto', vertex: { module, @@ -174,6 +174,7 @@ export class GpuViewLayerRenderer { OffsetCanvasHeight = 1 } const uniformBuffer = this._device.createBuffer({ + label: 'Monaco uniform buffer', size: UniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); @@ -187,21 +188,21 @@ export class GpuViewLayerRenderer { - const enum TextureInfoUniformBufferInfo { + const enum AtlasInfoUniformBufferInfo { Size = 2, SpriteSheetSize = 0, } - const textureInfoUniformBufferSize = TextureInfoUniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; - const textureInfoUniformBuffer = this._device.createBuffer({ - size: textureInfoUniformBufferSize, + const atlasInfoUniformBufferSize = AtlasInfoUniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; + const atlasInfoUniformBuffer = this._device.createBuffer({ + label: 'Monaco atlas info uniform buffer', + size: atlasInfoUniformBufferSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); { - const uniformValues = new Float32Array(TextureInfoUniformBufferInfo.Size); - // TODO: Update on canvas resize - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize] = atlas.pageSize; - uniformValues[TextureInfoUniformBufferInfo.SpriteSheetSize + 1] = atlas.pageSize; - this._device.queue.writeBuffer(textureInfoUniformBuffer, 0, uniformValues); + const uniformValues = new Float32Array(AtlasInfoUniformBufferInfo.Size); + uniformValues[AtlasInfoUniformBufferInfo.SpriteSheetSize] = atlas.pageSize; + uniformValues[AtlasInfoUniformBufferInfo.SpriteSheetSize + 1] = atlas.pageSize; + this._device.queue.writeBuffer(atlasInfoUniformBuffer, 0, uniformValues); } @@ -209,19 +210,19 @@ export class GpuViewLayerRenderer { // Static buffer // /////////////////// this._glyphStorageBuffer[0] = this._device.createBuffer({ - label: 'Glyph storage buffer', + label: 'Monaco glyph storage buffer', size: spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this._glyphStorageBuffer[1] = this._device.createBuffer({ - label: 'Glyph storage buffer', + label: 'Monaco glyph storage buffer', size: spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this._atlasGpuTextureVersions[0] = 0; this._atlasGpuTextureVersions[1] = 0; this._atlasGpuTexture = this._device.createTexture({ - label: 'Atlas texture', + label: 'Monaco atlas texture', format: 'rgba8unorm', // TODO: Dynamically grow/shrink layer count size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: 2 }, @@ -243,11 +244,12 @@ export class GpuViewLayerRenderer { const sampler = this._device.createSampler({ + label: 'Monaco atlas sampler', magFilter: 'nearest', minFilter: 'nearest', }); this._bindGroup = this._device.createBindGroup({ - label: 'ViewLayer bind group', + label: 'Monaco bind group', layout: this._pipeline.getBindGroupLayout(0), entries: [ // TODO: Pass in generically as array? @@ -256,7 +258,7 @@ export class GpuViewLayerRenderer { { binding: BindingId.TextureSampler, resource: sampler }, { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, - { binding: BindingId.TextureInfoUniform, resource: { buffer: textureInfoUniformBuffer } }, + { binding: BindingId.AtlasInfoUniform, resource: { buffer: atlasInfoUniformBuffer } }, ...this._renderStrategy.bindGroupEntries ], }); @@ -267,7 +269,7 @@ export class GpuViewLayerRenderer { storeOp: 'store', }; this._renderPassDescriptor = { - label: 'ViewLayer render pass', + label: 'Monaco render pass', colorAttachments: [this._renderPassColorAttachment], }; @@ -290,7 +292,7 @@ export class GpuViewLayerRenderer { const { vertexData } = this._squareVertices; this._vertexBuffer = this._device.createBuffer({ - label: 'vertex buffer vertices', + label: 'Monaco quad vertex buffer', size: vertexData.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); @@ -376,7 +378,7 @@ export class GpuViewLayerRenderer { this._updateAtlas(); - const encoder = this._device.createCommandEncoder(); + const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' }); this._renderPassColorAttachment.view = this._gpuCtx.getCurrentTexture().createView(); const pass = encoder.beginRenderPass(this._renderPassDescriptor); @@ -419,11 +421,11 @@ struct Uniforms { canvasDimensions: vec2f, }; -struct TextureInfoUniform { +struct AtlasInfoUniform { spriteSheetSize: vec2f, } -struct SpriteInfo { +struct GlyphInfo { position: vec2f, size: vec2f, origin: vec2f, @@ -451,11 +453,10 @@ struct VSOutput { }; @group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; -@group(0) @binding(${BindingId.TextureInfoUniform}) var textureInfoUniform: TextureInfoUniform; +@group(0) @binding(${BindingId.AtlasInfoUniform}) var atlasInfoUniform: AtlasInfoUniform; -// TODO: Make this an array of arrays -@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; -@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; +@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; +@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; @group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; @group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; @@ -466,18 +467,18 @@ struct VSOutput { ) -> VSOutput { let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; // TODO: Is there a nicer way to init this? - var spriteInfo = glyphInfo0[0]; + var glyph = glyphInfo0[0]; let glyphIndex = u32(dynamicUnitInfo.glyphIndex); if (u32(dynamicUnitInfo.textureIndex) == 0) { - spriteInfo = glyphInfo0[glyphIndex]; + glyph = glyphInfo0[glyphIndex]; } else { - spriteInfo = glyphInfo1[glyphIndex]; + glyph = glyphInfo1[glyphIndex]; } var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position + ((spriteInfo.origin * vec2f(2, -2)) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions), + (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * glyph.size + dynamicUnitInfo.position + ((glyph.origin * vec2f(2, -2)) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions), 0.0, 1.0 ); @@ -487,9 +488,9 @@ struct VSOutput { vsOut.texcoord = vert.position; vsOut.texcoord = ( // Sprite offset (0-1) - (spriteInfo.position / textureInfoUniform.spriteSheetSize) + + (glyph.position / atlasInfoUniform.spriteSheetSize) + // Sprite coordinate (0-1) - (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize)) + (vsOut.texcoord * (glyph.size / atlasInfoUniform.spriteSheetSize)) ); return vsOut; From e29313cd0745df56aeb2b58906b693c27c6046aa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:50:20 -0700 Subject: [PATCH 087/286] Adding missing labels --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index d8ea5a269c0..9933c3fea2d 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -380,7 +380,7 @@ export class GpuViewLayerRenderer { const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' }); - this._renderPassColorAttachment.view = this._gpuCtx.getCurrentTexture().createView(); + this._renderPassColorAttachment.view = this._gpuCtx.getCurrentTexture().createView({ label: 'Monaco canvas texture view' }); const pass = encoder.beginRenderPass(this._renderPassDescriptor); pass.setPipeline(this._pipeline); pass.setVertexBuffer(0, this._vertexBuffer); @@ -542,7 +542,7 @@ class FullFileRenderStrategy implements IRenderStrategy< initBuffers(): void { const bufferSize = FullFileRenderStrategy._lineCount * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._device.createBuffer({ - label: 'Full file cell buffer', + label: 'Monaco full file cell buffer', size: bufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); @@ -553,7 +553,7 @@ class FullFileRenderStrategy implements IRenderStrategy< const scrollOffsetBufferSize = 2; this._scrollOffsetBindBuffer = this._device.createBuffer({ - label: 'Scroll offset buffer', + label: 'Monaco scroll offset buffer', size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); From aef2733ea8111c49cd543d4f1e315537e3bbbaf3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 06:27:45 -0700 Subject: [PATCH 088/286] Only copy used parts of texture atlas This will slow as the page gets filled but this simple change improves start up quite a bit --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 1 + .../editor/browser/view/gpu/atlas/textureAtlasPage.ts | 10 +++++++++- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 5 ++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index 3ac74d5bfc9..51b15e6c1d9 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -39,6 +39,7 @@ export interface ITextureAtlasAllocator { export interface IReadableTextureAtlasPage { readonly version: number; + readonly usedArea: Readonly; readonly glyphs: IterableIterator; readonly source: OffscreenCanvas; getUsagePreview(): Promise; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 2bada51266d..a78fc546edb 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { TwoKeyMap } from 'vs/base/common/map'; -import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import type { IBoundingBox, IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -16,6 +16,11 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TextureAtlasPage extends Disposable implements IReadableTextureAtlasPage { private _version: number = 0; + private _usedArea: IBoundingBox = { left: 0, top: 0, right: 0, bottom: 0 }; + public get usedArea(): Readonly { + return this._usedArea; + } + /** * The version of the texture atlas. This is incremented every time the page's texture changes. */ @@ -82,7 +87,10 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla const glyph = this._allocator.allocate(chars, tokenFg, rasterizedGlyph)!; this._glyphMap.set(chars, tokenFg, glyph); this._glyphInOrderSet.add(glyph); + this._version++; + this._usedArea.right = Math.max(this._usedArea.right, glyph.x + glyph.w); + this._usedArea.bottom = Math.max(this._usedArea.bottom, glyph.y + glyph.h); if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 9933c3fea2d..b3fbe758e71 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -326,11 +326,10 @@ export class GpuViewLayerRenderer { entryOffset += SpriteInfoStorageBufferInfo.Size; } this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); - // TODO: Draw only dirty regions this._device.queue.copyExternalImageToTexture( { source: page.source }, - { texture: this._atlasGpuTexture, origin: { x: 0, y: 0, z: layerIndex } }, - { width: page.source.width, height: page.source.height }, + { texture: this._atlasGpuTexture, origin: { x: page.usedArea.left, y: page.usedArea.top, z: layerIndex } }, + { width: page.usedArea.right - page.usedArea.left, height: page.usedArea.bottom - page.usedArea.top }, ); this._atlasGpuTextureVersions[layerIndex] = page.version; } From e9635663752d54733fab20bb7e1e2ffb42a5ed73 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 06:50:37 -0700 Subject: [PATCH 089/286] Remove scratch page for now This is more difficult than iniitally anticipated due to restrictions on branching in a fragment shader --- .../browser/view/gpu/atlas/textureAtlas.ts | 30 ------------------- .../view/gpu/raster/glyphRasterizer.ts | 1 + 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 1ad458ee557..3af8e37e86e 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -14,34 +14,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TextureAtlas extends Disposable { - // TODO: Expose all page glyphs - the glyphs will need a textureId association - public get scratchGlyphs(): IterableIterator { - return this._scratchPage.glyphs; - } - // public get glyphs(): IterableIterator { - // return this._pages.glyphs; - // } - private readonly _glyphRasterizer: GlyphRasterizer; private _colorMap!: string[]; private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); - // get source(): OffscreenCanvas { - // return this._pages.source; - // } - - /** - * The scratch texture atlas page is a relatively small texture where glyphs that are required - * immediately are drawn to which reduces the latency of their first draw. In some future idle - * callback, the glyph will be transferred into one of the main pages. - */ - private readonly _scratchPage: TextureAtlasPage; - // TODO: Generically get pages externally - gpu shouldn't care about the details of the pages - get scratchSource(): OffscreenCanvas { - return this._scratchPage.source; - } - /** * The main texture atlas pages which are both larger textures and more efficiently packed * relative to the scratch page. The idea is the main pages are drawn to and uploaded to the GPU @@ -78,13 +55,6 @@ export class TextureAtlas extends Disposable { const dprFactor = Math.max(1, Math.floor(activeWindow.devicePixelRatio)); - // TODO: Scratch should be smaller - // TODO: Hook up scratch page to renderer - const scratchPageSize = Math.min(1024 * dprFactor, this._maxTextureSize); - // TODO: General way of assigning texture identifier - // TODO: Identify texture via a name, the texture index should be only known to the GPU code - this._scratchPage = this._register(this._instantiationService.createInstance(TextureAtlasPage, 0, scratchPageSize, 'shelf', this._glyphRasterizer)); - this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, 'slab', this._glyphRasterizer)); this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 1, this.pageSize, 'slab', this._glyphRasterizer)); diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 82a2f75910a..1ff452a119c 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -48,6 +48,7 @@ export class GlyphRasterizer extends Disposable { * therefore is only safe for synchronous access. */ public rasterizeGlyph(chars: string, fg: string): IRasterizedGlyph { + // TODO: Support workbench.fontAliasing this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); // TODO: Draw in middle using alphabetical baseline From b342821bbc030f17df0f27e0d30b21b6f770e6b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 06:54:11 -0700 Subject: [PATCH 090/286] Safer readonly access of atlas glyphs --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 6 +++--- src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts | 4 ++-- .../browser/view/gpu/atlas/textureAtlasSlabAllocator.ts | 2 +- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 2 +- src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts | 2 +- .../editor/test/browser/view/gpu/atlas/textureAtlas.test.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index 51b15e6c1d9..4b80eeb2575 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -25,12 +25,12 @@ export interface IBoundingBox { } export interface ITextureAtlasAllocator { - readonly glyphMap: TwoKeyMap; + readonly glyphMap: TwoKeyMap>; /** * Allocates a rasterized glyph to the canvas, drawing it and returning information on its * position in the canvas. This will return undefined if the glyph does not fit on the canvas. */ - allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined; + allocate(chars: string, tokenFg: number, rasterizedGlyph: Readonly): Readonly | undefined; /** * Gets a usage preview of the atlas for debugging purposes. */ @@ -40,7 +40,7 @@ export interface ITextureAtlasAllocator { export interface IReadableTextureAtlasPage { readonly version: number; readonly usedArea: Readonly; - readonly glyphs: IterableIterator; + readonly glyphs: IterableIterator>; readonly source: OffscreenCanvas; getUsagePreview(): Promise; } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index a78fc546edb..bbb5cba0d16 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -77,11 +77,11 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } // TODO: Color, style etc. - public getGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { + public getGlyph(chars: string, tokenFg: number): Readonly { return this._glyphMap.get(chars, tokenFg) ?? this._createGlyph(chars, tokenFg); } - private _createGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { + private _createGlyph(chars: string, tokenFg: number): Readonly { const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg]); // TODO: Handle undefined allocate result const glyph = this._allocator.allocate(chars, tokenFg, rasterizedGlyph)!; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index e2956498e5c..36d28efe8b3 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -36,7 +36,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private _openRegionsByHeight: Map = new Map(); private _openRegionsByWidth: Map = new Map(); - readonly glyphMap: TwoKeyMap = new TwoKeyMap(); + readonly glyphMap: TwoKeyMap> = new TwoKeyMap(); private readonly _slabW: number; private readonly _slabH: number; diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index b3fbe758e71..bf90be2c2a3 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -575,7 +575,7 @@ class FullFileRenderStrategy implements IRenderStrategy< let wgslX = 0; let wgslY = 0; let xOffset = 0; - let glyph: ITextureAtlasGlyph; + let glyph: Readonly; let cellIndex = 0; let tokenStartIndex = 0; let tokenEndIndex = 0; diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 1ff452a119c..2c01c7ba3b5 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -47,7 +47,7 @@ export class GlyphRasterizer extends Disposable { * Rasterizes a glyph. Note that the returned object is reused across different glyphs and * therefore is only safe for synchronous access. */ - public rasterizeGlyph(chars: string, fg: string): IRasterizedGlyph { + public rasterizeGlyph(chars: string, fg: string): Readonly { // TODO: Support workbench.fontAliasing this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 304736c7a2a..5fb29bcd299 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -26,7 +26,7 @@ function getUniqueGlyphId(): [chars: string, tokenFg: number] { return [lastUniqueGlyph, blackInt]; } -function assertIsValidGlyph(glyph: ITextureAtlasGlyph, atlas: TextureAtlas) { +function assertIsValidGlyph(glyph: Readonly, atlas: TextureAtlas) { // (x,y) are valid coordinates ok(isNumber(glyph.x)); ok(glyph.x >= 0); From d56a4b1fb039081d3d1e79d59aaf22457d1303f0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 07:01:19 -0700 Subject: [PATCH 091/286] Make GpuViewLayerRenderer owner of GlyphRasterizer TextureAtlas is shared across all renderers, but different renderers could have different font options --- .../editor/browser/view/gpu/atlas/textureAtlas.ts | 15 +++------------ src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 14 ++++++++++++-- .../browser/view/gpu/atlas/textureAtlas.test.ts | 15 +++++---------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 3af8e37e86e..2595559f98c 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -14,8 +14,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; export class TextureAtlas extends Disposable { - private readonly _glyphRasterizer: GlyphRasterizer; - private _colorMap!: string[]; private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); @@ -32,7 +30,7 @@ export class TextureAtlas extends Disposable { readonly pageSize: number; constructor( - parentDomNode: HTMLElement, + private readonly _glyphRasterizer: GlyphRasterizer, /** The maximum texture size supported by the GPU. */ private readonly _maxTextureSize: number, @IThemeService private readonly _themeService: IThemeService, @@ -40,20 +38,13 @@ export class TextureAtlas extends Disposable { ) { super(); - // TODO: Should pull in the font size from config instead of random dom node - const activeWindow = getActiveWindow(); - const style = activeWindow.getComputedStyle(parentDomNode); - const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); - this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { // TODO: Clear entire atlas on theme change this._colorMap = this._themeService.getColorTheme().tokenColorMap; this._warmUpAtlas(); })); - this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, style.fontFamily)); - - const dprFactor = Math.max(1, Math.floor(activeWindow.devicePixelRatio)); + const dprFactor = Math.max(1, Math.floor(getActiveWindow().devicePixelRatio)); this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, 'slab', this._glyphRasterizer)); @@ -62,7 +53,7 @@ export class TextureAtlas extends Disposable { } // TODO: Color, style etc. - public getGlyph(chars: string, tokenFg: number): ITextureAtlasGlyph { + public getGlyph(chars: string, tokenFg: number): Readonly { // HACK: Draw glyphs to different pages to test out multiple textures while there's no overflow logic const targetPage = chars.match(/[a-z]/i) ? 0 : 1; return this._pages[targetPage].getGlyph(chars, tokenFg); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index bf90be2c2a3..b67be1eee23 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -9,6 +9,7 @@ import { debounce } from 'vs/base/common/decorators'; import { URI } from 'vs/base/common/uri'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -67,11 +68,12 @@ export class GpuViewLayerRenderer { private _bindGroup!: GPUBindGroup; private _pipeline!: GPURenderPipeline; - private _vertexBuffer!: GPUBuffer; private _squareVertices!: { vertexData: Float32Array; numVertices: number }; private static _atlas: TextureAtlas; + private _glyphRasterizer: GlyphRasterizer; + private readonly _glyphStorageBuffer: GPUBuffer[] = []; private _atlasGpuTexture!: GPUTexture; private readonly _atlasGpuTextureVersions: number[] = []; @@ -92,6 +94,14 @@ export class GpuViewLayerRenderer { this.host = host; this.viewportData = viewportData; + // TODO: Can the font details come from settings/editor options instead? + const activeWindow = getActiveWindow(); + const style = activeWindow.getComputedStyle(domNode); + const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); + + // TODO: Register this + this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); + this._gpuCtx = this.domNode.getContext('webgpu')!; this.initWebgpu(); } @@ -117,7 +127,7 @@ export class GpuViewLayerRenderer { // Create texture atlas if (!GpuViewLayerRenderer._atlas) { - GpuViewLayerRenderer._atlas = this._instantiationService.createInstance(TextureAtlas, this.domNode, this._device.limits.maxTextureDimension2D); + GpuViewLayerRenderer._atlas = this._instantiationService.createInstance(TextureAtlas, this._glyphRasterizer, this._device.limits.maxTextureDimension2D); } const atlas = GpuViewLayerRenderer._atlas; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 5fb29bcd299..163dc288b66 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { ok } from 'assert'; -import { getActiveWindow } from 'vs/base/browser/dom'; -import { toDisposable } from 'vs/base/common/lifecycle'; import { isNumber } from 'vs/base/common/types'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { createCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; import type { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -72,24 +71,20 @@ suite('TextureAtlas', () => { }); let instantiationService: IInstantiationService; - let parentElement: HTMLElement; + let glyphRasterizer: GlyphRasterizer; setup(() => { instantiationService = createCodeEditorServices(store); - - const doc = getActiveWindow().document; - parentElement = doc.createElement('div'); - doc.body.appendChild(parentElement); - store.add(toDisposable(() => parentElement.remove())); + glyphRasterizer = new GlyphRasterizer(10, 'monospace'); }); test('get single glyph', () => { - const atlas = store.add(instantiationService.createInstance(TextureAtlas, parentElement, 512)); + const atlas = store.add(instantiationService.createInstance(TextureAtlas, glyphRasterizer, 512)); assertIsValidGlyph(atlas.getGlyph(...getUniqueGlyphId()), atlas); }); test('get multiple glyphs', () => { - const atlas = store.add(instantiationService.createInstance(TextureAtlas, parentElement, 512)); + const atlas = store.add(instantiationService.createInstance(TextureAtlas, glyphRasterizer, 512)); for (let i = 0; i < 10; i++) { assertIsValidGlyph(atlas.getGlyph(...getUniqueGlyphId()), atlas); } From a3e2f769a8d56d2ab2dd034f88f62bd7178e327b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 08:34:15 -0700 Subject: [PATCH 092/286] Warm up all rasterizers --- .../browser/view/gpu/atlas/textureAtlas.ts | 25 +++++++++++-------- .../view/gpu/atlas/textureAtlasPage.ts | 9 +++---- .../editor/browser/view/gpu/gpuViewLayer.ts | 24 ++++++++++-------- .../view/gpu/raster/glyphRasterizer.ts | 13 +++++++++- .../view/gpu/atlas/textureAtlas.test.ts | 8 +++--- 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 2595559f98c..27b2e592114 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -17,6 +17,8 @@ export class TextureAtlas extends Disposable { private _colorMap!: string[]; private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); + private readonly _warmedUpRasterizers = new Set(); + /** * The main texture atlas pages which are both larger textures and more efficiently packed * relative to the scratch page. The idea is the main pages are drawn to and uploaded to the GPU @@ -30,7 +32,6 @@ export class TextureAtlas extends Disposable { readonly pageSize: number; constructor( - private readonly _glyphRasterizer: GlyphRasterizer, /** The maximum texture size supported by the GPU. */ private readonly _maxTextureSize: number, @IThemeService private readonly _themeService: IThemeService, @@ -41,26 +42,28 @@ export class TextureAtlas extends Disposable { this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { // TODO: Clear entire atlas on theme change this._colorMap = this._themeService.getColorTheme().tokenColorMap; - this._warmUpAtlas(); })); const dprFactor = Math.max(1, Math.floor(getActiveWindow().devicePixelRatio)); this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); - this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, 'slab', this._glyphRasterizer)); - this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 1, this.pageSize, 'slab', this._glyphRasterizer)); + this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, 'slab')); + this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 1, this.pageSize, 'slab')); this._register(toDisposable(() => dispose(this._pages))); } // TODO: Color, style etc. - public getGlyph(chars: string, tokenFg: number): Readonly { + public getGlyph(rasterizer: GlyphRasterizer, chars: string, tokenFg: number): Readonly { + if (!this._warmedUpRasterizers.has(rasterizer.id)) { + this._warmUpAtlas(rasterizer); + this._warmedUpRasterizers.add(rasterizer.id); + } // HACK: Draw glyphs to different pages to test out multiple textures while there's no overflow logic const targetPage = chars.match(/[a-z]/i) ? 0 : 1; - return this._pages[targetPage].getGlyph(chars, tokenFg); + return this._pages[targetPage].getGlyph(rasterizer, chars, tokenFg); } public getUsagePreview(): Promise { - // TODO: Include scratch page return Promise.all(this._pages.map(e => e.getUsagePreview())); } @@ -68,7 +71,7 @@ export class TextureAtlas extends Disposable { * Warms up the atlas by rasterizing all printable ASCII characters for each token color. This * is distrubuted over multiple idle callbacks to avoid blocking the main thread. */ - private _warmUpAtlas(): void { + private _warmUpAtlas(rasterizer: GlyphRasterizer): void { this._warmUpTask.value?.clear(); const taskQueue = this._warmUpTask.value = new IdleTaskQueue(); // Warm up using roughly the larger glyphs first to help optimize atlas allocation @@ -76,7 +79,7 @@ export class TextureAtlas extends Disposable { for (let code = 65; code <= 90; code++) { taskQueue.enqueue(() => { for (const tokenFg of this._colorMap.keys()) { - this.getGlyph(String.fromCharCode(code), tokenFg); + this.getGlyph(rasterizer, String.fromCharCode(code), tokenFg); } }); } @@ -84,7 +87,7 @@ export class TextureAtlas extends Disposable { for (let code = 97; code <= 122; code++) { taskQueue.enqueue(() => { for (const tokenFg of this._colorMap.keys()) { - this.getGlyph(String.fromCharCode(code), tokenFg); + this.getGlyph(rasterizer, String.fromCharCode(code), tokenFg); } }); } @@ -92,7 +95,7 @@ export class TextureAtlas extends Disposable { for (let code = 33; code <= 126; code++) { taskQueue.enqueue(() => { for (const tokenFg of this._colorMap.keys()) { - this.getGlyph(String.fromCharCode(code), tokenFg); + this.getGlyph(rasterizer, String.fromCharCode(code), tokenFg); } }); } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index bbb5cba0d16..1672308b595 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -51,7 +51,6 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla textureIndex: number, pageSize: number, allocatorType: 'shelf' | 'slab', - private readonly _glyphRasterizer: GlyphRasterizer, @ILogService private readonly _logService: ILogService, @IThemeService private readonly _themeService: IThemeService, ) { @@ -77,12 +76,12 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } // TODO: Color, style etc. - public getGlyph(chars: string, tokenFg: number): Readonly { - return this._glyphMap.get(chars, tokenFg) ?? this._createGlyph(chars, tokenFg); + public getGlyph(rasterizer: GlyphRasterizer, chars: string, tokenFg: number): Readonly { + return this._glyphMap.get(chars, tokenFg) ?? this._createGlyph(rasterizer, chars, tokenFg); } - private _createGlyph(chars: string, tokenFg: number): Readonly { - const rasterizedGlyph = this._glyphRasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg]); + private _createGlyph(rasterizer: GlyphRasterizer, chars: string, tokenFg: number): Readonly { + const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg], false); // TODO: Handle undefined allocate result const glyph = this._allocator.allocate(chars, tokenFg, rasterizedGlyph)!; this._glyphMap.set(chars, tokenFg, glyph); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index b67be1eee23..6e7bed3f952 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -72,7 +72,7 @@ export class GpuViewLayerRenderer { private _squareVertices!: { vertexData: Float32Array; numVertices: number }; private static _atlas: TextureAtlas; - private _glyphRasterizer: GlyphRasterizer; + // private _glyphRasterizer: GlyphRasterizer; private readonly _glyphStorageBuffer: GPUBuffer[] = []; private _atlasGpuTexture!: GPUTexture; @@ -94,14 +94,6 @@ export class GpuViewLayerRenderer { this.host = host; this.viewportData = viewportData; - // TODO: Can the font details come from settings/editor options instead? - const activeWindow = getActiveWindow(); - const style = activeWindow.getComputedStyle(domNode); - const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); - - // TODO: Register this - this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); - this._gpuCtx = this.domNode.getContext('webgpu')!; this.initWebgpu(); } @@ -127,7 +119,7 @@ export class GpuViewLayerRenderer { // Create texture atlas if (!GpuViewLayerRenderer._atlas) { - GpuViewLayerRenderer._atlas = this._instantiationService.createInstance(TextureAtlas, this._glyphRasterizer, this._device.limits.maxTextureDimension2D); + GpuViewLayerRenderer._atlas = this._instantiationService.createInstance(TextureAtlas, this._device.limits.maxTextureDimension2D); } const atlas = GpuViewLayerRenderer._atlas; @@ -520,6 +512,8 @@ class FullFileRenderStrategy implements IRenderStrategy< readonly wgsl: string = fullFileRenderStrategyWgsl; + private readonly _glyphRasterizer: GlyphRasterizer; + private _cellBindBuffer!: GPUBuffer; private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; private _activeDoubleBufferIndex: 0 | 1 = 0; @@ -546,6 +540,14 @@ class FullFileRenderStrategy implements IRenderStrategy< // TODO: Detect when lines have been tokenized and clear _upToDateLines const colorMap = this._themeService.getColorTheme().tokenColorMap; console.log('colorMap', colorMap); + + // TODO: Can the font details come from settings/editor options instead? + const activeWindow = getActiveWindow(); + const style = activeWindow.getComputedStyle(this._canvas); + const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); + + // TODO: Register this + this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); } initBuffers(): void { @@ -689,7 +691,7 @@ class FullFileRenderStrategy implements IRenderStrategy< continue; } - glyph = this._atlas.getGlyph(chars, tokenFg); + glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenFg); screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 2c01c7ba3b5..da310f08bee 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -21,7 +21,14 @@ const $rasterizedGlyph: IRasterizedGlyph = { }; const $bbox = $rasterizedGlyph.boundingBox; +let nextId = 0; + export class GlyphRasterizer extends Disposable { + /** + * A unique identifier for this rasterizer. + */ + public readonly id = nextId++; + private _canvas: OffscreenCanvas; // A temporary context that glyphs are drawn to before being transfered to the atlas. private _ctx: OffscreenCanvasRenderingContext2D; @@ -47,7 +54,11 @@ export class GlyphRasterizer extends Disposable { * Rasterizes a glyph. Note that the returned object is reused across different glyphs and * therefore is only safe for synchronous access. */ - public rasterizeGlyph(chars: string, fg: string): Readonly { + public rasterizeGlyph( + chars: string, + fg: string, + isItalic: boolean + ): Readonly { // TODO: Support workbench.fontAliasing this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 163dc288b66..399761b28c3 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -79,14 +79,14 @@ suite('TextureAtlas', () => { }); test('get single glyph', () => { - const atlas = store.add(instantiationService.createInstance(TextureAtlas, glyphRasterizer, 512)); - assertIsValidGlyph(atlas.getGlyph(...getUniqueGlyphId()), atlas); + const atlas = store.add(instantiationService.createInstance(TextureAtlas, 512)); + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); }); test('get multiple glyphs', () => { - const atlas = store.add(instantiationService.createInstance(TextureAtlas, glyphRasterizer, 512)); + const atlas = store.add(instantiationService.createInstance(TextureAtlas, 512)); for (let i = 0; i < 10; i++) { - assertIsValidGlyph(atlas.getGlyph(...getUniqueGlyphId()), atlas); + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); } }); From 085a2b2ea612ce3c6162db81e7048c818ee4889b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:47:57 -0700 Subject: [PATCH 093/286] Use metadata as key, support bold/italic --- .../browser/view/gpu/atlas/textureAtlas.ts | 4 ++-- .../view/gpu/atlas/textureAtlasPage.ts | 14 ++++++------ .../editor/browser/view/gpu/gpuViewLayer.ts | 16 +++++++++----- .../view/gpu/raster/glyphRasterizer.ts | 22 ++++++++++++++----- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 27b2e592114..84c20c0f2b0 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -53,14 +53,14 @@ export class TextureAtlas extends Disposable { } // TODO: Color, style etc. - public getGlyph(rasterizer: GlyphRasterizer, chars: string, tokenFg: number): Readonly { + public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { if (!this._warmedUpRasterizers.has(rasterizer.id)) { this._warmUpAtlas(rasterizer); this._warmedUpRasterizers.add(rasterizer.id); } // HACK: Draw glyphs to different pages to test out multiple textures while there's no overflow logic const targetPage = chars.match(/[a-z]/i) ? 0 : 1; - return this._pages[targetPage].getGlyph(rasterizer, chars, tokenFg); + return this._pages[targetPage].getGlyph(rasterizer, chars, metadata); } public getUsagePreview(): Promise { diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 1672308b595..717500993af 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -76,15 +76,15 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } // TODO: Color, style etc. - public getGlyph(rasterizer: GlyphRasterizer, chars: string, tokenFg: number): Readonly { - return this._glyphMap.get(chars, tokenFg) ?? this._createGlyph(rasterizer, chars, tokenFg); + public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + return this._glyphMap.get(chars, metadata) ?? this._createGlyph(rasterizer, chars, metadata); } - private _createGlyph(rasterizer: GlyphRasterizer, chars: string, tokenFg: number): Readonly { - const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, this._colorMap[tokenFg], false); + private _createGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); // TODO: Handle undefined allocate result - const glyph = this._allocator.allocate(chars, tokenFg, rasterizedGlyph)!; - this._glyphMap.set(chars, tokenFg, glyph); + const glyph = this._allocator.allocate(chars, metadata, rasterizedGlyph)!; + this._glyphMap.set(chars, metadata, glyph); this._glyphInOrderSet.add(glyph); this._version++; @@ -94,7 +94,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { chars, - fg: this._colorMap[tokenFg], + fg: this._colorMap[metadata], rasterizedGlyph, glyph }); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 6e7bed3f952..4c6cf1f0aea 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -512,6 +512,7 @@ class FullFileRenderStrategy implements IRenderStrategy< readonly wgsl: string = fullFileRenderStrategyWgsl; + private readonly _colorMap: string[]; private readonly _glyphRasterizer: GlyphRasterizer; private _cellBindBuffer!: GPUBuffer; @@ -538,8 +539,8 @@ class FullFileRenderStrategy implements IRenderStrategy< @IThemeService private readonly _themeService: IThemeService, ) { // TODO: Detect when lines have been tokenized and clear _upToDateLines - const colorMap = this._themeService.getColorTheme().tokenColorMap; - console.log('colorMap', colorMap); + this._colorMap = this._themeService.getColorTheme().tokenColorMap; + console.log('colorMap', this._colorMap); // TODO: Can the font details come from settings/editor options instead? const activeWindow = getActiveWindow(); @@ -589,9 +590,11 @@ class FullFileRenderStrategy implements IRenderStrategy< let xOffset = 0; let glyph: Readonly; let cellIndex = 0; + let tokenStartIndex = 0; let tokenEndIndex = 0; - let tokenFg = 0; + let tokenMetadata = 0; + let lineData: ViewLineRenderingData; let content: string = ''; let fillStartIndex = 0; @@ -671,7 +674,10 @@ class FullFileRenderStrategy implements IRenderStrategy< // The faux indent part of the line should have no token type continue; } - tokenFg = tokens.getForeground(tokenIndex); + + + tokenMetadata = tokens.getMetadata(tokenIndex); + // console.log(`token: start=${tokenStartIndex}, end=${tokenEndIndex}, fg=${colorMap[tokenFg]}`); @@ -691,7 +697,7 @@ class FullFileRenderStrategy implements IRenderStrategy< continue; } - glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenFg); + glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index da310f08bee..0c353f10f59 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -5,6 +5,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { FontStyle, TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; const $rasterizedGlyph: IRasterizedGlyph = { source: null!, @@ -35,7 +37,7 @@ export class GlyphRasterizer extends Disposable { constructor( private readonly _fontSize: number, - fontFamily: string, + private readonly _fontFamily: string, ) { super(); @@ -43,7 +45,6 @@ export class GlyphRasterizer extends Disposable { this._ctx = ensureNonNullable(this._canvas.getContext('2d', { willReadFrequently: true })); - this._ctx.font = `${this._fontSize}px ${fontFamily}`; this._ctx.textBaseline = 'top'; this._ctx.fillStyle = '#FFFFFF'; } @@ -56,16 +57,27 @@ export class GlyphRasterizer extends Disposable { */ public rasterizeGlyph( chars: string, - fg: string, - isItalic: boolean + metadata: number, + colorMap: string[], ): Readonly { // TODO: Support workbench.fontAliasing this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + const fontSb = new StringBuilder(200); + const fontStyle = TokenMetadata.getFontStyle(metadata); + if (fontStyle & FontStyle.Italic) { + fontSb.appendString('italic '); + } + if (fontStyle & FontStyle.Bold) { + fontSb.appendString('bold '); + } + fontSb.appendString(`${this._fontSize}px ${this._fontFamily}`); + this._ctx.font = fontSb.build(); + // TODO: Draw in middle using alphabetical baseline const originX = this._fontSize; const originY = this._fontSize; - this._ctx.fillStyle = fg; + this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; // TODO: This might actually be slower // const textMetrics = this._ctx.measureText(chars); this._ctx.fillText(chars, originX, originY); From e3e79796cb5250f7de822661ab248d2523dcf3ac Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 09:51:37 -0700 Subject: [PATCH 094/286] Add note about strikethrough/underline --- src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 0c353f10f59..48bdd22e0f6 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -71,6 +71,9 @@ export class GlyphRasterizer extends Disposable { if (fontStyle & FontStyle.Bold) { fontSb.appendString('bold '); } + // TODO: Support FontStyle.Strikethrough and FontStyle.Underline text decorations, these + // need to be drawn manually to the canvas. See xterm.js for "dodging" the text for + // underlines. fontSb.appendString(`${this._fontSize}px ${this._fontFamily}`); this._ctx.font = fontSb.build(); From 340c9eddcf8ebd4dba4eead2090ef05127a2067a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:44:29 -0700 Subject: [PATCH 095/286] Use premultiplied alphaMode --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 1 + src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 4c6cf1f0aea..c1cef06fd6e 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -114,6 +114,7 @@ export class GpuViewLayerRenderer { this._gpuCtx.configure({ device: this._device, format: presentationFormat, + alphaMode: 'premultiplied', }); diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 48bdd22e0f6..981edcbde4e 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -71,11 +71,12 @@ export class GlyphRasterizer extends Disposable { if (fontStyle & FontStyle.Bold) { fontSb.appendString('bold '); } + fontSb.appendString(`${this._fontSize}px ${this._fontFamily}`); + this._ctx.font = fontSb.build(); + // TODO: Support FontStyle.Strikethrough and FontStyle.Underline text decorations, these // need to be drawn manually to the canvas. See xterm.js for "dodging" the text for // underlines. - fontSb.appendString(`${this._fontSize}px ${this._fontFamily}`); - this._ctx.font = fontSb.build(); // TODO: Draw in middle using alphabetical baseline const originX = this._fontSize; From 8c749a292e8d329cc03aceb4095d778aac650a6d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 10:53:00 -0700 Subject: [PATCH 096/286] Get font size/family from ViewContext --- .../editor/browser/view/gpu/gpuViewLayer.ts | 19 ++++++++----------- src/vs/editor/browser/view/viewLayer.ts | 4 +++- src/vs/editor/browser/view/viewOverlays.ts | 2 +- .../browser/viewParts/lines/viewLines.ts | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index c1cef06fd6e..7ef6fe6bbad 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -11,12 +11,13 @@ import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas' import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; +import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export const disableNonGpuRendering = true; @@ -84,6 +85,7 @@ export class GpuViewLayerRenderer { constructor( domNode: HTMLCanvasElement, + private readonly _context: ViewContext, host: IVisibleLinesHost, viewportData: ViewportData, @IFileService private readonly _fileService: IFileService, @@ -125,7 +127,7 @@ export class GpuViewLayerRenderer { const atlas = GpuViewLayerRenderer._atlas; - this._renderStrategy = this._instantiationService.createInstance(FullFileRenderStrategy, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._atlas); + this._renderStrategy = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._atlas); const module = this._device.createShaderModule({ label: 'Monaco shader module', @@ -513,7 +515,6 @@ class FullFileRenderStrategy implements IRenderStrategy< readonly wgsl: string = fullFileRenderStrategyWgsl; - private readonly _colorMap: string[]; private readonly _glyphRasterizer: GlyphRasterizer; private _cellBindBuffer!: GPUBuffer; @@ -533,23 +534,19 @@ class FullFileRenderStrategy implements IRenderStrategy< } constructor( + private readonly _context: ViewContext, private readonly _device: GPUDevice, private readonly _canvas: HTMLCanvasElement, private readonly _viewportData: ViewportData, private readonly _atlas: TextureAtlas, - @IThemeService private readonly _themeService: IThemeService, ) { // TODO: Detect when lines have been tokenized and clear _upToDateLines - this._colorMap = this._themeService.getColorTheme().tokenColorMap; - console.log('colorMap', this._colorMap); - - // TODO: Can the font details come from settings/editor options instead? const activeWindow = getActiveWindow(); - const style = activeWindow.getComputedStyle(this._canvas); - const fontSize = Math.ceil(parseInt(style.fontSize) * activeWindow.devicePixelRatio); + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = Math.ceil(this._context.configuration.options.get(EditorOption.fontSize) * activeWindow.devicePixelRatio); // TODO: Register this - this._glyphRasterizer = new GlyphRasterizer(fontSize, style.fontFamily); + this._glyphRasterizer = new GlyphRasterizer(fontSize, fontFamily); } initBuffers(): void { diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index dc04a0d9ea2..ffee2d4c0b6 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -13,6 +13,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; +import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** @@ -262,6 +263,7 @@ export class VisibleLinesCollection { private readonly _canvas: HTMLCanvasElement; constructor( + private readonly _context: ViewContext, host: IVisibleLinesHost, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { @@ -375,7 +377,7 @@ export class VisibleLinesCollection { } if (!this._gpuRenderer) { - this._gpuRenderer = this._instantiationService.createInstance(GpuViewLayerRenderer, this._canvas, this._host, viewportData); + this._gpuRenderer = this._instantiationService.createInstance(GpuViewLayerRenderer, this._canvas, this._context, this._host, viewportData); } renderer = this._gpuRenderer; renderer.update(viewportData); diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index fe331743db6..b305bb77fee 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -30,7 +30,7 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost, this); + this._visibleLines = instantiationService.createInstance(VisibleLinesCollection, context, this); this.domNode = this._visibleLines.domNode; const options = this._context.configuration.options; diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 0d387704a6f..512b8ae10bc 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -129,7 +129,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, super(context); this._linesContent = linesContent; this._textRangeRestingSpot = document.createElement('div'); - this._visibleLines = instantiationService.createInstance(VisibleLinesCollection, this); + this._visibleLines = instantiationService.createInstance(VisibleLinesCollection, context, this); this.domNode = this._visibleLines.domNode; const conf = this._context.configuration; From 0fc3891d30e0f53d7f725c24ff31e84a9a2e6047 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 11:20:17 -0700 Subject: [PATCH 097/286] Improve var/constant names --- .../editor/browser/view/gpu/gpuViewLayer.ts | 111 +++++++++--------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 7ef6fe6bbad..51d89f26d3c 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -34,23 +34,22 @@ const enum Constants { MaxAtlasPageGlyphCount = 10_000, } -const enum SpriteInfoStorageBufferInfo { - Size = 2 + 2 + 2, +const enum GlyphStorageBufferInfo { + FloatsPerEntry = 2 + 2 + 2, + BytesPerEntry = GlyphStorageBufferInfo.FloatsPerEntry * 4, Offset_TexturePosition = 0, Offset_TextureSize = 2, Offset_OriginPosition = 4, } -const spriteInfoStorageBufferByteSize = SpriteInfoStorageBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; const enum BindingId { - // TODO: Improve names GlyphInfo0, GlyphInfo1, - DynamicUnitInfo, + Cells, TextureSampler, Texture, Uniforms, - AtlasInfoUniform, + AtlasDimensionsUniform, ScrollOffset, } @@ -73,7 +72,6 @@ export class GpuViewLayerRenderer { private _squareVertices!: { vertexData: Float32Array; numVertices: number }; private static _atlas: TextureAtlas; - // private _glyphRasterizer: GlyphRasterizer; private readonly _glyphStorageBuffer: GPUBuffer[] = []; private _atlasGpuTexture!: GPUTexture; @@ -174,39 +172,42 @@ export class GpuViewLayerRenderer { // Write standard uniforms const enum UniformBufferInfo { - Size = 2, // 2x 32 bit floats - OffsetCanvasWidth = 0, - OffsetCanvasHeight = 1 + FloatsPerEntry = 2, + BytesPerEntry = UniformBufferInfo.FloatsPerEntry * 4, + Offset_CanvasWidth = 0, + Offset_CanvasHeight = 1 } const uniformBuffer = this._device.createBuffer({ label: 'Monaco uniform buffer', - size: UniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT, + size: UniformBufferInfo.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); { - const uniformValues = new Float32Array(UniformBufferInfo.Size); + const uniformValues = new Float32Array(UniformBufferInfo.FloatsPerEntry); // TODO: Update on canvas resize - uniformValues[UniformBufferInfo.OffsetCanvasWidth] = this.domNode.width; - uniformValues[UniformBufferInfo.OffsetCanvasHeight] = this.domNode.height; + uniformValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; + uniformValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); } const enum AtlasInfoUniformBufferInfo { - Size = 2, - SpriteSheetSize = 0, + FloatsPerEntry = 2, + BytesPerEntry = AtlasInfoUniformBufferInfo.FloatsPerEntry * 4, + Offset_Width = 0, + Offset_Height = 1, } - const atlasInfoUniformBufferSize = AtlasInfoUniformBufferInfo.Size * Float32Array.BYTES_PER_ELEMENT; + const atlasInfoUniformBufferSize = AtlasInfoUniformBufferInfo.BytesPerEntry; const atlasInfoUniformBuffer = this._device.createBuffer({ label: 'Monaco atlas info uniform buffer', size: atlasInfoUniformBufferSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); { - const uniformValues = new Float32Array(AtlasInfoUniformBufferInfo.Size); - uniformValues[AtlasInfoUniformBufferInfo.SpriteSheetSize] = atlas.pageSize; - uniformValues[AtlasInfoUniformBufferInfo.SpriteSheetSize + 1] = atlas.pageSize; + const uniformValues = new Float32Array(AtlasInfoUniformBufferInfo.FloatsPerEntry); + uniformValues[AtlasInfoUniformBufferInfo.Offset_Width] = atlas.pageSize; + uniformValues[AtlasInfoUniformBufferInfo.Offset_Height] = atlas.pageSize; this._device.queue.writeBuffer(atlasInfoUniformBuffer, 0, uniformValues); } @@ -216,12 +217,12 @@ export class GpuViewLayerRenderer { /////////////////// this._glyphStorageBuffer[0] = this._device.createBuffer({ label: 'Monaco glyph storage buffer', - size: spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount, + size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this._glyphStorageBuffer[1] = this._device.createBuffer({ label: 'Monaco glyph storage buffer', - size: spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount, + size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); this._atlasGpuTextureVersions[0] = 0; @@ -263,7 +264,7 @@ export class GpuViewLayerRenderer { { binding: BindingId.TextureSampler, resource: sampler }, { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, - { binding: BindingId.AtlasInfoUniform, resource: { buffer: atlasInfoUniformBuffer } }, + { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, ...this._renderStrategy.bindGroupEntries ], }); @@ -318,17 +319,17 @@ export class GpuViewLayerRenderer { } // TODO: Dynamically set buffer size - const bufferSize = spriteInfoStorageBufferByteSize * Constants.MaxAtlasPageGlyphCount; + const bufferSize = GlyphStorageBufferInfo.FloatsPerEntry * Constants.MaxAtlasPageGlyphCount; const values = new Float32Array(bufferSize / 4); let entryOffset = 0; for (const glyph of page.glyphs) { - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition] = glyph.x; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize] = glyph.w; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; - values[entryOffset + SpriteInfoStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; - entryOffset += SpriteInfoStorageBufferInfo.Size; + values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition] = glyph.x; + values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; + values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize] = glyph.w; + values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; + values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; + values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; + entryOffset += GlyphStorageBufferInfo.FloatsPerEntry; } this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); this._device.queue.copyExternalImageToTexture( @@ -425,8 +426,8 @@ struct Uniforms { canvasDimensions: vec2f, }; -struct AtlasInfoUniform { - spriteSheetSize: vec2f, +struct AtlasDimensionsUniform { + size: vec2f, } struct GlyphInfo { @@ -439,7 +440,7 @@ struct Vertex { @location(0) position: vec2f, }; -struct DynamicUnitInfo { +struct Cells { position: vec2f, unused1: vec2f, glyphIndex: f32, @@ -451,29 +452,31 @@ struct ScrollOffset { } struct VSOutput { - @builtin(position) position: vec4f, - @location(1) layerIndex: f32, - @location(0) texcoord: vec2f, + @builtin(position) position: vec4f, + @location(1) layerIndex: f32, + @location(0) texcoord: vec2f, }; -@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; -@group(0) @binding(${BindingId.AtlasInfoUniform}) var atlasInfoUniform: AtlasInfoUniform; +// Uniforms +@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; +@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: AtlasDimensionsUniform; +@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; -@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; -@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; -@group(0) @binding(${BindingId.DynamicUnitInfo}) var dynamicUnitInfoStructs: array; -@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; +// Storage buffers +@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; +@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; +@group(0) @binding(${BindingId.Cells}) var cells: array; @vertex fn vs( vert: Vertex, @builtin(instance_index) instanceIndex: u32, @builtin(vertex_index) vertexIndex : u32 ) -> VSOutput { - let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex]; + let cell = cells[instanceIndex]; // TODO: Is there a nicer way to init this? var glyph = glyphInfo0[0]; - let glyphIndex = u32(dynamicUnitInfo.glyphIndex); - if (u32(dynamicUnitInfo.textureIndex) == 0) { + let glyphIndex = u32(cell.glyphIndex); + if (u32(cell.textureIndex) == 0) { glyph = glyphInfo0[glyphIndex]; } else { glyph = glyphInfo1[glyphIndex]; @@ -482,26 +485,26 @@ struct VSOutput { var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * glyph.size + dynamicUnitInfo.position + ((glyph.origin * vec2f(2, -2)) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions), + (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions), 0.0, 1.0 ); - vsOut.layerIndex = dynamicUnitInfo.textureIndex; + vsOut.layerIndex = cell.textureIndex; // Textures are flipped from natural direction on the y-axis, so flip it back vsOut.texcoord = vert.position; vsOut.texcoord = ( - // Sprite offset (0-1) - (glyph.position / atlasInfoUniform.spriteSheetSize) + - // Sprite coordinate (0-1) - (vsOut.texcoord * (glyph.size / atlasInfoUniform.spriteSheetSize)) + // Glyph offset (0-1) + (glyph.position / atlasDims.size) + + // Glyph coordinate (0-1) + (vsOut.texcoord * (glyph.size / atlasDims.size)) ); return vsOut; } @group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; -@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d_array; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d_array; @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { return textureSample(ourTexture, ourSampler, vsOut.texcoord, u32(vsOut.layerIndex)); @@ -528,7 +531,7 @@ class FullFileRenderStrategy implements IRenderStrategy< get bindGroupEntries(): GPUBindGroupEntry[] { return [ - { binding: BindingId.DynamicUnitInfo, resource: { buffer: this._cellBindBuffer } }, + { binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } }, { binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } } ]; } From 55961be51d33d2cc59308a307d3a0e761c651852 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:04:37 -0700 Subject: [PATCH 098/286] React at canvas resizing --- .../editor/browser/view/gpu/gpuViewLayer.ts | 30 +++++++++++++++---- src/vs/editor/browser/view/viewLayer.ts | 5 ---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 51d89f26d3c..b088f73b028 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -9,6 +9,7 @@ import { debounce } from 'vs/base/common/decorators'; import { URI } from 'vs/base/common/uri'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { ensureNonNullable, observeDevicePixelDimensions } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -94,7 +95,7 @@ export class GpuViewLayerRenderer { this.host = host; this.viewportData = viewportData; - this._gpuCtx = this.domNode.getContext('webgpu')!; + this._gpuCtx = ensureNonNullable(this.domNode.getContext('webgpu')); this.initWebgpu(); } @@ -188,6 +189,16 @@ export class GpuViewLayerRenderer { uniformValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; uniformValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); + + // TODO: Track disposable + observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { + this.domNode.width = w; + this.domNode.height = h; + uniformValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; + uniformValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; + this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); + // TODO: Request render + }); } @@ -198,10 +209,9 @@ export class GpuViewLayerRenderer { Offset_Width = 0, Offset_Height = 1, } - const atlasInfoUniformBufferSize = AtlasInfoUniformBufferInfo.BytesPerEntry; const atlasInfoUniformBuffer = this._device.createBuffer({ label: 'Monaco atlas info uniform buffer', - size: atlasInfoUniformBufferSize, + size: AtlasInfoUniformBufferInfo.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); { @@ -334,8 +344,18 @@ export class GpuViewLayerRenderer { this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); this._device.queue.copyExternalImageToTexture( { source: page.source }, - { texture: this._atlasGpuTexture, origin: { x: page.usedArea.left, y: page.usedArea.top, z: layerIndex } }, - { width: page.usedArea.right - page.usedArea.left, height: page.usedArea.bottom - page.usedArea.top }, + { + texture: this._atlasGpuTexture, + origin: { + x: page.usedArea.left, + y: page.usedArea.top, + z: layerIndex + } + }, + { + width: page.usedArea.right - page.usedArea.left, + height: page.usedArea.bottom - page.usedArea.top + }, ); this._atlasGpuTextureVersions[layerIndex] = page.version; } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index ffee2d4c0b6..41c275daa35 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -368,11 +368,6 @@ export class VisibleLinesCollection { } else { // If not yet attached, listen for device pixel size and attach if (!this._canvas.parentElement) { - // TODO: Track disposable - observeDevicePixelDimensions(this._canvas, getActiveWindow(), (w, h) => { - this._canvas.width = w; - this._canvas.height = h; - }); this.domNode.domNode.appendChild(this._canvas); } From b7019208887438bdbfb812fcd74aa456351d9514 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Aug 2024 05:33:57 -0700 Subject: [PATCH 099/286] Register GpuViewLayerRenderer --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 11 ++++++----- src/vs/editor/browser/view/viewLayer.ts | 9 +++++---- src/vs/editor/browser/view/viewOverlays.ts | 2 +- src/vs/editor/browser/viewParts/lines/viewLines.ts | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index b088f73b028..4012d85037b 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -6,6 +6,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { VSBuffer } from 'vs/base/common/buffer'; import { debounce } from 'vs/base/common/decorators'; +import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; @@ -54,7 +55,7 @@ const enum BindingId { ScrollOffset, } -export class GpuViewLayerRenderer { +export class GpuViewLayerRenderer extends Disposable { readonly domNode: HTMLCanvasElement; host: IVisibleLinesHost; @@ -91,6 +92,8 @@ export class GpuViewLayerRenderer { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, ) { + super(); + this.domNode = domNode; this.host = host; this.viewportData = viewportData; @@ -185,20 +188,18 @@ export class GpuViewLayerRenderer { }); { const uniformValues = new Float32Array(UniformBufferInfo.FloatsPerEntry); - // TODO: Update on canvas resize uniformValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; uniformValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); - // TODO: Track disposable - observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { + this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { this.domNode.width = w; this.domNode.height = h; uniformValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; uniformValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); // TODO: Request render - }); + })); } diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 41c275daa35..f1421ecd395 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; import { BugIndicatingError } from 'vs/base/common/errors'; -import { observeDevicePixelDimensions } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { Disposable } from 'vs/base/common/lifecycle'; import { GpuViewLayerRenderer } from 'vs/editor/browser/view/gpu/gpuViewLayer'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; @@ -254,7 +253,7 @@ export interface IVisibleLinesHost { createVisibleLine(): T; } -export class VisibleLinesCollection { +export class VisibleLinesCollection extends Disposable { private readonly _host: IVisibleLinesHost; public readonly domNode: FastDomNode; @@ -267,6 +266,8 @@ export class VisibleLinesCollection { host: IVisibleLinesHost, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { + super(); + this._host = host; this.domNode = this._createDomNode(); @@ -372,7 +373,7 @@ export class VisibleLinesCollection { } if (!this._gpuRenderer) { - this._gpuRenderer = this._instantiationService.createInstance(GpuViewLayerRenderer, this._canvas, this._context, this._host, viewportData); + this._gpuRenderer = this._register(this._instantiationService.createInstance(GpuViewLayerRenderer, this._canvas, this._context, this._host, viewportData)); } renderer = this._gpuRenderer; renderer.update(viewportData); diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index b305bb77fee..db70ce54da9 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -30,7 +30,7 @@ export class ViewOverlays extends ViewPart implements IVisibleLinesHost, context, this); + this._visibleLines = this._register(instantiationService.createInstance(VisibleLinesCollection, context, this)); this.domNode = this._visibleLines.domNode; const options = this._context.configuration.options; diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 512b8ae10bc..67b98728e72 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -129,7 +129,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, super(context); this._linesContent = linesContent; this._textRangeRestingSpot = document.createElement('div'); - this._visibleLines = instantiationService.createInstance(VisibleLinesCollection, context, this); + this._visibleLines = this._register(instantiationService.createInstance(VisibleLinesCollection, context, this)); this.domNode = this._visibleLines.domNode; const conf = this._context.configuration; From 0866f9a3e151fef547bc98cbbd252b92c6ef71cc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Aug 2024 05:55:41 -0700 Subject: [PATCH 100/286] Register GPUBuffers --- .../editor/browser/view/gpu/gpuDisposable.ts | 20 +++ .../editor/browser/view/gpu/gpuViewLayer.ts | 118 +++++++++--------- 2 files changed, 78 insertions(+), 60 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/gpuDisposable.ts diff --git a/src/vs/editor/browser/view/gpu/gpuDisposable.ts b/src/vs/editor/browser/view/gpu/gpuDisposable.ts new file mode 100644 index 00000000000..f34e257a11d --- /dev/null +++ b/src/vs/editor/browser/view/gpu/gpuDisposable.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IDisposable } from 'vs/base/common/lifecycle'; +import { isFunction } from 'vs/base/common/types'; + +export namespace GpuLifecycle { + export function createBuffer(device: GPUDevice, descriptor: GPUBufferDescriptor, initialValues?: Float32Array | (() => Float32Array)): { buffer: GPUBuffer } & IDisposable { + const buffer = device.createBuffer(descriptor); + if (initialValues) { + device.queue.writeBuffer(buffer, 0, isFunction(initialValues) ? initialValues() : initialValues); + } + return { + buffer, + dispose: () => buffer.destroy() + }; + } +} diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 4012d85037b..0e8f0195853 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -10,6 +10,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { GpuLifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; import { ensureNonNullable, observeDevicePixelDimensions } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; @@ -71,7 +72,7 @@ export class GpuViewLayerRenderer extends Disposable { private _pipeline!: GPURenderPipeline; private _vertexBuffer!: GPUBuffer; - private _squareVertices!: { vertexData: Float32Array; numVertices: number }; + private _quadVertices!: { vertexData: Float32Array; numVertices: number }; private static _atlas: TextureAtlas; @@ -98,6 +99,12 @@ export class GpuViewLayerRenderer extends Disposable { this.host = host; this.viewportData = viewportData; + this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { + this.domNode.width = w; + this.domNode.height = h; + // TODO: Request render + })); + this._gpuCtx = ensureNonNullable(this.domNode.getContext('webgpu')); this.initWebgpu(); } @@ -129,7 +136,7 @@ export class GpuViewLayerRenderer extends Disposable { const atlas = GpuViewLayerRenderer._atlas; - this._renderStrategy = this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._atlas); + this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._atlas)); const module = this._device.createShaderModule({ label: 'Monaco shader module', @@ -181,26 +188,21 @@ export class GpuViewLayerRenderer extends Disposable { Offset_CanvasWidth = 0, Offset_CanvasHeight = 1 } - const uniformBuffer = this._device.createBuffer({ + const uniformBufferValues = new Float32Array(UniformBufferInfo.FloatsPerEntry); + const uniformBuffer = this._register(GpuLifecycle.createBuffer(this._device, { label: 'Monaco uniform buffer', size: UniformBufferInfo.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - { - const uniformValues = new Float32Array(UniformBufferInfo.FloatsPerEntry); - uniformValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; - uniformValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; - this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); - - this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { - this.domNode.width = w; - this.domNode.height = h; - uniformValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; - uniformValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; - this._device.queue.writeBuffer(uniformBuffer, 0, uniformValues); - // TODO: Request render - })); - } + }, () => { + uniformBufferValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; + uniformBufferValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; + return uniformBufferValues; + })).buffer; + this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { + uniformBufferValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; + uniformBufferValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; + this._device.queue.writeBuffer(uniformBuffer, 0, uniformBufferValues); + })); @@ -210,32 +212,31 @@ export class GpuViewLayerRenderer extends Disposable { Offset_Width = 0, Offset_Height = 1, } - const atlasInfoUniformBuffer = this._device.createBuffer({ + const atlasInfoUniformBuffer = this._register(GpuLifecycle.createBuffer(this._device, { label: 'Monaco atlas info uniform buffer', size: AtlasInfoUniformBufferInfo.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - { - const uniformValues = new Float32Array(AtlasInfoUniformBufferInfo.FloatsPerEntry); - uniformValues[AtlasInfoUniformBufferInfo.Offset_Width] = atlas.pageSize; - uniformValues[AtlasInfoUniformBufferInfo.Offset_Height] = atlas.pageSize; - this._device.queue.writeBuffer(atlasInfoUniformBuffer, 0, uniformValues); - } + }, () => { + const values = new Float32Array(AtlasInfoUniformBufferInfo.FloatsPerEntry); + values[AtlasInfoUniformBufferInfo.Offset_Width] = atlas.pageSize; + values[AtlasInfoUniformBufferInfo.Offset_Height] = atlas.pageSize; + return values; + })).buffer; /////////////////// // Static buffer // /////////////////// - this._glyphStorageBuffer[0] = this._device.createBuffer({ + this._glyphStorageBuffer[0] = this._register(GpuLifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); - this._glyphStorageBuffer[1] = this._device.createBuffer({ + })).buffer; + this._glyphStorageBuffer[1] = this._register(GpuLifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); + })).buffer; this._atlasGpuTextureVersions[0] = 0; this._atlasGpuTextureVersions[1] = 0; this._atlasGpuTexture = this._device.createTexture({ @@ -256,7 +257,24 @@ export class GpuViewLayerRenderer extends Disposable { this._renderStrategy.initBuffers(); - this._updateSquareVertices(); + + + this._quadVertices = { + vertexData: new Float32Array([ + 1, 0, + 1, 1, + 0, 1, + 0, 0, + 0, 1, + 1, 0, + ]), + numVertices: 6 + }; + this._vertexBuffer = this._register(GpuLifecycle.createBuffer(this._device, { + label: 'Monaco quad vertex buffer', + size: this._quadVertices.vertexData.byteLength, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }, this._quadVertices.vertexData)).buffer; @@ -294,28 +312,6 @@ export class GpuViewLayerRenderer extends Disposable { this._initialized = true; } - private _updateSquareVertices() { - this._squareVertices = { - vertexData: new Float32Array([ - 1, 0, - 1, 1, - 0, 1, - 0, 0, - 0, 1, - 1, 0, - ]), - numVertices: 6 - }; - const { vertexData } = this._squareVertices; - - this._vertexBuffer = this._device.createBuffer({ - label: 'Monaco quad vertex buffer', - size: vertexData.byteLength, - usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - }); - this._device.queue.writeBuffer(this._vertexBuffer, 0, vertexData); - } - update(viewportData: ViewportData) { this.viewportData = viewportData; } @@ -417,7 +413,7 @@ export class GpuViewLayerRenderer extends Disposable { if (this._renderStrategy?.draw) { this._renderStrategy.draw(pass, ctx, startLineNumber, stopLineNumber, deltaTop); } else { - pass.draw(this._squareVertices.numVertices, visibleObjectCount); + pass.draw(this._quadVertices.numVertices, visibleObjectCount); } pass.end(); @@ -532,7 +528,7 @@ struct VSOutput { } `; -class FullFileRenderStrategy implements IRenderStrategy { +class FullFileRenderStrategy extends Disposable implements IRenderStrategy { private static _lineCount = 3000; private static _columnCount = 200; @@ -564,6 +560,8 @@ class FullFileRenderStrategy implements IRenderStrategy< private readonly _viewportData: ViewportData, private readonly _atlas: TextureAtlas, ) { + super(); + // TODO: Detect when lines have been tokenized and clear _upToDateLines const activeWindow = getActiveWindow(); const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); @@ -575,22 +573,22 @@ class FullFileRenderStrategy implements IRenderStrategy< initBuffers(): void { const bufferSize = FullFileRenderStrategy._lineCount * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; - this._cellBindBuffer = this._device.createBuffer({ + this._cellBindBuffer = this._register(GpuLifecycle.createBuffer(this._device, { label: 'Monaco full file cell buffer', size: bufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - }); + })).buffer; this._cellValueBuffers = [ new ArrayBuffer(bufferSize), new ArrayBuffer(bufferSize), ]; const scrollOffsetBufferSize = 2; - this._scrollOffsetBindBuffer = this._device.createBuffer({ + this._scrollOffsetBindBuffer = this._register(GpuLifecycle.createBuffer(this._device, { label: 'Monaco scroll offset buffer', size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); + })).buffer; this._scrollOffsetValueBuffers = [ new Float32Array(scrollOffsetBufferSize), new Float32Array(scrollOffsetBufferSize), From bcb35995f9502cd4f033ab88ed604d5ae0857aa3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Aug 2024 06:03:13 -0700 Subject: [PATCH 101/286] Register GPUDevice --- .../editor/browser/view/gpu/gpuDisposable.ts | 34 ++++++++++++--- .../editor/browser/view/gpu/gpuViewLayer.ts | 42 +++++++------------ 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuDisposable.ts b/src/vs/editor/browser/view/gpu/gpuDisposable.ts index f34e257a11d..7710c4af596 100644 --- a/src/vs/editor/browser/view/gpu/gpuDisposable.ts +++ b/src/vs/editor/browser/view/gpu/gpuDisposable.ts @@ -6,15 +6,37 @@ import { type IDisposable } from 'vs/base/common/lifecycle'; import { isFunction } from 'vs/base/common/types'; -export namespace GpuLifecycle { - export function createBuffer(device: GPUDevice, descriptor: GPUBufferDescriptor, initialValues?: Float32Array | (() => Float32Array)): { buffer: GPUBuffer } & IDisposable { +export interface IDisposableGPUObject extends IDisposable { + value: T; +} + +export namespace GPULifecycle { + export async function requestDevice(): Promise> { + if (!navigator.gpu) { + throw new Error('This browser does not support WebGPU'); + } + + const adapter = (await navigator.gpu.requestAdapter())!; + if (!adapter) { + throw new Error('This browser supports WebGPU but it appears to be disabled'); + } + + const device = await adapter.requestDevice(); + return wrapDestroyableInDisposable(device); + } + + export function createBuffer(device: GPUDevice, descriptor: GPUBufferDescriptor, initialValues?: Float32Array | (() => Float32Array)): IDisposableGPUObject { const buffer = device.createBuffer(descriptor); if (initialValues) { device.queue.writeBuffer(buffer, 0, isFunction(initialValues) ? initialValues() : initialValues); } - return { - buffer, - dispose: () => buffer.destroy() - }; + return wrapDestroyableInDisposable(buffer); } } + +function wrapDestroyableInDisposable(value: T): IDisposableGPUObject { + return { + value, + dispose: () => value.destroy() + }; +} diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 0e8f0195853..8f42e73c960 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -10,7 +10,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; -import { GpuLifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; +import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; import { ensureNonNullable, observeDevicePixelDimensions } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; @@ -64,7 +64,6 @@ export class GpuViewLayerRenderer extends Disposable { private readonly _gpuCtx!: GPUCanvasContext; - private _adapter!: GPUAdapter; private _device!: GPUDevice; private _renderPassDescriptor!: GPURenderPassDescriptor; private _renderPassColorAttachment!: GPURenderPassColorAttachment; @@ -110,16 +109,7 @@ export class GpuViewLayerRenderer extends Disposable { } async initWebgpu() { - if (!navigator.gpu) { - throw new Error('this browser does not support WebGPU'); - } - - this._adapter = (await navigator.gpu.requestAdapter())!; - if (!this._adapter) { - throw new Error('this browser supports webgpu but it appears disabled'); - } - - this._device = await this._adapter.requestDevice(); + this._device = this._register(await GPULifecycle.requestDevice()).value; const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); this._gpuCtx.configure({ @@ -189,7 +179,7 @@ export class GpuViewLayerRenderer extends Disposable { Offset_CanvasHeight = 1 } const uniformBufferValues = new Float32Array(UniformBufferInfo.FloatsPerEntry); - const uniformBuffer = this._register(GpuLifecycle.createBuffer(this._device, { + const uniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco uniform buffer', size: UniformBufferInfo.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, @@ -197,7 +187,7 @@ export class GpuViewLayerRenderer extends Disposable { uniformBufferValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; uniformBufferValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; return uniformBufferValues; - })).buffer; + })).value; this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { uniformBufferValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; uniformBufferValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; @@ -212,7 +202,7 @@ export class GpuViewLayerRenderer extends Disposable { Offset_Width = 0, Offset_Height = 1, } - const atlasInfoUniformBuffer = this._register(GpuLifecycle.createBuffer(this._device, { + const atlasInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco atlas info uniform buffer', size: AtlasInfoUniformBufferInfo.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, @@ -221,22 +211,22 @@ export class GpuViewLayerRenderer extends Disposable { values[AtlasInfoUniformBufferInfo.Offset_Width] = atlas.pageSize; values[AtlasInfoUniformBufferInfo.Offset_Height] = atlas.pageSize; return values; - })).buffer; + })).value; /////////////////// // Static buffer // /////////////////// - this._glyphStorageBuffer[0] = this._register(GpuLifecycle.createBuffer(this._device, { + this._glyphStorageBuffer[0] = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).buffer; - this._glyphStorageBuffer[1] = this._register(GpuLifecycle.createBuffer(this._device, { + })).value; + this._glyphStorageBuffer[1] = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).buffer; + })).value; this._atlasGpuTextureVersions[0] = 0; this._atlasGpuTextureVersions[1] = 0; this._atlasGpuTexture = this._device.createTexture({ @@ -270,11 +260,11 @@ export class GpuViewLayerRenderer extends Disposable { ]), numVertices: 6 }; - this._vertexBuffer = this._register(GpuLifecycle.createBuffer(this._device, { + this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco quad vertex buffer', size: this._quadVertices.vertexData.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - }, this._quadVertices.vertexData)).buffer; + }, this._quadVertices.vertexData)).value; @@ -573,22 +563,22 @@ class FullFileRenderStrategy extends Disposable implemen initBuffers(): void { const bufferSize = FullFileRenderStrategy._lineCount * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; - this._cellBindBuffer = this._register(GpuLifecycle.createBuffer(this._device, { + this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco full file cell buffer', size: bufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).buffer; + })).value; this._cellValueBuffers = [ new ArrayBuffer(bufferSize), new ArrayBuffer(bufferSize), ]; const scrollOffsetBufferSize = 2; - this._scrollOffsetBindBuffer = this._register(GpuLifecycle.createBuffer(this._device, { + this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco scroll offset buffer', size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - })).buffer; + })).value; this._scrollOffsetValueBuffers = [ new Float32Array(scrollOffsetBufferSize), new Float32Array(scrollOffsetBufferSize), From 12a010725646ea5f786867e5e455b31f2f31c505 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Aug 2024 06:22:12 -0700 Subject: [PATCH 102/286] Register GPUTextures --- src/vs/editor/browser/view/gpu/gpuDisposable.ts | 9 +++++---- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuDisposable.ts b/src/vs/editor/browser/view/gpu/gpuDisposable.ts index 7710c4af596..f5c84aed316 100644 --- a/src/vs/editor/browser/view/gpu/gpuDisposable.ts +++ b/src/vs/editor/browser/view/gpu/gpuDisposable.ts @@ -15,14 +15,11 @@ export namespace GPULifecycle { if (!navigator.gpu) { throw new Error('This browser does not support WebGPU'); } - const adapter = (await navigator.gpu.requestAdapter())!; if (!adapter) { throw new Error('This browser supports WebGPU but it appears to be disabled'); } - - const device = await adapter.requestDevice(); - return wrapDestroyableInDisposable(device); + return wrapDestroyableInDisposable(await adapter.requestDevice()); } export function createBuffer(device: GPUDevice, descriptor: GPUBufferDescriptor, initialValues?: Float32Array | (() => Float32Array)): IDisposableGPUObject { @@ -32,6 +29,10 @@ export namespace GPULifecycle { } return wrapDestroyableInDisposable(buffer); } + + export function createTexture(device: GPUDevice, descriptor: GPUTextureDescriptor): IDisposableGPUObject { + return wrapDestroyableInDisposable(device.createTexture(descriptor)); + } } function wrapDestroyableInDisposable(value: T): IDisposableGPUObject { diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 8f42e73c960..ecf5142d313 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -229,7 +229,7 @@ export class GpuViewLayerRenderer extends Disposable { })).value; this._atlasGpuTextureVersions[0] = 0; this._atlasGpuTextureVersions[1] = 0; - this._atlasGpuTexture = this._device.createTexture({ + this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, { label: 'Monaco atlas texture', format: 'rgba8unorm', // TODO: Dynamically grow/shrink layer count @@ -238,7 +238,7 @@ export class GpuViewLayerRenderer extends Disposable { usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, - }); + })).value; this._updateAtlas(); From 74f9f8f825527aadb31b4cf4b9b0a4087fa8f6c5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Aug 2024 06:40:38 -0700 Subject: [PATCH 103/286] Move atlas logging and image save into actions --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 2 + .../browser/view/gpu/atlas/textureAtlas.ts | 4 + .../view/gpu/atlas/textureAtlasPage.ts | 4 + .../gpu/atlas/textureAtlasShelfAllocator.ts | 40 +++++++--- .../gpu/atlas/textureAtlasSlabAllocator.ts | 65 ++++++++++++---- .../editor/browser/view/gpu/gpuViewLayer.ts | 43 ++-------- .../editor/contrib/gpu/browser/gpuActions.ts | 78 +++++++++++++++++++ src/vs/editor/editor.all.ts | 1 + 8 files changed, 176 insertions(+), 61 deletions(-) create mode 100644 src/vs/editor/contrib/gpu/browser/gpuActions.ts diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index 4b80eeb2575..9955566f1a2 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -35,6 +35,7 @@ export interface ITextureAtlasAllocator { * Gets a usage preview of the atlas for debugging purposes. */ getUsagePreview(): Promise; + getStats(): string; } export interface IReadableTextureAtlasPage { @@ -43,4 +44,5 @@ export interface IReadableTextureAtlasPage { readonly glyphs: IterableIterator>; readonly source: OffscreenCanvas; getUsagePreview(): Promise; + getStats(): string; } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 84c20c0f2b0..dd444fd9b45 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -67,6 +67,10 @@ export class TextureAtlas extends Disposable { return Promise.all(this._pages.map(e => e.getUsagePreview())); } + public getStats(): string[] { + return this._pages.map(e => e.getStats()); + } + /** * Warms up the atlas by rasterizing all printable ASCII characters for each token color. This * is distrubuted over multiple idle callbacks to avoid blocking the main thread. diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 717500993af..0b8dcf8c5d3 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -107,4 +107,8 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla // TODO: Standardize usage stats and make them loggable return this._allocator.getUsagePreview(); } + + getStats(): string { + return this._allocator.getStats(); + } } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts index 534e163fd2a..4f7352cf22f 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts @@ -95,6 +95,31 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { ctx.fillStyle = '#808080'; ctx.fillRect(0, 0, w, h); + const rowHeight: Map = new Map(); // y -> h + const rowWidth: Map = new Map(); // y -> w + for (const g of this.glyphMap.values()) { + rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); + rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); + } + for (const g of this.glyphMap.values()) { + ctx.fillStyle = '#4040FF'; + ctx.fillRect(g.x, g.y, g.w, g.h); + ctx.fillStyle = '#FF0000'; + ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); + } + for (const [rowY, rowW] of rowWidth.entries()) { + if (rowY !== this._currentRow.y) { + ctx.fillStyle = '#FF0000'; + ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); + } + } + return canvas.convertToBlob(); + } + + getStats(): string { + const w = this._canvas.width; + const h = this._canvas.height; + let usedPixels = 0; let wastedPixels = 0; const totalPixels = w * h; @@ -108,26 +133,19 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { for (const g of this.glyphMap.values()) { usedPixels += g.w * g.h; wastedPixels += g.w * (rowHeight.get(g.y)! - g.h); - ctx.fillStyle = '#4040FF'; - ctx.fillRect(g.x, g.y, g.w, g.h); - ctx.fillStyle = '#FF0000'; - ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); } for (const [rowY, rowW] of rowWidth.entries()) { if (rowY !== this._currentRow.y) { - ctx.fillStyle = '#FF0000'; - ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); wastedPixels += (w - rowW) * rowHeight.get(rowY)!; } } - console.log([ - `Texture atlas stats:`, - ` Total: ${totalPixels}`, + return [ + `page${this._textureIndex}:`, + ` Total: ${totalPixels} (${w}x${h})`, ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, - ].join('\n')); - return canvas.convertToBlob(); + ].join('\n'); } } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index 36d28efe8b3..3b5b539da32 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -322,9 +322,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { let slabEntryPixels = 0; let usedPixels = 0; let slabEdgePixels = 0; - let wastedPixels = 0; let restrictedPixels = 0; - const totalPixels = w * h; const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); const slabH = slabW; @@ -365,31 +363,72 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { restrictedPixels += r.w * r.h; } - const edgeUsedPixels = slabEdgePixels - restrictedPixels; - console.log({ edgeUsedPixels, slabEdgePixels, restrictedPixels }); - wastedPixels = slabEntryPixels - (usedPixels - edgeUsedPixels); - // Overlay actual glyphs on top ctx.globalAlpha = 0.5; ctx.drawImage(this._canvas, 0, 0); ctx.globalAlpha = 1; + return canvas.convertToBlob(); + } + + public getStats(): string { + const w = this._canvas.width; + const h = this._canvas.height; + + let slabEntryPixels = 0; + let usedPixels = 0; + let slabEdgePixels = 0; + let wastedPixels = 0; + let restrictedPixels = 0; + const totalPixels = w * h; + const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); + const slabH = slabW; + + // Draw wasted underneath glyphs first + for (const slab of this._slabs) { + let x = 0; + let y = 0; + for (let i = 0; i < slab.count; i++) { + if (x + slab.entryW > slabW) { + x = 0; + y += slab.entryH; + } + slabEntryPixels += slab.entryW * slab.entryH; + x += slab.entryW; + } + const entriesPerRow = Math.floor(slabW / slab.entryW); + const entriesPerCol = Math.floor(slabH / slab.entryH); + const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol; + slabEdgePixels += (slabW * slabH) - thisSlabPixels; + } + + // Draw glyphs + for (const g of this.glyphMap.values()) { + usedPixels += g.w * g.h; + } + + // Draw unused space on side + const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat()); + for (const r of unusedRegions) { + restrictedPixels += r.w * r.h; + } + + const edgeUsedPixels = slabEdgePixels - restrictedPixels; + wastedPixels = slabEntryPixels - (usedPixels - edgeUsedPixels); + // usedPixels += slabEdgePixels - restrictedPixels; const efficiency = usedPixels / (usedPixels + wastedPixels + restrictedPixels); - // Report stats - console.log([ - `Texture atlas stats:`, - ` Total: ${totalPixels}px`, + return [ + `page[${this._textureIndex}]:`, + ` Total: ${totalPixels}px (${w}x${h})`, ` Used: ${usedPixels}px (${((usedPixels / totalPixels) * 100).toFixed(2)}%)`, ` Wasted: ${wastedPixels}px (${((wastedPixels / totalPixels) * 100).toFixed(2)}%)`, `Restricted: ${restrictedPixels}px (${((restrictedPixels / totalPixels) * 100).toFixed(2)}%) (hard to allocate)`, `Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toFixed(2)}%`, ` Slabs: ${this._slabs.length} of ${Math.floor(this._canvas.width / slabW) * Math.floor(this._canvas.height / slabH)}` - ].join('\n')); - - return canvas.convertToBlob(); + ].join('\n'); } } diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index ecf5142d313..8459df161b5 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -4,10 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from 'vs/base/browser/dom'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; @@ -19,9 +16,7 @@ import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; -import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export const disableNonGpuRendering = true; @@ -73,7 +68,7 @@ export class GpuViewLayerRenderer extends Disposable { private _vertexBuffer!: GPUBuffer; private _quadVertices!: { vertexData: Float32Array; numVertices: number }; - private static _atlas: TextureAtlas; + static atlas: TextureAtlas; private readonly _glyphStorageBuffer: GPUBuffer[] = []; private _atlasGpuTexture!: GPUTexture; @@ -88,9 +83,7 @@ export class GpuViewLayerRenderer extends Disposable { private readonly _context: ViewContext, host: IVisibleLinesHost, viewportData: ViewportData, - @IFileService private readonly _fileService: IFileService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, ) { super(); @@ -120,13 +113,13 @@ export class GpuViewLayerRenderer extends Disposable { // Create texture atlas - if (!GpuViewLayerRenderer._atlas) { - GpuViewLayerRenderer._atlas = this._instantiationService.createInstance(TextureAtlas, this._device.limits.maxTextureDimension2D); + if (!GpuViewLayerRenderer.atlas) { + GpuViewLayerRenderer.atlas = this._instantiationService.createInstance(TextureAtlas, this._device.limits.maxTextureDimension2D); } - const atlas = GpuViewLayerRenderer._atlas; + const atlas = GpuViewLayerRenderer.atlas; - this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer._atlas)); + this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer.atlas)); const module = this._device.createShaderModule({ label: 'Monaco shader module', @@ -307,7 +300,7 @@ export class GpuViewLayerRenderer extends Disposable { } private _updateAtlas() { - const atlas = GpuViewLayerRenderer._atlas; + const atlas = GpuViewLayerRenderer.atlas; for (const [layerIndex, page] of atlas.pages.entries()) { // Skip the update if it's already the latest version @@ -346,30 +339,6 @@ export class GpuViewLayerRenderer extends Disposable { ); this._atlasGpuTextureVersions[layerIndex] = page.version; } - - GpuViewLayerRenderer._drawToAtlas(this._fileService, this._workspaceContextService); - } - - @debounce(500) - private static async _drawToAtlas(fileService: IFileService, workspaceContextService: IWorkspaceContextService) { - const folders = workspaceContextService.getWorkspace().folders; - if (folders.length > 0) { - const atlas = GpuViewLayerRenderer._atlas; - const promises = []; - for (const [layerIndex, page] of atlas.pages.entries()) { - promises.push(...[ - fileService.writeFile( - URI.joinPath(folders[0].uri, `atlasPage${layerIndex}_usage.png`), - VSBuffer.wrap(new Uint8Array(await (await page.getUsagePreview()).arrayBuffer())) - ), - fileService.writeFile( - URI.joinPath(folders[0].uri, `atlasPage${layerIndex}_actual.png`), - VSBuffer.wrap(new Uint8Array(await (await page.source.convertToBlob()).arrayBuffer())) - ), - ]); - } - await promises; - } } public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts new file mode 100644 index 00000000000..da3fe88fc62 --- /dev/null +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { URI } from 'vs/base/common/uri'; +import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction, type ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { GpuViewLayerRenderer } from 'vs/editor/browser/view/gpu/gpuViewLayer'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { localize } from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; + +class LogTextureAtlasStatsAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.logTextureAtlasStats', + label: localize('logTextureAtlasStats.label', "Log Texture Atlas States"), + alias: 'Log Texture Atlas States', + precondition: ContextKeyExpr.and(EditorContextKeys.notInCompositeEditor), + }); + } + + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const logService = accessor.get(ILogService); + + const atlas = GpuViewLayerRenderer.atlas; + if (!GpuViewLayerRenderer.atlas) { + logService.error('No texture atlas found'); + } + + const stats = atlas.getStats(); + logService.info(['Texture atlas stats', ...stats].join('\n\n')); + } +} + +class SaveTextureAtlasAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.saveTextureAtlas', + label: localize('saveTextureAtlas.label', "Save Texture Atlas"), + alias: 'Save Texture Atlas', + precondition: ContextKeyExpr.and(EditorContextKeys.notInCompositeEditor), + }); + } + + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const workspaceContextService = accessor.get(IWorkspaceContextService); + const fileService = accessor.get(IFileService); + const folders = workspaceContextService.getWorkspace().folders; + if (folders.length > 0) { + const atlas = GpuViewLayerRenderer.atlas; + const promises = []; + for (const [layerIndex, page] of atlas.pages.entries()) { + promises.push(...[ + fileService.writeFile( + URI.joinPath(folders[0].uri, `textureAtlasPage${layerIndex}_actual.png`), + VSBuffer.wrap(new Uint8Array(await (await page.source.convertToBlob()).arrayBuffer())) + ), + fileService.writeFile( + URI.joinPath(folders[0].uri, `textureAtlasPage${layerIndex}_usage.png`), + VSBuffer.wrap(new Uint8Array(await (await page.getUsagePreview()).arrayBuffer())) + ), + ]); + } + await promises; + } + } +} + +registerEditorAction(LogTextureAtlasStatsAction); +registerEditorAction(SaveTextureAtlasAction); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index e40c7056aa7..4db0cdd4e68 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -25,6 +25,7 @@ import 'vs/editor/contrib/find/browser/findController'; import 'vs/editor/contrib/folding/browser/folding'; import 'vs/editor/contrib/fontZoom/browser/fontZoom'; import 'vs/editor/contrib/format/browser/formatActions'; +import 'vs/editor/contrib/gpu/browser/gpuActions'; import 'vs/editor/contrib/documentSymbols/browser/documentSymbols'; import 'vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution'; import 'vs/editor/contrib/inlineProgress/browser/inlineProgress'; From 17d9cfc432d3cc1ace1e4d323340149ba75fb957 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Aug 2024 06:58:11 -0700 Subject: [PATCH 104/286] Fix atlas warming by ignoring unimportant metadata in glyph key --- .../browser/view/gpu/atlas/textureAtlas.ts | 13 ++++---- .../view/gpu/atlas/textureAtlasPage.ts | 6 +++- .../editor/browser/view/gpu/gpuViewLayer.ts | 4 +++ .../editor/common/encodedTokenAttributes.ts | 32 +++++++++---------- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index dd444fd9b45..a8109e37561 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -10,6 +10,7 @@ import type { IReadableTextureAtlasPage, ITextureAtlasGlyph } from 'vs/editor/br import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; +import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -82,24 +83,24 @@ export class TextureAtlas extends Disposable { // A-Z for (let code = 65; code <= 90; code++) { taskQueue.enqueue(() => { - for (const tokenFg of this._colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), tokenFg); + for (const fgColor of this._colorMap.keys()) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); } }); } // a-z for (let code = 97; code <= 122; code++) { taskQueue.enqueue(() => { - for (const tokenFg of this._colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), tokenFg); + for (const fgColor of this._colorMap.keys()) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); } }); } // Remaining ascii for (let code = 33; code <= 126; code++) { taskQueue.enqueue(() => { - for (const tokenFg of this._colorMap.keys()) { - this.getGlyph(rasterizer, String.fromCharCode(code), tokenFg); + for (const fgColor of this._colorMap.keys()) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); } }); } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 0b8dcf8c5d3..08ddf9287dd 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -10,6 +10,7 @@ import type { IBoundingBox, IReadableTextureAtlasPage, ITextureAtlasAllocator, I import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -77,6 +78,9 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla // TODO: Color, style etc. public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + // Ignore metadata that doesn't affect the glyph + metadata ^= (MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); + return this._glyphMap.get(chars, metadata) ?? this._createGlyph(rasterizer, chars, metadata); } @@ -94,7 +98,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { chars, - fg: this._colorMap[metadata], + metadata, rasterizedGlyph, glyph }); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 8459df161b5..5c53d65aefa 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -17,6 +17,7 @@ import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData' import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; export const disableNonGpuRendering = true; @@ -84,6 +85,7 @@ export class GpuViewLayerRenderer extends Disposable { host: IVisibleLinesHost, viewportData: ViewportData, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, ) { super(); @@ -308,6 +310,8 @@ export class GpuViewLayerRenderer extends Disposable { continue; } + this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version); + // TODO: Dynamically set buffer size const bufferSize = GlyphStorageBufferInfo.FloatsPerEntry * Constants.MaxAtlasPageGlyphCount; const values = new Float32Array(bufferSize / 4); diff --git a/src/vs/editor/common/encodedTokenAttributes.ts b/src/vs/editor/common/encodedTokenAttributes.ts index c0f2fad70f1..bf144bd8a17 100644 --- a/src/vs/editor/common/encodedTokenAttributes.ts +++ b/src/vs/editor/common/encodedTokenAttributes.ts @@ -65,26 +65,26 @@ export const enum StandardTokenType { * */ export const enum MetadataConsts { - LANGUAGEID_MASK = 0b00000000000000000000000011111111, - TOKEN_TYPE_MASK = 0b00000000000000000000001100000000, - BALANCED_BRACKETS_MASK = 0b00000000000000000000010000000000, - FONT_STYLE_MASK = 0b00000000000000000111100000000000, - FOREGROUND_MASK = 0b00000000111111111000000000000000, - BACKGROUND_MASK = 0b11111111000000000000000000000000, + LANGUAGEID_MASK /* */ = 0b00000000000000000000000011111111, + TOKEN_TYPE_MASK /* */ = 0b00000000000000000000001100000000, + BALANCED_BRACKETS_MASK /* */ = 0b00000000000000000000010000000000, + FONT_STYLE_MASK /* */ = 0b00000000000000000111100000000000, + FOREGROUND_MASK /* */ = 0b00000000111111111000000000000000, + BACKGROUND_MASK /* */ = 0b11111111000000000000000000000000, - ITALIC_MASK = 0b00000000000000000000100000000000, - BOLD_MASK = 0b00000000000000000001000000000000, - UNDERLINE_MASK = 0b00000000000000000010000000000000, - STRIKETHROUGH_MASK = 0b00000000000000000100000000000000, + ITALIC_MASK /* */ = 0b00000000000000000000100000000000, + BOLD_MASK /* */ = 0b00000000000000000001000000000000, + UNDERLINE_MASK /* */ = 0b00000000000000000010000000000000, + STRIKETHROUGH_MASK /* */ = 0b00000000000000000100000000000000, // Semantic tokens cannot set the language id, so we can // use the first 8 bits for control purposes - SEMANTIC_USE_ITALIC = 0b00000000000000000000000000000001, - SEMANTIC_USE_BOLD = 0b00000000000000000000000000000010, - SEMANTIC_USE_UNDERLINE = 0b00000000000000000000000000000100, - SEMANTIC_USE_STRIKETHROUGH = 0b00000000000000000000000000001000, - SEMANTIC_USE_FOREGROUND = 0b00000000000000000000000000010000, - SEMANTIC_USE_BACKGROUND = 0b00000000000000000000000000100000, + SEMANTIC_USE_ITALIC /* */ = 0b00000000000000000000000000000001, + SEMANTIC_USE_BOLD /* */ = 0b00000000000000000000000000000010, + SEMANTIC_USE_UNDERLINE /* */ = 0b00000000000000000000000000000100, + SEMANTIC_USE_STRIKETHROUGH /* */ = 0b00000000000000000000000000001000, + SEMANTIC_USE_FOREGROUND /* */ = 0b00000000000000000000000000010000, + SEMANTIC_USE_BACKGROUND /* */ = 0b00000000000000000000000000100000, LANGUAGEID_OFFSET = 0, TOKEN_TYPE_OFFSET = 8, From 3cfe905cb8035edf2b824efc865f9d0d0febca7e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 17 Aug 2024 08:44:33 -0700 Subject: [PATCH 105/286] Move usage preview colors into a const enum --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 7 +++++++ .../view/gpu/atlas/textureAtlasShelfAllocator.ts | 10 +++++----- .../view/gpu/atlas/textureAtlasSlabAllocator.ts | 10 +++++----- src/vs/editor/browser/view/gpu/gpuUtils.ts | 1 + 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index 9955566f1a2..3cac69d0efb 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -46,3 +46,10 @@ export interface IReadableTextureAtlasPage { getUsagePreview(): Promise; getStats(): string; } + +export const enum UsagePreviewColors { + Unused = '#808080', + Used = '#4040FF', + Wasted = '#FF0000', + Restricted = '#FF000088', +} diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts index 4f7352cf22f..a17e97745db 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { TwoKeyMap } from 'vs/base/common/map'; -import type { ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -92,7 +92,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { const h = this._canvas.height; const canvas = new OffscreenCanvas(w, h); const ctx = ensureNonNullable(canvas.getContext('2d')); - ctx.fillStyle = '#808080'; + ctx.fillStyle = UsagePreviewColors.Unused; ctx.fillRect(0, 0, w, h); const rowHeight: Map = new Map(); // y -> h @@ -102,14 +102,14 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); } for (const g of this.glyphMap.values()) { - ctx.fillStyle = '#4040FF'; + ctx.fillStyle = UsagePreviewColors.Used; ctx.fillRect(g.x, g.y, g.w, g.h); - ctx.fillStyle = '#FF0000'; + ctx.fillStyle = UsagePreviewColors.Wasted; ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); } for (const [rowY, rowW] of rowWidth.entries()) { if (rowY !== this._currentRow.y) { - ctx.fillStyle = '#FF0000'; + ctx.fillStyle = UsagePreviewColors.Wasted; ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); } } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index 3b5b539da32..153c5b72555 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -5,7 +5,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { TwoKeyMap } from 'vs/base/common/map'; -import type { ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -316,7 +316,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { const canvas = new OffscreenCanvas(w, h); const ctx = ensureNonNullable(canvas.getContext('2d')); - ctx.fillStyle = '#808080'; + ctx.fillStyle = UsagePreviewColors.Unused; ctx.fillRect(0, 0, w, h); let slabEntryPixels = 0; @@ -336,7 +336,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { y += slab.entryH; } // TODO: This doesn't visualize wasted space between entries - draw glyphs on top? - ctx.fillStyle = '#FF0000'; + ctx.fillStyle = UsagePreviewColors.Wasted; ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH); slabEntryPixels += slab.entryW * slab.entryH; @@ -351,14 +351,14 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Draw glyphs for (const g of this.glyphMap.values()) { usedPixels += g.w * g.h; - ctx.fillStyle = '#4040FF'; + ctx.fillStyle = UsagePreviewColors.Used; ctx.fillRect(g.x, g.y, g.w, g.h); } // Draw unused space on side const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat()); for (const r of unusedRegions) { - ctx.fillStyle = '#FF000080'; + ctx.fillStyle = UsagePreviewColors.Restricted; ctx.fillRect(r.x, r.y, r.w, r.h); restrictedPixels += r.w * r.h; } diff --git a/src/vs/editor/browser/view/gpu/gpuUtils.ts b/src/vs/editor/browser/view/gpu/gpuUtils.ts index 66497f1c205..d83123bb8d2 100644 --- a/src/vs/editor/browser/view/gpu/gpuUtils.ts +++ b/src/vs/editor/browser/view/gpu/gpuUtils.ts @@ -12,6 +12,7 @@ export function ensureNonNullable(value: T | null): T { return value; } +// TODO: Move capabilities into ElementSizeObserver? export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { // Observe any resizes to the element and extract the actual pixel size of the element if the // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when From a25992fabe314d127399e1ad589306f84506f4fd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:13:25 -0700 Subject: [PATCH 106/286] ObjectCollectionBuffer start --- .../view/gpu/objectCollectionBuffer.ts | 139 ++++++++++++++++++ .../view/gpu/objectCollectionBuffer.test.ts | 53 +++++++ 2 files changed, 192 insertions(+) create mode 100644 src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts create mode 100644 src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts diff --git a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts new file mode 100644 index 00000000000..26a6e372018 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable, type IDisposable } from 'vs/base/common/lifecycle'; + +export interface ObjectCollectionBufferPropertySpec { + name: string; +} + +export type ObjectCollectionPropertyValues = { + [K in T[number]['name']]: number; +}; + +export interface IObjectCollectionBuffer extends IDisposable { + /** + * The underlying buffer. This **should not** be modified externally. + */ + readonly buffer: ArrayBuffer; + /** + * A view of the underlying buffer. This **should not** be modified externally. + */ + readonly view: Float32Array; + + /** + * Creates an entry in the collection. This will return a managed object that can be modified + * which will update the underlying buffer. + * @param data The data of the entry. + */ + createEntry(data: ObjectCollectionPropertyValues): IObjectCollectionBufferEntry; +} + +export interface IObjectCollectionBufferEntry extends IDisposable { + set(propertyName: T[number]['name'], value: number): void; + get(propertyName: T[number]['name']): number; +} + +export function createObjectCollectionBuffer( + propertySpecs: T, + capacity: number +): IObjectCollectionBuffer { + return new ObjectCollectionBuffer(propertySpecs, capacity); +} + +class ObjectCollectionBuffer extends Disposable implements IObjectCollectionBuffer { + buffer: ArrayBuffer; + view: Float32Array; + + private readonly _propertySpecsMap: Map = new Map(); + private readonly _entrySize: number; + // Linked list for fast access to tail and insertion in middle? + private readonly _entries: Set> = new Set(); + + constructor( + public propertySpecs: T, + public capacity: number + ) { + super(); + + this.view = new Float32Array(capacity * 2); + this.buffer = this.view.buffer; + this._entrySize = propertySpecs.length; + for (let i = 0; i < propertySpecs.length; i++) { + const spec = { + offset: i, + ...propertySpecs[i] + }; + this._propertySpecsMap.set(spec.name, spec); + } + } + + createEntry(data: ObjectCollectionPropertyValues): IObjectCollectionBufferEntry { + if (this._entries.size === this.capacity) { + throw new Error(`Cannot create more entries ObjectCollectionBuffer entries (capacity=${this.capacity})`); + } + + const value = new ObjectCollectionBufferEntry(this.view, this._propertySpecsMap, this._entries.size, data); + // TODO: Needs unregistering when disposed + this._register(value.onDidChange(() => { + // TODO: Listen and react to change + })); + this._register(value.onWillDispose(() => { + // TODO: A linked list could make this O(1) + const deletedEntryIndex = value.i; + + // Shift all entries after the deleted entry to the left + this.view.set(this.view.subarray(deletedEntryIndex * this._entrySize + 2, this._entries.size * this._entrySize + 2), deletedEntryIndex * this._entrySize); + + // Update entries to reflect the new i + for (const entry of this._entries) { + if (entry.i > deletedEntryIndex) { + entry.i--; + } + } + + // There may be stale data at the end but it doesn't matter + })); + this._entries.add(value); + return value; + } +} + +class ObjectCollectionBufferEntry extends Disposable implements IObjectCollectionBufferEntry { + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + constructor( + private _view: Float32Array, + private _propertySpecsMap: Map, + public i: number, + data: ObjectCollectionPropertyValues, + ) { + super(); + for (const propertySpec of this._propertySpecsMap.values()) { + // TODO: Type isn't that safe + this._view[this.i * this._propertySpecsMap.size + propertySpec.offset] = data[propertySpec.name as keyof typeof data]; + } + } + + override dispose() { + this._onWillDispose.fire(); + super.dispose(); + } + + set(propertyName: T[number]['name'], value: number): void { + // TODO: 2 -> pull from spec + this._view[this.i * this._propertySpecsMap.size + this._propertySpecsMap.get(propertyName)!.offset] = value; + this._onDidChange.fire(); + } + + get(propertyName: T[number]['name']): number { + return this._view[this.i * this._propertySpecsMap.size + this._propertySpecsMap.get(propertyName)!.offset]; + } +} diff --git a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts new file mode 100644 index 00000000000..1b46048ed4a --- /dev/null +++ b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, throws } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { createObjectCollectionBuffer } from 'vs/editor/browser/view/gpu/objectCollectionBuffer'; + +suite('ObjectCollectionBuffer', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('createEntry', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'a' }, + { name: 'b' }, + ], 5)); + deepStrictEqual(Array.from(buffer.view), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + store.add(buffer.createEntry({ a: 1, b: 2 })); + store.add(buffer.createEntry({ a: 3, b: 4 })); + store.add(buffer.createEntry({ a: 5, b: 6 })); + store.add(buffer.createEntry({ a: 7, b: 8 })); + store.add(buffer.createEntry({ a: 9, b: 10 })); + deepStrictEqual(Array.from(buffer.view), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + test('createEntry beyond capacity', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'a' }, + { name: 'b' }, + ], 1)); + store.add(buffer.createEntry({ a: 1, b: 2 })); + throws(() => buffer.createEntry({ a: 3, b: 4 })); + }); + + test('dispose entry', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'a' }, + { name: 'b' }, + ], 5)); + deepStrictEqual(Array.from(buffer.view), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + store.add(buffer.createEntry({ a: 1, b: 2 })); + const entry1 = buffer.createEntry({ a: 3, b: 4 }); + store.add(buffer.createEntry({ a: 5, b: 6 })); + const entry2 = buffer.createEntry({ a: 7, b: 8 }); + store.add(buffer.createEntry({ a: 9, b: 10 })); + entry1.dispose(); + entry2.dispose(); + deepStrictEqual(Array.from(buffer.view).slice(0, 6), [1, 2, 5, 6, 9, 10]); + }); +}); From 8b34e508ee28be8a038e52407b9303f2fa1920c3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 19 Aug 2024 07:37:16 -0700 Subject: [PATCH 107/286] Build out ObjectCollectionBuffer more --- .../view/gpu/objectCollectionBuffer.ts | 26 +++++++++- .../view/gpu/objectCollectionBuffer.test.ts | 49 +++++++++++++++---- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts index 26a6e372018..98522769638 100644 --- a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts +++ b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter } from 'vs/base/common/event'; +import { Emitter, type Event } from 'vs/base/common/event'; import { Disposable, type IDisposable } from 'vs/base/common/lifecycle'; export interface ObjectCollectionBufferPropertySpec { @@ -23,6 +23,16 @@ export interface IObjectCollectionBuffer; /** * Creates an entry in the collection. This will return a managed object that can be modified @@ -41,18 +51,28 @@ export function createObjectCollectionBuffer { - return new ObjectCollectionBuffer(propertySpecs, capacity); + return new ObjectCollectionBuffer(propertySpecs, capacity); } class ObjectCollectionBuffer extends Disposable implements IObjectCollectionBuffer { buffer: ArrayBuffer; view: Float32Array; + get bufferUsedSize() { + return this.viewUsedSize * Float32Array.BYTES_PER_ELEMENT; + } + get viewUsedSize() { + return this._entries.size * this._entrySize; + } + private readonly _propertySpecsMap: Map = new Map(); private readonly _entrySize: number; // Linked list for fast access to tail and insertion in middle? private readonly _entries: Set> = new Set(); + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + constructor( public propertySpecs: T, public capacity: number @@ -80,10 +100,12 @@ class ObjectCollectionBuffer ext // TODO: Needs unregistering when disposed this._register(value.onDidChange(() => { // TODO: Listen and react to change + this._onDidChange.fire(); })); this._register(value.onWillDispose(() => { // TODO: A linked list could make this O(1) const deletedEntryIndex = value.i; + this._entries.delete(value); // Shift all entries after the deleted entry to the left this.view.set(this.view.subarray(deletedEntryIndex * this._entrySize + 2, this._entries.size * this._entrySize + 2), deletedEntryIndex * this._entrySize); diff --git a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts index 1b46048ed4a..f70572ec735 100644 --- a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts +++ b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts @@ -3,33 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { deepStrictEqual, throws } from 'assert'; +import { deepStrictEqual, strictEqual, throws } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { createObjectCollectionBuffer } from 'vs/editor/browser/view/gpu/objectCollectionBuffer'; +import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from 'vs/editor/browser/view/gpu/objectCollectionBuffer'; suite('ObjectCollectionBuffer', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); + function assertUsedData(buffer: IObjectCollectionBuffer, expected: number[]) { + deepStrictEqual(Array.from(buffer.view.subarray(0, buffer.viewUsedSize)), expected); + } + test('createEntry', () => { const buffer = store.add(createObjectCollectionBuffer([ { name: 'a' }, { name: 'b' }, - ], 5)); - deepStrictEqual(Array.from(buffer.view), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + ] as const, 5)); + assertUsedData(buffer, []); store.add(buffer.createEntry({ a: 1, b: 2 })); store.add(buffer.createEntry({ a: 3, b: 4 })); store.add(buffer.createEntry({ a: 5, b: 6 })); store.add(buffer.createEntry({ a: 7, b: 8 })); store.add(buffer.createEntry({ a: 9, b: 10 })); - deepStrictEqual(Array.from(buffer.view), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + assertUsedData(buffer, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); }); test('createEntry beyond capacity', () => { const buffer = store.add(createObjectCollectionBuffer([ { name: 'a' }, { name: 'b' }, - ], 1)); + ] as const, 1)); store.add(buffer.createEntry({ a: 1, b: 2 })); throws(() => buffer.createEntry({ a: 3, b: 4 })); }); @@ -38,9 +42,7 @@ suite('ObjectCollectionBuffer', () => { const buffer = store.add(createObjectCollectionBuffer([ { name: 'a' }, { name: 'b' }, - ], 5)); - deepStrictEqual(Array.from(buffer.view), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); - + ] as const, 5)); store.add(buffer.createEntry({ a: 1, b: 2 })); const entry1 = buffer.createEntry({ a: 3, b: 4 }); store.add(buffer.createEntry({ a: 5, b: 6 })); @@ -48,6 +50,33 @@ suite('ObjectCollectionBuffer', () => { store.add(buffer.createEntry({ a: 9, b: 10 })); entry1.dispose(); entry2.dispose(); - deepStrictEqual(Array.from(buffer.view).slice(0, 6), [1, 2, 5, 6, 9, 10]); + // Data from disposed entries is stale and doesn't need to be validated + assertUsedData(buffer, [1, 2, 5, 6, 9, 10]); + }); + + test('entry.get', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'foo' }, + { name: 'bar' }, + ] as const, 5)); + const entry = store.add(buffer.createEntry({ foo: 1, bar: 2 })); + strictEqual(entry.get('foo'), 1); + strictEqual(entry.get('bar'), 2); + }); + + test('entry.set', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'foo' }, + { name: 'bar' }, + ] as const, 5)); + const entry = store.add(buffer.createEntry({ foo: 1, bar: 2 })); + let changeCount = 0; + store.add(buffer.onDidChange(() => changeCount++)); + entry.set('foo', 3); + strictEqual(changeCount, 1); + strictEqual(entry.get('foo'), 3); + entry.set('bar', 4); + strictEqual(changeCount, 2); + strictEqual(entry.get('bar'), 4); }); }); From 7a7d6f5e4985650f985bddf2a6aace7cef0bc648 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:35:29 -0700 Subject: [PATCH 108/286] Switch to linked list for objectcollectionbuffer entry tracking --- .../editor/browser/view/gpu/objectCollectionBuffer.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts index 98522769638..08eb8dde5b6 100644 --- a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts +++ b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts @@ -5,6 +5,7 @@ import { Emitter, type Event } from 'vs/base/common/event'; import { Disposable, type IDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; export interface ObjectCollectionBufferPropertySpec { name: string; @@ -67,8 +68,7 @@ class ObjectCollectionBuffer ext private readonly _propertySpecsMap: Map = new Map(); private readonly _entrySize: number; - // Linked list for fast access to tail and insertion in middle? - private readonly _entries: Set> = new Set(); + private readonly _entries: LinkedList> = new LinkedList(); private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange = this._onDidChange.event; @@ -97,15 +97,15 @@ class ObjectCollectionBuffer ext } const value = new ObjectCollectionBufferEntry(this.view, this._propertySpecsMap, this._entries.size, data); + const removeFromEntries = this._entries.push(value); // TODO: Needs unregistering when disposed this._register(value.onDidChange(() => { // TODO: Listen and react to change this._onDidChange.fire(); })); this._register(value.onWillDispose(() => { - // TODO: A linked list could make this O(1) const deletedEntryIndex = value.i; - this._entries.delete(value); + removeFromEntries(); // Shift all entries after the deleted entry to the left this.view.set(this.view.subarray(deletedEntryIndex * this._entrySize + 2, this._entries.size * this._entrySize + 2), deletedEntryIndex * this._entrySize); @@ -117,9 +117,8 @@ class ObjectCollectionBuffer ext } } - // There may be stale data at the end but it doesn't matter + // There may be stale data at the end which should be ignored })); - this._entries.add(value); return value; } } From 2c9296bb6d916cc2e1aaaae2178bc2367ebda499 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:53:01 -0700 Subject: [PATCH 109/286] ObjectCollectionBuffer polish --- .../view/gpu/objectCollectionBuffer.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts index 08eb8dde5b6..f4a46c33158 100644 --- a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts +++ b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, type Event } from 'vs/base/common/event'; -import { Disposable, type IDisposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, dispose, toDisposable, type IDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; export interface ObjectCollectionBufferPropertySpec { @@ -89,6 +89,7 @@ class ObjectCollectionBuffer ext }; this._propertySpecsMap.set(spec.name, spec); } + this._register(toDisposable(() => dispose(this._entries))); } createEntry(data: ObjectCollectionPropertyValues): IObjectCollectionBufferEntry { @@ -98,12 +99,9 @@ class ObjectCollectionBuffer ext const value = new ObjectCollectionBufferEntry(this.view, this._propertySpecsMap, this._entries.size, data); const removeFromEntries = this._entries.push(value); - // TODO: Needs unregistering when disposed - this._register(value.onDidChange(() => { - // TODO: Listen and react to change - this._onDidChange.fire(); - })); - this._register(value.onWillDispose(() => { + const listeners: IDisposable[] = []; + listeners.push(Event.forward(value.onDidChange, this._onDidChange)); + listeners.push(value.onWillDispose(() => { const deletedEntryIndex = value.i; removeFromEntries(); @@ -116,8 +114,7 @@ class ObjectCollectionBuffer ext entry.i--; } } - - // There may be stale data at the end which should be ignored + dispose(listeners); })); return value; } @@ -138,7 +135,6 @@ class ObjectCollectionBufferEntry pull from spec this._view[this.i * this._propertySpecsMap.size + this._propertySpecsMap.get(propertyName)!.offset] = value; this._onDidChange.fire(); } From bcf0f9d2b36400739a995c4699ba3e079fb1d256 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:58:11 -0700 Subject: [PATCH 110/286] Improve uniforms names/struct --- .../editor/browser/view/gpu/gpuViewLayer.ts | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 5c53d65aefa..9cce441f677 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -47,7 +47,7 @@ const enum BindingId { Cells, TextureSampler, Texture, - Uniforms, + CanvasDimensionsUniform, AtlasDimensionsUniform, ScrollOffset, } @@ -167,26 +167,26 @@ export class GpuViewLayerRenderer extends Disposable { // Write standard uniforms - const enum UniformBufferInfo { + const enum CanvasDimensionsUniformBufferInfo { FloatsPerEntry = 2, - BytesPerEntry = UniformBufferInfo.FloatsPerEntry * 4, + BytesPerEntry = CanvasDimensionsUniformBufferInfo.FloatsPerEntry * 4, Offset_CanvasWidth = 0, Offset_CanvasHeight = 1 } - const uniformBufferValues = new Float32Array(UniformBufferInfo.FloatsPerEntry); - const uniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { + const canvasDimensionsUniformBufferValues = new Float32Array(CanvasDimensionsUniformBufferInfo.FloatsPerEntry); + const canvasDimensionsUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco uniform buffer', - size: UniformBufferInfo.BytesPerEntry, + size: CanvasDimensionsUniformBufferInfo.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }, () => { - uniformBufferValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; - uniformBufferValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; - return uniformBufferValues; + canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; + canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; + return canvasDimensionsUniformBufferValues; })).value; this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { - uniformBufferValues[UniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; - uniformBufferValues[UniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; - this._device.queue.writeBuffer(uniformBuffer, 0, uniformBufferValues); + canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; + canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; + this._device.queue.writeBuffer(canvasDimensionsUniformBuffer, 0, canvasDimensionsUniformBufferValues); })); @@ -277,7 +277,7 @@ export class GpuViewLayerRenderer extends Disposable { { binding: BindingId.GlyphInfo1, resource: { buffer: this._glyphStorageBuffer[1] } }, { binding: BindingId.TextureSampler, resource: sampler }, { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, - { binding: BindingId.Uniforms, resource: { buffer: uniformBuffer } }, + { binding: BindingId.CanvasDimensionsUniform, resource: { buffer: canvasDimensionsUniformBuffer } }, { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, ...this._renderStrategy.bindGroupEntries ], @@ -402,14 +402,6 @@ interface IRenderStrategy { // #region Full file render strategy const fullFileRenderStrategyWgsl = ` -struct Uniforms { - canvasDimensions: vec2f, -}; - -struct AtlasDimensionsUniform { - size: vec2f, -} - struct GlyphInfo { position: vec2f, size: vec2f, @@ -438,14 +430,14 @@ struct VSOutput { }; // Uniforms -@group(0) @binding(${BindingId.Uniforms}) var uniforms: Uniforms; -@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: AtlasDimensionsUniform; -@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; +@group(0) @binding(${BindingId.CanvasDimensionsUniform}) var canvasDims: vec2f; +@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: vec2f; +@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; // Storage buffers -@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; -@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; -@group(0) @binding(${BindingId.Cells}) var cells: array; +@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; +@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; +@group(0) @binding(${BindingId.Cells}) var cells: array; @vertex fn vs( vert: Vertex, @@ -465,7 +457,7 @@ struct VSOutput { var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions), + (((vert.position * vec2f(2, -2)) / canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / canvasDims) + ((scrollOffset.offset * 2) / canvasDims), 0.0, 1.0 ); @@ -475,9 +467,9 @@ struct VSOutput { vsOut.texcoord = vert.position; vsOut.texcoord = ( // Glyph offset (0-1) - (glyph.position / atlasDims.size) + + (glyph.position / atlasDims) + // Glyph coordinate (0-1) - (vsOut.texcoord * (glyph.size / atlasDims.size)) + (vsOut.texcoord * (glyph.size / atlasDims)) ); return vsOut; From afda4b009ae1c0d2c4ed1b2bf10d2a6d3390fbf4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 19 Aug 2024 12:31:48 -0700 Subject: [PATCH 111/286] Draw glyph debug command --- .../view/gpu/atlas/textureAtlasPage.ts | 3 +- src/vs/editor/browser/view/gpu/gpuUtils.ts | 9 +++ .../editor/browser/view/gpu/gpuViewLayer.ts | 22 ++---- .../view/gpu/objectCollectionBuffer.ts | 7 ++ .../editor/contrib/gpu/browser/gpuActions.ts | 71 +++++++++++++++++++ 5 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 08ddf9287dd..38806be08b8 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -76,11 +76,12 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla })); } - // TODO: Color, style etc. public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { // Ignore metadata that doesn't affect the glyph metadata ^= (MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); + // TODO: Encode font size and family into key + return this._glyphMap.get(chars, metadata) ?? this._createGlyph(rasterizer, chars, metadata); } diff --git a/src/vs/editor/browser/view/gpu/gpuUtils.ts b/src/vs/editor/browser/view/gpu/gpuUtils.ts index d83123bb8d2..52224c53643 100644 --- a/src/vs/editor/browser/view/gpu/gpuUtils.ts +++ b/src/vs/editor/browser/view/gpu/gpuUtils.ts @@ -5,6 +5,15 @@ import { toDisposable, type IDisposable } from 'vs/base/common/lifecycle'; +export const quadVertices = new Float32Array([ + 1, 0, + 1, 1, + 0, 1, + 0, 0, + 0, 1, + 1, 0, +]); + export function ensureNonNullable(value: T | null): T { if (!value) { throw new Error(`Value "${value}" cannot be null`); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 9cce441f677..568ee52aaf5 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; -import { ensureNonNullable, observeDevicePixelDimensions } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { ensureNonNullable, observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -67,7 +67,6 @@ export class GpuViewLayerRenderer extends Disposable { private _pipeline!: GPURenderPipeline; private _vertexBuffer!: GPUBuffer; - private _quadVertices!: { vertexData: Float32Array; numVertices: number }; static atlas: TextureAtlas; @@ -244,22 +243,11 @@ export class GpuViewLayerRenderer extends Disposable { - this._quadVertices = { - vertexData: new Float32Array([ - 1, 0, - 1, 1, - 0, 1, - 0, 0, - 0, 1, - 1, 0, - ]), - numVertices: 6 - }; this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco quad vertex buffer', - size: this._quadVertices.vertexData.byteLength, + label: 'Monaco vertex buffer', + size: quadVertices.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - }, this._quadVertices.vertexData)).value; + }, quadVertices)).value; @@ -376,7 +364,7 @@ export class GpuViewLayerRenderer extends Disposable { if (this._renderStrategy?.draw) { this._renderStrategy.draw(pass, ctx, startLineNumber, stopLineNumber, deltaTop); } else { - pass.draw(this._quadVertices.numVertices, visibleObjectCount); + pass.draw(quadVertices.length / 2, visibleObjectCount); } pass.end(); diff --git a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts index f4a46c33158..e7f7ed27435 100644 --- a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts +++ b/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts @@ -33,6 +33,9 @@ export interface IObjectCollectionBuffer; /** @@ -43,6 +46,10 @@ export interface IObjectCollectionBuffer): IObjectCollectionBufferEntry; } +/** + * An entry in an {@link ObjectCollectionBuffer}. Property values on the entry can be changed and + * their values will be updated automatically in the buffer. + */ export interface IObjectCollectionBufferEntry extends IDisposable { set(propertyName: T[number]['name'], value: number): void; get(propertyName: T[number]['name']): number; diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index da3fe88fc62..874ef00a3c8 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -7,12 +7,16 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, type ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GpuViewLayerRenderer } from 'vs/editor/browser/view/gpu/gpuViewLayer'; +import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; class LogTextureAtlasStatsAction extends EditorAction { @@ -32,6 +36,7 @@ class LogTextureAtlasStatsAction extends EditorAction { const atlas = GpuViewLayerRenderer.atlas; if (!GpuViewLayerRenderer.atlas) { logService.error('No texture atlas found'); + return; } const stats = atlas.getStats(); @@ -74,5 +79,71 @@ class SaveTextureAtlasAction extends EditorAction { } } +class DrawGlyphAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.drawGlyph', + label: localize('drawGlyph.label', "Draw Glyph"), + alias: 'Draw Glyph', + precondition: ContextKeyExpr.true(), + }); + } + + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const configurationService = accessor.get(IConfigurationService); + const fileService = accessor.get(IFileService); + const logService = accessor.get(ILogService); + const quickInputService = accessor.get(IQuickInputService); + const workspaceContextService = accessor.get(IWorkspaceContextService); + + const folders = workspaceContextService.getWorkspace().folders; + if (folders.length === 0) { + return; + } + + const atlas = GpuViewLayerRenderer.atlas; + if (!GpuViewLayerRenderer.atlas) { + logService.error('No texture atlas found'); + return; + } + + const fontFamily = configurationService.getValue('editor.fontFamily'); + const fontSize = configurationService.getValue('editor.fontSize'); + const rasterizer = new GlyphRasterizer(fontSize, fontFamily); + let chars = await quickInputService.input({ + prompt: 'Enter a character to draw (prefix with 0x for code point))' + }); + if (!chars) { + return; + } + const codePoint = chars.match(/0x(?\d+)/)?.groups?.codePoint; + if (codePoint !== undefined) { + chars = String.fromCodePoint(parseInt(codePoint)); + } + const metadata = 0; + const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, metadata); + if (!rasterizedGlyph) { + return; + } + const imageData = atlas.pages[rasterizedGlyph.textureIndex].source.getContext('2d')?.getImageData( + rasterizedGlyph.x, + rasterizedGlyph.y, + rasterizedGlyph.w, + rasterizedGlyph.h + ); + if (!imageData) { + return; + } + const canvas = new OffscreenCanvas(imageData.width, imageData.height); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.putImageData(imageData, 0, 0); + const blob = await canvas.convertToBlob({ type: 'image/png' }); + const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${metadata}_${fontSize}px_${fontFamily.replaceAll(/[,\.'\s]/g, '_')}.png`); + await fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer()))); + } +} + registerEditorAction(LogTextureAtlasStatsAction); registerEditorAction(SaveTextureAtlasAction); +registerEditorAction(DrawGlyphAction); From e059d013f9626f0d2744aa5e83e1d8583273250c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 20 Aug 2024 06:42:36 -0700 Subject: [PATCH 112/286] Use IReference over new interface --- .../editor/browser/view/gpu/gpuDisposable.ts | 16 ++++++-------- .../editor/browser/view/gpu/gpuViewLayer.ts | 21 ++++++++----------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/gpuDisposable.ts b/src/vs/editor/browser/view/gpu/gpuDisposable.ts index f5c84aed316..37ee97563b3 100644 --- a/src/vs/editor/browser/view/gpu/gpuDisposable.ts +++ b/src/vs/editor/browser/view/gpu/gpuDisposable.ts @@ -3,15 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type IDisposable } from 'vs/base/common/lifecycle'; +import { type IReference } from 'vs/base/common/lifecycle'; import { isFunction } from 'vs/base/common/types'; -export interface IDisposableGPUObject extends IDisposable { - value: T; -} - export namespace GPULifecycle { - export async function requestDevice(): Promise> { + export async function requestDevice(): Promise> { if (!navigator.gpu) { throw new Error('This browser does not support WebGPU'); } @@ -22,7 +18,7 @@ export namespace GPULifecycle { return wrapDestroyableInDisposable(await adapter.requestDevice()); } - export function createBuffer(device: GPUDevice, descriptor: GPUBufferDescriptor, initialValues?: Float32Array | (() => Float32Array)): IDisposableGPUObject { + export function createBuffer(device: GPUDevice, descriptor: GPUBufferDescriptor, initialValues?: Float32Array | (() => Float32Array)): IReference { const buffer = device.createBuffer(descriptor); if (initialValues) { device.queue.writeBuffer(buffer, 0, isFunction(initialValues) ? initialValues() : initialValues); @@ -30,14 +26,14 @@ export namespace GPULifecycle { return wrapDestroyableInDisposable(buffer); } - export function createTexture(device: GPUDevice, descriptor: GPUTextureDescriptor): IDisposableGPUObject { + export function createTexture(device: GPUDevice, descriptor: GPUTextureDescriptor): IReference { return wrapDestroyableInDisposable(device.createTexture(descriptor)); } } -function wrapDestroyableInDisposable(value: T): IDisposableGPUObject { +function wrapDestroyableInDisposable(value: T): IReference { return { - value, + object: value, dispose: () => value.destroy() }; } diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 568ee52aaf5..eb628b0dba0 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -103,7 +103,7 @@ export class GpuViewLayerRenderer extends Disposable { } async initWebgpu() { - this._device = this._register(await GPULifecycle.requestDevice()).value; + this._device = this._register(await GPULifecycle.requestDevice()).object; const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); this._gpuCtx.configure({ @@ -181,7 +181,7 @@ export class GpuViewLayerRenderer extends Disposable { canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; return canvasDimensionsUniformBufferValues; - })).value; + })).object; this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; @@ -205,22 +205,19 @@ export class GpuViewLayerRenderer extends Disposable { values[AtlasInfoUniformBufferInfo.Offset_Width] = atlas.pageSize; values[AtlasInfoUniformBufferInfo.Offset_Height] = atlas.pageSize; return values; - })).value; + })).object; - /////////////////// - // Static buffer // - /////////////////// this._glyphStorageBuffer[0] = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).value; + })).object; this._glyphStorageBuffer[1] = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).value; + })).object; this._atlasGpuTextureVersions[0] = 0; this._atlasGpuTextureVersions[1] = 0; this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, { @@ -232,7 +229,7 @@ export class GpuViewLayerRenderer extends Disposable { usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, - })).value; + })).object; this._updateAtlas(); @@ -247,7 +244,7 @@ export class GpuViewLayerRenderer extends Disposable { label: 'Monaco vertex buffer', size: quadVertices.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - }, quadVertices)).value; + }, quadVertices)).object; @@ -520,7 +517,7 @@ class FullFileRenderStrategy extends Disposable implemen label: 'Monaco full file cell buffer', size: bufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).value; + })).object; this._cellValueBuffers = [ new ArrayBuffer(bufferSize), new ArrayBuffer(bufferSize), @@ -531,7 +528,7 @@ class FullFileRenderStrategy extends Disposable implemen label: 'Monaco scroll offset buffer', size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - })).value; + })).object; this._scrollOffsetValueBuffers = [ new Float32Array(scrollOffsetBufferSize), new Float32Array(scrollOffsetBufferSize), From 12942ef995165c583edba9fd6bf10e37e3255d72 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:36:51 -0700 Subject: [PATCH 113/286] Docs, simplify allocator interface --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 65 +++++++++++++++++-- .../browser/view/gpu/atlas/textureAtlas.ts | 4 +- .../view/gpu/atlas/textureAtlasPage.ts | 15 ++--- .../gpu/atlas/textureAtlasShelfAllocator.ts | 24 +++---- .../gpu/atlas/textureAtlasSlabAllocator.ts | 19 +++--- .../editor/browser/view/gpu/gpuViewLayer.ts | 10 +-- .../view/gpu/raster/glyphRasterizer.ts | 6 ++ .../editor/contrib/gpu/browser/gpuActions.ts | 2 +- .../view/gpu/atlas/textureAtlas.test.ts | 6 +- .../gpu/atlas/textureAtlasAllocator.test.ts | 17 +---- 10 files changed, 105 insertions(+), 63 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index 3cac69d0efb..d538bb00cbe 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -3,47 +3,98 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { TwoKeyMap } from 'vs/base/common/map'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; -export interface ITextureAtlasGlyph { - textureIndex: number; - index: number; +/** + * Information about a {@link IRasterizedGlyph rasterized glyph} that has been drawn to a texture + * atlas page. + */ +export interface ITextureAtlasPageGlyph { + /** + * The page index of the texture atlas page that the glyph was drawn to. + */ + pageIndex: number; + /** + * The index of the glyph in the texture atlas page. + */ + glyphIndex: number; + /** The x coordinate of the glyph on the texture atlas page. */ x: number; + /** The y coordinate of the glyph on the texture atlas page. */ y: number; + /** The width of the glyph in pixels. */ w: number; + /** The height of the glyph in pixels. */ h: number; + /** The x offset from {@link x} of the glyph's origin. */ originOffsetX: number; + /** The y offset from {@link y} of the glyph's origin. */ originOffsetY: number; } +/** + * A simple bounding box in a 2D plane. + */ export interface IBoundingBox { + /** The left x coordinate (inclusive). */ left: number; + /** The top y coordinate (inclusive). */ top: number; + /** The right x coordinate (exclusive). */ right: number; + /** The bottom y coordinate (exclusive). */ bottom: number; } +/** + * A texture atlas allocator is responsible for taking rasterized glyphs, drawing them to a texture + * atlas page canvas and return information on the texture atlas glyph. + */ export interface ITextureAtlasAllocator { - readonly glyphMap: TwoKeyMap>; /** * Allocates a rasterized glyph to the canvas, drawing it and returning information on its * position in the canvas. This will return undefined if the glyph does not fit on the canvas. */ - allocate(chars: string, tokenFg: number, rasterizedGlyph: Readonly): Readonly | undefined; + allocate(rasterizedGlyph: Readonly): Readonly | undefined; /** * Gets a usage preview of the atlas for debugging purposes. */ getUsagePreview(): Promise; + /** + * Gets statistics about the allocator's current state for debugging purposes. + */ getStats(): string; } +/** + * A texture atlas page that can be read from but not modified. + */ export interface IReadableTextureAtlasPage { + /** + * A unique identifier for the current state of the texture atlas page. This is a number that + * increments whenever a glyph is drawn to the page. + */ readonly version: number; + /** + * A bounding box representing the area of the texture atlas page that is currently in use. + */ readonly usedArea: Readonly; - readonly glyphs: IterableIterator>; + /** + * An iterator over all glyphs that have been drawn to the page. This will iterate through + * glyphs in the order they have been drawn. + */ + readonly glyphs: IterableIterator>; + /** + * The source canvas for the texture atlas page. + */ readonly source: OffscreenCanvas; + /** + * Gets a usage preview of the atlas for debugging purposes. + */ getUsagePreview(): Promise; + /** + * Gets statistics about the allocator's current state for debugging purposes. + */ getStats(): string; } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index a8109e37561..aa3463bcfa2 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -6,7 +6,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import type { IReadableTextureAtlasPage, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; @@ -54,7 +54,7 @@ export class TextureAtlas extends Disposable { } // TODO: Color, style etc. - public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { if (!this._warmedUpRasterizers.has(rasterizer.id)) { this._warmUpAtlas(rasterizer); this._warmedUpRasterizers.add(rasterizer.id); diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 38806be08b8..8f4b1de7e15 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { TwoKeyMap } from 'vs/base/common/map'; -import type { IBoundingBox, IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import type { IBoundingBox, IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -31,11 +31,11 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla private readonly _canvas: OffscreenCanvas; - private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); + private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); // HACK: This is an ordered set of glyphs to be passed to the GPU since currently the shader // uses the index of the glyph. This should be improved to derive from _glyphMap - private readonly _glyphInOrderSet: Set = new Set(); - get glyphs(): IterableIterator { + private readonly _glyphInOrderSet: Set = new Set(); + get glyphs(): IterableIterator { return this._glyphInOrderSet.values(); } @@ -76,7 +76,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla })); } - public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { // Ignore metadata that doesn't affect the glyph metadata ^= (MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); @@ -85,10 +85,10 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla return this._glyphMap.get(chars, metadata) ?? this._createGlyph(rasterizer, chars, metadata); } - private _createGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + private _createGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); // TODO: Handle undefined allocate result - const glyph = this._allocator.allocate(chars, metadata, rasterizedGlyph)!; + const glyph = this._allocator.allocate(rasterizedGlyph)!; this._glyphMap.set(chars, metadata, glyph); this._glyphInOrderSet.add(glyph); @@ -109,7 +109,6 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } getUsagePreview(): Promise { - // TODO: Standardize usage stats and make them loggable return this._allocator.getUsagePreview(); } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts index a17e97745db..1e7fbe9ed34 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TwoKeyMap } from 'vs/base/common/map'; -import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -22,7 +21,8 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { h: 0 }; - readonly glyphMap: TwoKeyMap = new TwoKeyMap(); + /** A set of all glyphs allocated, this is only tracked to enable debug related functionality */ + private _allocatedGlyphs: Set> = new Set(); private _nextIndex = 0; @@ -35,7 +35,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { })); } - public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined { + public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasPageGlyph | undefined { // Finalize and increment row if it doesn't fix horizontally if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) { this._currentRow.x = 0; @@ -66,9 +66,9 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { ); // Create glyph object - const glyph: ITextureAtlasGlyph = { - textureIndex: this._textureIndex, - index: this._nextIndex++, + const glyph: ITextureAtlasPageGlyph = { + pageIndex: this._textureIndex, + glyphIndex: this._nextIndex++, x: this._currentRow.x, y: this._currentRow.y, w: glyphWidth, @@ -82,7 +82,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); // Set the glyph - this.glyphMap.set(chars, tokenFg, glyph); + this._allocatedGlyphs.add(glyph); return glyph; } @@ -97,11 +97,11 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { const rowHeight: Map = new Map(); // y -> h const rowWidth: Map = new Map(); // y -> w - for (const g of this.glyphMap.values()) { + for (const g of this._allocatedGlyphs) { rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); } - for (const g of this.glyphMap.values()) { + for (const g of this._allocatedGlyphs) { ctx.fillStyle = UsagePreviewColors.Used; ctx.fillRect(g.x, g.y, g.w, g.h); ctx.fillStyle = UsagePreviewColors.Wasted; @@ -126,11 +126,11 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { const rowHeight: Map = new Map(); // y -> h const rowWidth: Map = new Map(); // y -> w - for (const g of this.glyphMap.values()) { + for (const g of this._allocatedGlyphs) { rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); } - for (const g of this.glyphMap.values()) { + for (const g of this._allocatedGlyphs) { usedPixels += g.w * g.h; wastedPixels += g.w * (rowHeight.get(g.y)! - g.h); } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index 153c5b72555..2a7b64da2b0 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -5,7 +5,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { TwoKeyMap } from 'vs/base/common/map'; -import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -36,7 +36,8 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private _openRegionsByHeight: Map = new Map(); private _openRegionsByWidth: Map = new Map(); - readonly glyphMap: TwoKeyMap> = new TwoKeyMap(); + /** A set of all glyphs allocated, this is only tracked to enable debug related functionality */ + private _allocatedGlyphs: Set> = new Set(); private readonly _slabW: number; private readonly _slabH: number; @@ -65,7 +66,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH); } - public allocate(chars: string, tokenFg: number, rasterizedGlyph: IRasterizedGlyph): ITextureAtlasGlyph | undefined { + public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasPageGlyph | undefined { // Find ideal slab, creating it if there is none suitable const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; @@ -293,9 +294,9 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { ); // Create glyph object - const glyph: ITextureAtlasGlyph = { - textureIndex: this._textureIndex, - index: this._nextIndex++, + const glyph: ITextureAtlasPageGlyph = { + pageIndex: this._textureIndex, + glyphIndex: this._nextIndex++, x: dx, y: dy, w: glyphWidth, @@ -305,7 +306,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { }; // Set the glyph - this.glyphMap.set(chars, tokenFg, glyph); + this._allocatedGlyphs.add(glyph); return glyph; } @@ -349,7 +350,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } // Draw glyphs - for (const g of this.glyphMap.values()) { + for (const g of this._allocatedGlyphs) { usedPixels += g.w * g.h; ctx.fillStyle = UsagePreviewColors.Used; ctx.fillRect(g.x, g.y, g.w, g.h); @@ -404,7 +405,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } // Draw glyphs - for (const g of this.glyphMap.values()) { + for (const g of this._allocatedGlyphs) { usedPixels += g.w * g.h; } diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index eb628b0dba0..377a826c1b8 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -5,7 +5,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; -import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; import { ensureNonNullable, observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; @@ -386,7 +386,7 @@ interface IRenderStrategy { // #region Full file render strategy -const fullFileRenderStrategyWgsl = ` +const fullFileRenderStrategyWgsl = /*wgsl*/` struct GlyphInfo { position: vec2f, size: vec2f, @@ -548,7 +548,7 @@ class FullFileRenderStrategy extends Disposable implemen let wgslX = 0; let wgslY = 0; let xOffset = 0; - let glyph: Readonly; + let glyph: Readonly; let cellIndex = 0; let tokenStartIndex = 0; @@ -671,8 +671,8 @@ class FullFileRenderStrategy extends Disposable implemen cellBuffer[cellIndex + 1] = -wgslY; // y cellBuffer[cellIndex + 2] = 0; cellBuffer[cellIndex + 3] = 0; - cellBuffer[cellIndex + 4] = glyph.index; // glyphIndex - cellBuffer[cellIndex + 5] = glyph.textureIndex; // textureIndex + cellBuffer[cellIndex + 4] = glyph.glyphIndex; // glyphIndex + cellBuffer[cellIndex + 5] = glyph.pageIndex; // textureIndex } tokenStartIndex = tokenEndIndex; diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 981edcbde4e..197215b492c 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -216,11 +216,17 @@ export interface IBoundingBox { bottom: number; } +/** + * A glyph that has been rasterized to a canvas. + */ export interface IRasterizedGlyph { source: OffscreenCanvas; /** * The bounding box of the glyph within {@link source}. */ boundingBox: IBoundingBox; + /** + * The offset to the glyph's origin (where it should be drawn to). + */ originOffset: { x: number; y: number }; } diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 874ef00a3c8..df1e5f77b1b 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -126,7 +126,7 @@ class DrawGlyphAction extends EditorAction { if (!rasterizedGlyph) { return; } - const imageData = atlas.pages[rasterizedGlyph.textureIndex].source.getContext('2d')?.getImageData( + const imageData = atlas.pages[rasterizedGlyph.pageIndex].source.getContext('2d')?.getImageData( rasterizedGlyph.x, rasterizedGlyph.y, rasterizedGlyph.w, diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 399761b28c3..f0c107e8702 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -6,7 +6,7 @@ import { ok } from 'assert'; import { isNumber } from 'vs/base/common/types'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import type { ITextureAtlasGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -25,7 +25,7 @@ function getUniqueGlyphId(): [chars: string, tokenFg: number] { return [lastUniqueGlyph, blackInt]; } -function assertIsValidGlyph(glyph: Readonly, atlas: TextureAtlas) { +function assertIsValidGlyph(glyph: Readonly, atlas: TextureAtlas) { // (x,y) are valid coordinates ok(isNumber(glyph.x)); ok(glyph.x >= 0); @@ -51,7 +51,7 @@ function assertIsValidGlyph(glyph: Readonly, atlas: TextureA ok(glyph.y + glyph.h <= atlas.pageSize); // Each of the glyph's outer pixel edges contain at least 1 non-transparent pixel - const ctx = ensureNonNullable(atlas.pages[glyph.textureIndex].source.getContext('2d')); + const ctx = ensureNonNullable(atlas.pages[glyph.pageIndex].source.getContext('2d')); const edges = [ ctx.getImageData(glyph.x, glyph.y, glyph.w, 1).data, ctx.getImageData(glyph.x, glyph.y + glyph.h - 1, glyph.w, 1).data, diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 9269ecef2e0..2bd77caadb1 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -11,7 +11,6 @@ import { TextureAtlasSlabAllocator, TextureAtlasSlabAllocatorOptions } from 'vs/ import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; -const blackInt = 0x000000FF; const blackArr = [0x00, 0x00, 0x00, 0xFF]; const pixel1x1 = createRasterizedGlyph(1, 1, [...blackArr]); @@ -31,18 +30,8 @@ function createRasterizedGlyph(w: number, h: number, data: ArrayLike): I }; } -let lastUniqueGlyph: string | undefined; -function getUniqueGlyphId(): [chars: string, tokenFg: number] { - if (!lastUniqueGlyph) { - lastUniqueGlyph = 'a'; - } else { - lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); - } - return [lastUniqueGlyph, blackInt]; -} - function allocateAndAssert(allocator: ITextureAtlasAllocator, rasterizedGlyph: IRasterizedGlyph, expected: { x: number; y: number; w: number; h: number } | undefined): void { - const actual = allocator.allocate(...getUniqueGlyphId(), rasterizedGlyph); + const actual = allocator.allocate(rasterizedGlyph); if (!actual) { strictEqual(actual, expected); return; @@ -58,10 +47,6 @@ function allocateAndAssert(allocator: ITextureAtlasAllocator, rasterizedGlyph: I suite('TextureAtlasAllocator', () => { ensureNoDisposablesAreLeakedInTestSuite(); - suiteSetup(() => { - lastUniqueGlyph = undefined; - }); - suite('TextureAtlasShelfAllocator', () => { function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; allocator: TextureAtlasShelfAllocator } { const canvas = new OffscreenCanvas(w, h); From 5d3eac50c77c0342ef27cec4f4ab1e717a9ee35f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:22:03 -0700 Subject: [PATCH 114/286] Refactors, testglyphrasterizer, basic page overflow --- src/vs/editor/browser/view/gpu/atlas/atlas.ts | 16 +---- .../browser/view/gpu/atlas/textureAtlas.ts | 29 ++++++--- .../view/gpu/atlas/textureAtlasPage.ts | 19 +++--- .../gpu/atlas/textureAtlasShelfAllocator.ts | 2 +- .../gpu/atlas/textureAtlasSlabAllocator.ts | 2 +- .../view/gpu/raster/glyphRasterizer.ts | 25 +------- .../editor/browser/view/gpu/raster/raster.ts | 59 +++++++++++++++++++ .../view/gpu/atlas/textureAtlas.test.ts | 58 +++++++++++++++--- .../gpu/atlas/textureAtlasAllocator.test.ts | 2 +- 9 files changed, 149 insertions(+), 63 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/raster/raster.ts diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/view/gpu/atlas/atlas.ts index d538bb00cbe..b8b4fbf9c8c 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/atlas.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { IBoundingBox, IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; /** * Information about a {@link IRasterizedGlyph rasterized glyph} that has been drawn to a texture @@ -32,20 +32,6 @@ export interface ITextureAtlasPageGlyph { originOffsetY: number; } -/** - * A simple bounding box in a 2D plane. - */ -export interface IBoundingBox { - /** The left x coordinate (inclusive). */ - left: number; - /** The top y coordinate (inclusive). */ - top: number; - /** The right x coordinate (exclusive). */ - right: number; - /** The bottom y coordinate (exclusive). */ - bottom: number; -} - /** * A texture atlas allocator is responsible for taking rasterized glyphs, drawing them to a texture * atlas page canvas and return information on the texture atlas glyph. diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index aa3463bcfa2..df5516d86bf 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; -import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { IGlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/raster'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -49,19 +49,33 @@ export class TextureAtlas extends Disposable { this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, 'slab')); - this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 1, this.pageSize, 'slab')); this._register(toDisposable(() => dispose(this._pages))); } // TODO: Color, style etc. - public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { if (!this._warmedUpRasterizers.has(rasterizer.id)) { this._warmUpAtlas(rasterizer); this._warmedUpRasterizers.add(rasterizer.id); } - // HACK: Draw glyphs to different pages to test out multiple textures while there's no overflow logic - const targetPage = chars.match(/[a-z]/i) ? 0 : 1; - return this._pages[targetPage].getGlyph(rasterizer, chars, metadata); + return this._tryGetGlyph(0, rasterizer, chars, metadata); + } + + private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + // TODO: This should only rasterize a single time, currently it rasterized for each full page before it creates a new page + return ( + this._pages[pageIndex].getGlyph(rasterizer, chars, metadata) ?? ( + (pageIndex + 1 < this._pages.length + ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, metadata) + : undefined) + ) ?? this._getGlyphFromNewPage(rasterizer, chars, metadata) + ); + } + + private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + // TODO: Support more than 2 pages and the GPU texture layer limit + this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, 'slab')); + return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, metadata)!; } public getUsagePreview(): Promise { @@ -76,7 +90,7 @@ export class TextureAtlas extends Disposable { * Warms up the atlas by rasterizing all printable ASCII characters for each token color. This * is distrubuted over multiple idle callbacks to avoid blocking the main thread. */ - private _warmUpAtlas(rasterizer: GlyphRasterizer): void { + private _warmUpAtlas(rasterizer: IGlyphRasterizer): void { this._warmUpTask.value?.clear(); const taskQueue = this._warmUpTask.value = new IdleTaskQueue(); // Warm up using roughly the larger glyphs first to help optimize atlas allocation @@ -106,3 +120,4 @@ export class TextureAtlas extends Disposable { } } } + diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 8f4b1de7e15..49b5097998e 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -6,10 +6,10 @@ import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { TwoKeyMap } from 'vs/base/common/map'; -import type { IBoundingBox, IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; -import type { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { IBoundingBox, IGlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/raster'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -76,25 +76,30 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla })); } - public getGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { // Ignore metadata that doesn't affect the glyph metadata ^= (MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); // TODO: Encode font size and family into key + // IMPORTANT: There are intentionally no intermediate variables here to aid in runtime + // optimization as it's a very hot function return this._glyphMap.get(chars, metadata) ?? this._createGlyph(rasterizer, chars, metadata); } - private _createGlyph(rasterizer: GlyphRasterizer, chars: string, metadata: number): Readonly { + private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); - // TODO: Handle undefined allocate result const glyph = this._allocator.allocate(rasterizedGlyph)!; + if (glyph === undefined) { + // TODO: Log this? In practice it should not happen + return undefined; + } this._glyphMap.set(chars, metadata, glyph); this._glyphInOrderSet.add(glyph); this._version++; - this._usedArea.right = Math.max(this._usedArea.right, glyph.x + glyph.w); - this._usedArea.bottom = Math.max(this._usedArea.bottom, glyph.y + glyph.h); + this._usedArea.right = Math.max(this._usedArea.right, glyph.x + glyph.w - 1); + this._usedArea.bottom = Math.max(this._usedArea.bottom, glyph.y + glyph.h - 1); if (this._logService.getLevel() === LogLevel.Trace) { this._logService.trace('New glyph', { diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts index 1e7fbe9ed34..45eb324ecbb 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts @@ -5,7 +5,7 @@ import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; /** * The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index 2a7b64da2b0..e2b103316bc 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -7,7 +7,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { TwoKeyMap } from 'vs/base/common/map'; import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; export interface TextureAtlasSlabAllocatorOptions { slabW?: number; diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 197215b492c..a33cfaa9494 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -5,6 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import { FontStyle, TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; @@ -25,7 +26,7 @@ const $bbox = $rasterizedGlyph.boundingBox; let nextId = 0; -export class GlyphRasterizer extends Disposable { +export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { /** * A unique identifier for this rasterizer. */ @@ -208,25 +209,3 @@ export class GlyphRasterizer extends Disposable { } } } - -export interface IBoundingBox { - left: number; - top: number; - right: number; - bottom: number; -} - -/** - * A glyph that has been rasterized to a canvas. - */ -export interface IRasterizedGlyph { - source: OffscreenCanvas; - /** - * The bounding box of the glyph within {@link source}. - */ - boundingBox: IBoundingBox; - /** - * The offset to the glyph's origin (where it should be drawn to). - */ - originOffset: { x: number; y: number }; -} diff --git a/src/vs/editor/browser/view/gpu/raster/raster.ts b/src/vs/editor/browser/view/gpu/raster/raster.ts new file mode 100644 index 00000000000..b204596312d --- /dev/null +++ b/src/vs/editor/browser/view/gpu/raster/raster.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; + +export interface IGlyphRasterizer { + /** + * A unique identifier for the rasterizer + */ + id: number; + + /** + * Rasterizes a glyph. + * @param chars The character(s) to rasterize. This can be a single character, a ligature, an + * emoji, etc. + * @param metadata The metadata of the glyph to rasterize. See {@link MetadataConsts} for how + * this works. + * @param colorMap A theme's color map. + */ + rasterizeGlyph( + chars: string, + metadata: number, + colorMap: string[], + ): Readonly; +} + +/** + * A simple bounding box in a 2D plane. + */ +export interface IBoundingBox { + /** The left x coordinate (inclusive). */ + left: number; + /** The top y coordinate (inclusive). */ + top: number; + /** The right x coordinate (inclusive). */ + right: number; + /** The bottom y coordinate (inclusive). */ + bottom: number; +} + +/** + * A glyph that has been rasterized to a canvas. + */ +export interface IRasterizedGlyph { + /** + * The source canvas the glyph was rasterized to. + */ + source: OffscreenCanvas; + /** + * The bounding box of the glyph within {@link source}. + */ + boundingBox: IBoundingBox; + /** + * The offset to the glyph's origin (where it should be drawn to). + */ + originOffset: { x: number; y: number }; +} diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index f0c107e8702..d047043e47c 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ok } from 'assert'; +import { ok, strictEqual } from 'assert'; import { isNumber } from 'vs/base/common/types'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; import { createCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; import type { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -63,6 +63,39 @@ function assertIsValidGlyph(glyph: Readonly, atlas: Text } } +class TestGlyphRasterizer implements IGlyphRasterizer { + readonly id = 0; + nextGlyphColor: [number, number, number, number] = [0, 0, 0, 0]; + nextGlyphDimensions: [number, number] = [0, 0]; + rasterizeGlyph(chars: string, metadata: number, colorMap: string[]): Readonly { + const w = this.nextGlyphDimensions[0]; + const h = this.nextGlyphDimensions[1]; + if (w === 0 || h === 0) { + throw new Error('TestGlyphRasterizer.nextGlyphDimensions must be set to a non-zero value before calling rasterizeGlyph'); + } + const imageData = new ImageData(w, h); + let i = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const [r, g, b, a] = this.nextGlyphColor; + i = (y * w + x) * 4; + imageData.data[i + 0] = r; + imageData.data[i + 1] = g; + imageData.data[i + 2] = b; + imageData.data[i + 3] = a; + } + } + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.putImageData(imageData, 0, 0); + return { + source: canvas, + boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, + originOffset: { x: 0, y: 0 }, + }; + } +} + suite('TextureAtlas', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -71,26 +104,35 @@ suite('TextureAtlas', () => { }); let instantiationService: IInstantiationService; - let glyphRasterizer: GlyphRasterizer; + + let atlas: TextureAtlas; + let glyphRasterizer: TestGlyphRasterizer; setup(() => { instantiationService = createCodeEditorServices(store); - glyphRasterizer = new GlyphRasterizer(10, 'monospace'); + atlas = store.add(instantiationService.createInstance(TextureAtlas, 8)); + glyphRasterizer = new TestGlyphRasterizer(); + glyphRasterizer.nextGlyphDimensions = [1, 1]; + glyphRasterizer.nextGlyphColor = [0, 0, 0, 0xFF]; }); test('get single glyph', () => { - const atlas = store.add(instantiationService.createInstance(TextureAtlas, 512)); assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); }); test('get multiple glyphs', () => { - const atlas = store.add(instantiationService.createInstance(TextureAtlas, 512)); for (let i = 0; i < 10; i++) { assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); } }); - test.skip('adding glyph to full page creates new page', () => { - throw new Error('NYI'); // TODO: Implement + test('adding glyph to full page creates new page', () => { + atlas = store.add(instantiationService.createInstance(TextureAtlas, 2)); + for (let i = 0; i < 4; i++) { + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + } + strictEqual(atlas.pages.length, 1); + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + strictEqual(atlas.pages.length, 2); }); }); diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 2bd77caadb1..91c213267c7 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -9,7 +9,7 @@ import type { ITextureAtlasAllocator } from 'vs/editor/browser/view/gpu/atlas/at import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; import { TextureAtlasSlabAllocator, TextureAtlasSlabAllocatorOptions } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; const blackArr = [0x00, 0x00, 0x00, 0xFF]; From ec77d19b98eb2077fe6d93c95cce05a81eaea192 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 05:51:20 -0700 Subject: [PATCH 115/286] Improve handling of large glyphs when they don't fit into slab --- .../browser/view/gpu/atlas/textureAtlas.ts | 17 +++-- .../view/gpu/atlas/textureAtlasPage.ts | 7 ++- .../gpu/atlas/textureAtlasShelfAllocator.ts | 9 ++- .../gpu/atlas/textureAtlasSlabAllocator.ts | 37 ++++++++--- .../editor/browser/view/gpu/gpuViewLayer.ts | 2 +- .../view/gpu/raster/glyphRasterizer.ts | 62 +++++++++++++------ .../view/gpu/atlas/textureAtlas.test.ts | 37 +++++++++-- 7 files changed, 126 insertions(+), 45 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index df5516d86bf..f17f57fc3e0 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -7,18 +7,22 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; -import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; +import { TextureAtlasPage, type AllocatorType } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import type { IGlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/raster'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +export interface ITextureAtlasOptions { + allocatorType?: AllocatorType; +} + export class TextureAtlas extends Disposable { private _colorMap!: string[]; private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); - private readonly _warmedUpRasterizers = new Set(); + private readonly _allocatorType: AllocatorType; /** * The main texture atlas pages which are both larger textures and more efficiently packed @@ -35,11 +39,14 @@ export class TextureAtlas extends Disposable { constructor( /** The maximum texture size supported by the GPU. */ private readonly _maxTextureSize: number, + options: ITextureAtlasOptions | undefined, @IThemeService private readonly _themeService: IThemeService, @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); + this._allocatorType = options?.allocatorType ?? 'slab'; + this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { // TODO: Clear entire atlas on theme change this._colorMap = this._themeService.getColorTheme().tokenColorMap; @@ -48,7 +55,7 @@ export class TextureAtlas extends Disposable { const dprFactor = Math.max(1, Math.floor(getActiveWindow().devicePixelRatio)); this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); - this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, 'slab')); + this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, this._allocatorType)); this._register(toDisposable(() => dispose(this._pages))); } @@ -62,7 +69,7 @@ export class TextureAtlas extends Disposable { } private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { - // TODO: This should only rasterize a single time, currently it rasterized for each full page before it creates a new page + // TODO: Should the texture atlas have a map of glyphs to pages so this doesn't iterate through all pages? return ( this._pages[pageIndex].getGlyph(rasterizer, chars, metadata) ?? ( (pageIndex + 1 < this._pages.length @@ -74,7 +81,7 @@ export class TextureAtlas extends Disposable { private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { // TODO: Support more than 2 pages and the GPU texture layer limit - this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, 'slab')); + this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType)); return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, metadata)!; } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 49b5097998e..2d730bcddc1 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -14,6 +14,8 @@ import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +export type AllocatorType = 'shelf' | 'slab' | ((canvas: OffscreenCanvas, textureIndex: number) => ITextureAtlasAllocator); + export class TextureAtlasPage extends Disposable implements IReadableTextureAtlasPage { private _version: number = 0; @@ -51,7 +53,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla constructor( textureIndex: number, pageSize: number, - allocatorType: 'shelf' | 'slab', + allocatorType: AllocatorType, @ILogService private readonly _logService: ILogService, @IThemeService private readonly _themeService: IThemeService, ) { @@ -62,6 +64,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla switch (allocatorType) { case 'shelf': this._allocator = new TextureAtlasShelfAllocator(this._canvas, textureIndex); break; case 'slab': this._allocator = new TextureAtlasSlabAllocator(this._canvas, textureIndex); break; + default: this._allocator = allocatorType(this._canvas, textureIndex); break; } this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { @@ -89,7 +92,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); - const glyph = this._allocator.allocate(rasterizedGlyph)!; + const glyph = this._allocator.allocate(rasterizedGlyph); if (glyph === undefined) { // TODO: Log this? In practice it should not happen return undefined; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts index 45eb324ecbb..247cfa960b7 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts @@ -36,6 +36,13 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { } public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasPageGlyph | undefined { + // The glyph does not fit into the atlas page + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; + if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) { + throw new Error('Glyph is too large for the atlas page'); + } + // Finalize and increment row if it doesn't fix horizontally if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) { this._currentRow.x = 0; @@ -49,8 +56,6 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { } // Draw glyph - const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; - const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; this._ctx.drawImage( rasterizedGlyph.source, // source diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index e2b103316bc..48bc2a2282d 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -39,10 +39,10 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { /** A set of all glyphs allocated, this is only tracked to enable debug related functionality */ private _allocatedGlyphs: Set> = new Set(); - private readonly _slabW: number; - private readonly _slabH: number; - private readonly _slabsPerRow: number; - private readonly _slabsPerColumn: number; + private _slabW: number; + private _slabH: number; + private _slabsPerRow: number; + private _slabsPerColumn: number; private _nextIndex = 0; constructor( @@ -70,6 +70,30 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // Find ideal slab, creating it if there is none suitable const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; + + // The glyph does not fit into the atlas page, glyphs should never be this large in practice + if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) { + throw new Error('Glyph is too large for the atlas page'); + } + + // The glyph does not fit into a slab + if (glyphWidth > this._slabW || glyphHeight > this._slabH) { + // Only if this is the allocator's first glyph, resize the slab size to fit the glyph. + if (this._allocatedGlyphs.size > 0) { + return undefined; + } + // Find the largest power of 2 devisor that the glyph fits into, this ensure there is no + // wasted space outside the allocated slabs. + let sizeCandidate = this._canvas.width; + while (glyphWidth < sizeCandidate / 2 && glyphHeight < sizeCandidate / 2) { + sizeCandidate /= 2; + } + this._slabW = sizeCandidate; + this._slabH = sizeCandidate; + this._slabsPerRow = Math.floor(this._canvas.width / this._slabW); + this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH); + } + // const dpr = getActiveWindow().devicePixelRatio; // TODO: Include font size as well as DPR in nearestXPixels calculation @@ -107,11 +131,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } } - // The glyph does not fit into a slab - if (glyphWidth > this._slabW || glyphHeight > this._slabH) { - return undefined; - } - let dx: number | undefined; let dy: number | undefined; diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 377a826c1b8..6d9cc902132 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -115,7 +115,7 @@ export class GpuViewLayerRenderer extends Disposable { // Create texture atlas if (!GpuViewLayerRenderer.atlas) { - GpuViewLayerRenderer.atlas = this._instantiationService.createInstance(TextureAtlas, this._device.limits.maxTextureDimension2D); + GpuViewLayerRenderer.atlas = this._instantiationService.createInstance(TextureAtlas, this._device.limits.maxTextureDimension2D, undefined); } const atlas = GpuViewLayerRenderer.atlas; diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index a33cfaa9494..540e8db81f7 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -9,21 +9,6 @@ import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import { FontStyle, TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; -const $rasterizedGlyph: IRasterizedGlyph = { - source: null!, - boundingBox: { - left: 0, - bottom: 0, - right: 0, - top: 0, - }, - originOffset: { - x: 0, - y: 0, - } -}; -const $bbox = $rasterizedGlyph.boundingBox; - let nextId = 0; export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { @@ -36,6 +21,25 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // A temporary context that glyphs are drawn to before being transfered to the atlas. private _ctx: OffscreenCanvasRenderingContext2D; + private _workGlyph: IRasterizedGlyph = { + source: null!, + boundingBox: { + left: 0, + bottom: 0, + right: 0, + top: 0, + }, + originOffset: { + x: 0, + y: 0, + } + }; + + private _workGlyphConfig: { + chars: string; + metadata: number; + } = { chars: '', metadata: 0 }; + constructor( private readonly _fontSize: number, private readonly _fontFamily: string, @@ -60,6 +64,22 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { chars: string, metadata: number, colorMap: string[], + ): Readonly { + // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary + // work when the rasterizer is called multiple times like when the glyph doesn't fit into a + // page. + if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === metadata) { + return this._workGlyph; + } + this._workGlyphConfig.chars = chars; + this._workGlyphConfig.metadata = metadata; + return this._rasterizeGlyph(chars, metadata, colorMap); + } + + public _rasterizeGlyph( + chars: string, + metadata: number, + colorMap: string[], ): Readonly { // TODO: Support workbench.fontAliasing this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); @@ -88,7 +108,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._ctx.fillText(chars, originX, originY); const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); - this._findGlyphBoundingBox(imageData, $rasterizedGlyph.boundingBox); + this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); // const offset = { // x: textMetrics.actualBoundingBoxLeft, // y: textMetrics.actualBoundingBoxAscent @@ -100,9 +120,9 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // yInt: Math.ceil(textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent), // }; // console.log(`${chars}_${fg}`, textMetrics, boundingBox, originX, originY, { width: boundingBox.right - boundingBox.left, height: boundingBox.bottom - boundingBox.top }); - $rasterizedGlyph.source = this._canvas; - $rasterizedGlyph.originOffset.x = $bbox.left - originX; - $rasterizedGlyph.originOffset.y = $bbox.top - originY; + this._workGlyph.source = this._canvas; + this._workGlyph.originOffset.x = this._workGlyph.boundingBox.left - originX; + this._workGlyph.originOffset.y = this._workGlyph.boundingBox.top - originY; // const result2: IRasterizedGlyph = { // source: this._canvas, @@ -139,7 +159,9 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // debugger; // } - return $rasterizedGlyph; + + + return this._workGlyph; } // TODO: Does this even need to happen when measure text is used? diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index d047043e47c..664080345d3 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ok, strictEqual } from 'assert'; +import { ok, strictEqual, throws } from 'assert'; import { isNumber } from 'vs/base/common/types'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; import { createCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; @@ -37,10 +38,10 @@ function assertIsValidGlyph(glyph: Readonly, atlas: Text // (w,h) are valid dimensions ok(isNumber(glyph.w)); ok(glyph.w > 0); - ok(glyph.w < atlas.pageSize); + ok(glyph.w <= atlas.pageSize); ok(isNumber(glyph.h)); ok(glyph.h > 0); - ok(glyph.h < atlas.pageSize); + ok(glyph.h <= atlas.pageSize); // (originOffsetX, originOffsetY) are valid offsets ok(isNumber(glyph.originOffsetX)); @@ -110,7 +111,7 @@ suite('TextureAtlas', () => { setup(() => { instantiationService = createCodeEditorServices(store); - atlas = store.add(instantiationService.createInstance(TextureAtlas, 8)); + atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, undefined)); glyphRasterizer = new TestGlyphRasterizer(); glyphRasterizer.nextGlyphDimensions = [1, 1]; glyphRasterizer.nextGlyphColor = [0, 0, 0, 0xFF]; @@ -121,18 +122,42 @@ suite('TextureAtlas', () => { }); test('get multiple glyphs', () => { + atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, undefined)); for (let i = 0; i < 10; i++) { assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); } }); test('adding glyph to full page creates new page', () => { - atlas = store.add(instantiationService.createInstance(TextureAtlas, 2)); for (let i = 0; i < 4; i++) { assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); } strictEqual(atlas.pages.length, 1); assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); - strictEqual(atlas.pages.length, 2); + strictEqual(atlas.pages.length, 2, 'the 5th glyph should overflow to a new page'); + }); + + test('adding a glyph larger than the atlas', () => { + glyphRasterizer.nextGlyphDimensions = [3, 2]; + throws(() => atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), 'should throw when the glyph is too large, this should not happen in practice'); + }); + + test('adding a glyph larger than the standard slab size', () => { + glyphRasterizer.nextGlyphDimensions = [2, 2]; + atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, { + allocatorType: (canvas, textureIndex) => new TextureAtlasSlabAllocator(canvas, textureIndex, { slabW: 1, slabH: 1 }) + })); + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + }); + + test('adding a non-first glyph larger than the standard slab size, causing an overflow to a new page', () => { + atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, { + allocatorType: (canvas, textureIndex) => new TextureAtlasSlabAllocator(canvas, textureIndex, { slabW: 1, slabH: 1 }) + })); + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + strictEqual(atlas.pages.length, 1); + glyphRasterizer.nextGlyphDimensions = [2, 2]; + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + strictEqual(atlas.pages.length, 2, 'the 2nd glyph should overflow to a new page with a larger slab size'); }); }); From 740ba1ce332ea24b51baef034c3ccfa926d3790f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 06:59:53 -0700 Subject: [PATCH 116/286] Fix metadata key, glyphinfo buffer size and make getGlyph page lookup O(1) amortized --- .../browser/view/gpu/atlas/textureAtlas.ts | 31 ++++++++++++++----- .../view/gpu/atlas/textureAtlasPage.ts | 6 ---- .../editor/browser/view/gpu/gpuViewLayer.ts | 3 +- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index f17f57fc3e0..d7015ccf611 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -6,6 +6,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { TwoKeyMap } from 'vs/base/common/map'; import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasPage, type AllocatorType } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import type { IGlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/raster'; @@ -36,6 +37,14 @@ export class TextureAtlas extends Disposable { readonly pageSize: number; + /** + * A maps of glyph keys to the page to start searching for the glyph. This is set before + * searching to have as little runtime overhead (branching, intermediate variables) as possible, + * so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all + * pages with a lower index do not contain the glyph. + */ + private readonly _glyphPageIndex: TwoKeyMap = new TwoKeyMap(); + constructor( /** The maximum texture size supported by the GPU. */ private readonly _maxTextureSize: number, @@ -56,32 +65,38 @@ export class TextureAtlas extends Disposable { this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, this._allocatorType)); + this._register(toDisposable(() => dispose(this._pages))); } - // TODO: Color, style etc. public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + // Ignore metadata that doesn't affect the glyph + metadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); + + // TODO: Encode font size and family into key + if (!this._warmedUpRasterizers.has(rasterizer.id)) { this._warmUpAtlas(rasterizer); this._warmedUpRasterizers.add(rasterizer.id); } - return this._tryGetGlyph(0, rasterizer, chars, metadata); + return this._tryGetGlyph(this._glyphPageIndex.get(chars, metadata) ?? 0, rasterizer, chars, metadata); } private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { - // TODO: Should the texture atlas have a map of glyphs to pages so this doesn't iterate through all pages? + this._glyphPageIndex.set(chars, metadata, pageIndex); return ( - this._pages[pageIndex].getGlyph(rasterizer, chars, metadata) ?? ( - (pageIndex + 1 < this._pages.length - ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, metadata) - : undefined) - ) ?? this._getGlyphFromNewPage(rasterizer, chars, metadata) + this._pages[pageIndex].getGlyph(rasterizer, chars, metadata) + ?? (pageIndex + 1 < this._pages.length + ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, metadata) + : undefined) + ?? this._getGlyphFromNewPage(rasterizer, chars, metadata) ); } private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { // TODO: Support more than 2 pages and the GPU texture layer limit this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType)); + this._glyphPageIndex.set(chars, metadata, this._pages.length - 1); return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, metadata)!; } diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 2d730bcddc1..e1cace47696 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -10,7 +10,6 @@ import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPa import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import type { IBoundingBox, IGlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/raster'; -import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -80,11 +79,6 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { - // Ignore metadata that doesn't affect the glyph - metadata ^= (MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); - - // TODO: Encode font size and family into key - // IMPORTANT: There are intentionally no intermediate variables here to aid in runtime // optimization as it's a very hot function return this._glyphMap.get(chars, metadata) ?? this._createGlyph(rasterizer, chars, metadata); diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 6d9cc902132..e9f11698f7a 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -298,8 +298,7 @@ export class GpuViewLayerRenderer extends Disposable { this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version); // TODO: Dynamically set buffer size - const bufferSize = GlyphStorageBufferInfo.FloatsPerEntry * Constants.MaxAtlasPageGlyphCount; - const values = new Float32Array(bufferSize / 4); + const values = new Float32Array(GlyphStorageBufferInfo.FloatsPerEntry * Constants.MaxAtlasPageGlyphCount); let entryOffset = 0; for (const glyph of page.glyphs) { values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition] = glyph.x; From 81b7e000d5b549ef2c704f019ae1dc3dbd5450fc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 08:34:57 -0700 Subject: [PATCH 117/286] Throw in error case where writing more glyphs than a page can handle --- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index e9f11698f7a..781329509dd 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -309,6 +309,9 @@ export class GpuViewLayerRenderer extends Disposable { values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; entryOffset += GlyphStorageBufferInfo.FloatsPerEntry; } + if (entryOffset / GlyphStorageBufferInfo.FloatsPerEntry > Constants.MaxAtlasPageGlyphCount) { + throw new Error(`Attempting to write more glyphs (${entryOffset / GlyphStorageBufferInfo.FloatsPerEntry}) than the GPUBuffer can hold (${Constants.MaxAtlasPageGlyphCount})`); + } this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); this._device.queue.copyExternalImageToTexture( { source: page.source }, From 4de205a04ec7e5e2fca3b81872d02f887aeffa2b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 08:40:37 -0700 Subject: [PATCH 118/286] Share atlas page and gpu side max glyph constants --- .../browser/view/gpu/atlas/textureAtlasPage.ts | 5 +++++ src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 13 ++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index e1cace47696..11da53fe53f 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -18,6 +18,8 @@ export type AllocatorType = 'shelf' | 'slab' | ((canvas: OffscreenCanvas, textur export class TextureAtlasPage extends Disposable implements IReadableTextureAtlasPage { private _version: number = 0; + static readonly maximumGlyphCount = 5_000; + private _usedArea: IBoundingBox = { left: 0, top: 0, right: 0, bottom: 0 }; public get usedArea(): Readonly { return this._usedArea; @@ -85,6 +87,9 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { + if (this._glyphInOrderSet.size >= TextureAtlasPage.maximumGlyphCount) { + return undefined; + } const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); const glyph = this._allocator.allocate(rasterizedGlyph); if (glyph === undefined) { diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 781329509dd..928303984d2 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -7,6 +7,7 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; import { ensureNonNullable, observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; @@ -29,8 +30,6 @@ interface IRendererContext { const enum Constants { IndicesPerCell = 6, - - MaxAtlasPageGlyphCount = 10_000, } const enum GlyphStorageBufferInfo { @@ -210,12 +209,12 @@ export class GpuViewLayerRenderer extends Disposable { this._glyphStorageBuffer[0] = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', - size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, + size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, })).object; this._glyphStorageBuffer[1] = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco glyph storage buffer', - size: GlyphStorageBufferInfo.BytesPerEntry * Constants.MaxAtlasPageGlyphCount, + size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, })).object; this._atlasGpuTextureVersions[0] = 0; @@ -298,7 +297,7 @@ export class GpuViewLayerRenderer extends Disposable { this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version); // TODO: Dynamically set buffer size - const values = new Float32Array(GlyphStorageBufferInfo.FloatsPerEntry * Constants.MaxAtlasPageGlyphCount); + const values = new Float32Array(GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount); let entryOffset = 0; for (const glyph of page.glyphs) { values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition] = glyph.x; @@ -309,8 +308,8 @@ export class GpuViewLayerRenderer extends Disposable { values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; entryOffset += GlyphStorageBufferInfo.FloatsPerEntry; } - if (entryOffset / GlyphStorageBufferInfo.FloatsPerEntry > Constants.MaxAtlasPageGlyphCount) { - throw new Error(`Attempting to write more glyphs (${entryOffset / GlyphStorageBufferInfo.FloatsPerEntry}) than the GPUBuffer can hold (${Constants.MaxAtlasPageGlyphCount})`); + if (entryOffset / GlyphStorageBufferInfo.FloatsPerEntry > TextureAtlasPage.maximumGlyphCount) { + throw new Error(`Attempting to write more glyphs (${entryOffset / GlyphStorageBufferInfo.FloatsPerEntry}) than the GPUBuffer can hold (${TextureAtlasPage.maximumGlyphCount})`); } this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); this._device.queue.copyExternalImageToTexture( From 17eb9a86343a6ea9cc4e09c042a06c07d268667e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 08:49:04 -0700 Subject: [PATCH 119/286] Fix allocator tests for new large glyph logic --- .../view/gpu/atlas/textureAtlasPage.ts | 24 ++++++++++++------- .../gpu/atlas/textureAtlasAllocator.test.ts | 15 ++++++++---- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index 11da53fe53f..fbaa96afa0e 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -18,6 +18,10 @@ export type AllocatorType = 'shelf' | 'slab' | ((canvas: OffscreenCanvas, textur export class TextureAtlasPage extends Disposable implements IReadableTextureAtlasPage { private _version: number = 0; + /** + * The maximum number of glyphs that can be drawn to the page. This is currently a hard static + * cap that must not be reached as it will cause the GPU buffer to overflow. + */ static readonly maximumGlyphCount = 5_000; private _usedArea: IBoundingBox = { left: 0, top: 0, right: 0, bottom: 0 }; @@ -33,24 +37,19 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } private readonly _canvas: OffscreenCanvas; + get source(): OffscreenCanvas { + return this._canvas; + } private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); - // HACK: This is an ordered set of glyphs to be passed to the GPU since currently the shader - // uses the index of the glyph. This should be improved to derive from _glyphMap private readonly _glyphInOrderSet: Set = new Set(); get glyphs(): IterableIterator { return this._glyphInOrderSet.values(); } private readonly _allocator: ITextureAtlasAllocator; - private _colorMap!: string[]; - get source(): OffscreenCanvas { - return this._canvas; - } - - // TODO: Should pull in the font size from config instead of random dom node constructor( textureIndex: number, pageSize: number, @@ -87,18 +86,25 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla } private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { + // Ensure the glyph can fit on the page if (this._glyphInOrderSet.size >= TextureAtlasPage.maximumGlyphCount) { return undefined; } + + // Rasterize and allocate the glyph const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); const glyph = this._allocator.allocate(rasterizedGlyph); + + // Ensure the glyph was allocated if (glyph === undefined) { - // TODO: Log this? In practice it should not happen return undefined; } + + // Save the glyph this._glyphMap.set(chars, metadata, glyph); this._glyphInOrderSet.add(glyph); + // Update page version and it's tracked used area this._version++; this._usedArea.right = Math.max(this._usedArea.right, glyph.x + glyph.w - 1); this._usedArea.bottom = Math.max(this._usedArea.bottom, glyph.y + glyph.h - 1); diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 91c213267c7..d0c35a63549 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { deepStrictEqual, strictEqual } from 'assert'; +import { deepStrictEqual, strictEqual, throws } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import type { ITextureAtlasAllocator } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; @@ -149,13 +149,20 @@ suite('TextureAtlasAllocator', () => { allocateAndAssert(allocator, pixel1x1, undefined); }); + // TODO: Share test with other allocator test('glyph too large for canvas', () => { - const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); - allocateAndAssert(allocator, pixel2x1, undefined); + const { allocator } = initAllocator(1, 1); + throws(() => allocateAndAssert(allocator, pixel2x1, undefined), new Error('Glyph is too large for the atlas page')); }); - test('glyph too large for slab', () => { + test('glyph too large for slab (increase slab size for first glyph)', () => { const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 1 }); + allocateAndAssert(allocator, pixel2x1, { x: 0, y: 0, w: 2, h: 1 }); + }); + + test('glyph too large for slab (undefined as it\'s not the first glyph)', () => { + const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); allocateAndAssert(allocator, pixel2x1, undefined); }); From d183152cf96e7c4bba947d536b8323d2722bed78 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:02:10 -0700 Subject: [PATCH 120/286] Shared allocator tests --- .../test/browser/view/gpu/atlas/testUtil.ts | 55 +++++++++++++++++++ .../view/gpu/atlas/textureAtlas.test.ts | 43 +-------------- .../gpu/atlas/textureAtlasAllocator.test.ts | 49 +++++++++++------ 3 files changed, 90 insertions(+), 57 deletions(-) create mode 100644 src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts diff --git a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts b/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts new file mode 100644 index 00000000000..bb3d3c2b509 --- /dev/null +++ b/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { fail, ok } from 'assert'; +import { isNumber } from 'vs/base/common/types'; +import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; + +export function assertIsValidGlyph(glyph: Readonly | undefined, atlasOrSource: TextureAtlas | OffscreenCanvas) { + if (glyph === undefined) { + fail('glyph is undefined'); + } + const pageW = atlasOrSource instanceof TextureAtlas ? atlasOrSource.pageSize : atlasOrSource.width; + const pageH = atlasOrSource instanceof TextureAtlas ? atlasOrSource.pageSize : atlasOrSource.width; + const source = atlasOrSource instanceof TextureAtlas ? atlasOrSource.pages[glyph.pageIndex].source : atlasOrSource; + + // (x,y) are valid coordinates + ok(isNumber(glyph.x)); + ok(glyph.x >= 0); + ok(glyph.x < pageW); + ok(isNumber(glyph.y)); + ok(glyph.y >= 0); + ok(glyph.y < pageH); + + // (w,h) are valid dimensions + ok(isNumber(glyph.w)); + ok(glyph.w > 0); + ok(glyph.w <= pageW); + ok(isNumber(glyph.h)); + ok(glyph.h > 0); + ok(glyph.h <= pageH); + + // (originOffsetX, originOffsetY) are valid offsets + ok(isNumber(glyph.originOffsetX)); + ok(isNumber(glyph.originOffsetY)); + + // (x,y) + (w,h) are within the bounds of the atlas + ok(glyph.x + glyph.w <= pageW); + ok(glyph.y + glyph.h <= pageH); + + // Each of the glyph's outer pixel edges contain at least 1 non-transparent pixel + const ctx = ensureNonNullable(source.getContext('2d')); + const edges = [ + ctx.getImageData(glyph.x, glyph.y, glyph.w, 1).data, + ctx.getImageData(glyph.x, glyph.y + glyph.h - 1, glyph.w, 1).data, + ctx.getImageData(glyph.x, glyph.y, 1, glyph.h).data, + ctx.getImageData(glyph.x + glyph.w - 1, glyph.y, 1, glyph.h).data, + ]; + for (const edge of edges) { + ok(edge.some(color => (color & 0xFF) !== 0)); + } +} diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 664080345d3..c617a66001b 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ok, strictEqual, throws } from 'assert'; -import { isNumber } from 'vs/base/common/types'; +import { strictEqual, throws } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; import { createCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; +import { assertIsValidGlyph } from 'vs/editor/test/browser/view/gpu/atlas/testUtil'; import type { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const blackInt = 0x000000FF; @@ -26,44 +25,6 @@ function getUniqueGlyphId(): [chars: string, tokenFg: number] { return [lastUniqueGlyph, blackInt]; } -function assertIsValidGlyph(glyph: Readonly, atlas: TextureAtlas) { - // (x,y) are valid coordinates - ok(isNumber(glyph.x)); - ok(glyph.x >= 0); - ok(glyph.x < atlas.pageSize); - ok(isNumber(glyph.y)); - ok(glyph.y >= 0); - ok(glyph.y < atlas.pageSize); - - // (w,h) are valid dimensions - ok(isNumber(glyph.w)); - ok(glyph.w > 0); - ok(glyph.w <= atlas.pageSize); - ok(isNumber(glyph.h)); - ok(glyph.h > 0); - ok(glyph.h <= atlas.pageSize); - - // (originOffsetX, originOffsetY) are valid offsets - ok(isNumber(glyph.originOffsetX)); - ok(isNumber(glyph.originOffsetY)); - - // (x,y) + (w,h) are within the bounds of the atlas - ok(glyph.x + glyph.w <= atlas.pageSize); - ok(glyph.y + glyph.h <= atlas.pageSize); - - // Each of the glyph's outer pixel edges contain at least 1 non-transparent pixel - const ctx = ensureNonNullable(atlas.pages[glyph.pageIndex].source.getContext('2d')); - const edges = [ - ctx.getImageData(glyph.x, glyph.y, glyph.w, 1).data, - ctx.getImageData(glyph.x, glyph.y + glyph.h - 1, glyph.w, 1).data, - ctx.getImageData(glyph.x, glyph.y, 1, glyph.h).data, - ctx.getImageData(glyph.x + glyph.w - 1, glyph.y, 1, glyph.h).data, - ]; - for (const edge of edges) { - ok(edge.some(color => (color & 0xFF) !== 0)); - } -} - class TestGlyphRasterizer implements IGlyphRasterizer { readonly id = 0; nextGlyphColor: [number, number, number, number] = [0, 0, 0, 0]; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index d0c35a63549..9036c90759e 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -10,6 +10,7 @@ import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/tex import { TextureAtlasSlabAllocator, TextureAtlasSlabAllocatorOptions } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; +import { assertIsValidGlyph } from 'vs/editor/test/browser/view/gpu/atlas/testUtil'; const blackArr = [0x00, 0x00, 0x00, 0xFF]; @@ -44,15 +45,41 @@ function allocateAndAssert(allocator: ITextureAtlasAllocator, rasterizedGlyph: I }, expected); } +function initShelfAllocator(w: number, h: number): { canvas: OffscreenCanvas; allocator: TextureAtlasShelfAllocator } { + const canvas = new OffscreenCanvas(w, h); + const allocator = new TextureAtlasShelfAllocator(canvas, 0); + return { canvas, allocator }; +} + +function initSlabAllocator(w: number, h: number, options?: TextureAtlasSlabAllocatorOptions): { canvas: OffscreenCanvas; allocator: TextureAtlasSlabAllocator } { + const canvas = new OffscreenCanvas(w, h); + const allocator = new TextureAtlasSlabAllocator(canvas, 0, options); + return { canvas, allocator }; +} + +const allocatorDefinitions: { name: string; initAllocator: (w: number, h: number) => { canvas: OffscreenCanvas; allocator: ITextureAtlasAllocator } }[] = [ + { name: 'shelf', initAllocator: initShelfAllocator }, + { name: 'slab', initAllocator: initSlabAllocator }, +]; + suite('TextureAtlasAllocator', () => { ensureNoDisposablesAreLeakedInTestSuite(); - suite('TextureAtlasShelfAllocator', () => { - function initAllocator(w: number, h: number): { canvas: OffscreenCanvas; allocator: TextureAtlasShelfAllocator } { - const canvas = new OffscreenCanvas(w, h); - const allocator = new TextureAtlasShelfAllocator(canvas, 0); - return { canvas, allocator }; + suite('shared tests', () => { + for (const { name, initAllocator } of allocatorDefinitions) { + test('single allocation', () => { + const { canvas, allocator } = initAllocator(2, 2); + assertIsValidGlyph(allocator.allocate(pixel1x1), canvas); + }); + test(`(${name}) glyph too large for canvas`, () => { + const { allocator } = initAllocator(1, 1); + throws(() => allocateAndAssert(allocator, pixel2x1, undefined), new Error('Glyph is too large for the atlas page')); + }); } + }); + + suite('TextureAtlasShelfAllocator', () => { + const initAllocator = initShelfAllocator; test('single allocation', () => { const { allocator } = initAllocator(2, 2); @@ -96,11 +123,7 @@ suite('TextureAtlasAllocator', () => { }); suite('TextureAtlasSlabAllocator', () => { - function initAllocator(w: number, h: number, options?: TextureAtlasSlabAllocatorOptions): { canvas: OffscreenCanvas; allocator: TextureAtlasSlabAllocator } { - const canvas = new OffscreenCanvas(w, h); - const allocator = new TextureAtlasSlabAllocator(canvas, 0, options); - return { canvas, allocator }; - } + const initAllocator = initSlabAllocator; test('single allocation', () => { const { allocator } = initAllocator(2, 2); @@ -149,12 +172,6 @@ suite('TextureAtlasAllocator', () => { allocateAndAssert(allocator, pixel1x1, undefined); }); - // TODO: Share test with other allocator - test('glyph too large for canvas', () => { - const { allocator } = initAllocator(1, 1); - throws(() => allocateAndAssert(allocator, pixel2x1, undefined), new Error('Glyph is too large for the atlas page')); - }); - test('glyph too large for slab (increase slab size for first glyph)', () => { const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 1 }); allocateAndAssert(allocator, pixel2x1, { x: 0, y: 0, w: 2, h: 1 }); From 06ff7ac7e9e02edf93df31f1a94cbb0324638407 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:23:07 -0700 Subject: [PATCH 121/286] Fix null cells rendering the first glyph to the middle of the editor --- .../editor/browser/view/gpu/atlas/textureAtlas.ts | 14 +++++++++++--- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 8 ++++---- .../browser/view/gpu/raster/glyphRasterizer.ts | 8 ++------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index d7015ccf611..45ecf1c711d 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -9,6 +9,7 @@ import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/co import { TwoKeyMap } from 'vs/base/common/map'; import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlasPage, type AllocatorType } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; +import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IGlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/raster'; import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; @@ -64,21 +65,28 @@ export class TextureAtlas extends Disposable { const dprFactor = Math.max(1, Math.floor(getActiveWindow().devicePixelRatio)); this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); - this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, this._allocatorType)); + const firstPage = this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, this._allocatorType); + this._pages.push(firstPage); + + // IMPORTANT: The first glyph on the first page must be an empty glyph such that zeroed out + // cells end up rendering nothing + firstPage.getGlyph(new GlyphRasterizer(1, ''), '', 0); this._register(toDisposable(() => dispose(this._pages))); } public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + // TODO: Encode font size and family into key // Ignore metadata that doesn't affect the glyph metadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); - // TODO: Encode font size and family into key - + // Warm up common glyphs if (!this._warmedUpRasterizers.has(rasterizer.id)) { this._warmUpAtlas(rasterizer); this._warmedUpRasterizers.add(rasterizer.id); } + + // Try get the glyph, overflowing to a new page if necessary return this._tryGetGlyph(this._glyphPageIndex.get(chars, metadata) ?? 0, rasterizer, chars, metadata); } diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 928303984d2..d342dee9977 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -483,6 +483,7 @@ class FullFileRenderStrategy extends Disposable implemen private _activeDoubleBufferIndex: 0 | 1 = 0; private readonly _upToDateLines: [Set, Set] = [new Set(), new Set()]; + private _visibleObjectCount: number = 0; private _scrollOffsetBindBuffer!: GPUBuffer; private _scrollOffsetValueBuffers!: [Float32Array, Float32Array]; @@ -711,18 +712,17 @@ class FullFileRenderStrategy extends Disposable implemen this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; + this._visibleObjectCount = visibleObjectCount; return visibleObjectCount; } draw(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void { - const visibleObjectCount = (stopLineNumber - startLineNumber + 1) * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; - - if (visibleObjectCount <= 0) { + if (this._visibleObjectCount <= 0) { console.error('Attempt to draw 0 objects'); } else { pass.draw( 6, // square verticies - visibleObjectCount, + this._visibleObjectCount, undefined, (startLineNumber - 1) * FullFileRenderStrategy._columnCount ); diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 540e8db81f7..23f4da25469 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -18,7 +18,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { public readonly id = nextId++; private _canvas: OffscreenCanvas; - // A temporary context that glyphs are drawn to before being transfered to the atlas. private _ctx: OffscreenCanvasRenderingContext2D; private _workGlyph: IRasterizedGlyph = { @@ -36,9 +35,9 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { }; private _workGlyphConfig: { - chars: string; + chars: string | undefined; metadata: number; - } = { chars: '', metadata: 0 }; + } = { chars: undefined, metadata: 0 }; constructor( private readonly _fontSize: number, @@ -138,9 +137,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // } // }; - // DEBUG: Show image data in console - // (console as any).image(imageData); - // TODO: Verify result 1 and 2 are the same // if (result2.boundingBox.left > result.boundingBox.left) { From efeb71f955cf59d5b8c8e9b20b22f5ac05eacfd8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:25:52 -0700 Subject: [PATCH 122/286] Consistency pass --- .../browser/view/gpu/atlas/textureAtlas.ts | 4 +--- .../browser/view/gpu/atlas/textureAtlasPage.ts | 17 ++++------------- .../view/gpu/atlas/textureAtlasSlabAllocator.ts | 1 - .../browser/view/gpu/raster/glyphRasterizer.ts | 9 +-------- src/vs/editor/browser/view/gpu/raster/raster.ts | 2 +- 5 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts index 45ecf1c711d..b176e01fa89 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts @@ -32,9 +32,7 @@ export class TextureAtlas extends Disposable { * much less frequently so as to not drop frames. */ private readonly _pages: TextureAtlasPage[] = []; - get pages(): IReadableTextureAtlasPage[] { - return this._pages; - } + get pages(): IReadableTextureAtlasPage[] { return this._pages; } readonly pageSize: number; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts index fbaa96afa0e..a721307beea 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts @@ -16,7 +16,9 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; export type AllocatorType = 'shelf' | 'slab' | ((canvas: OffscreenCanvas, textureIndex: number) => ITextureAtlasAllocator); export class TextureAtlasPage extends Disposable implements IReadableTextureAtlasPage { + private _version: number = 0; + get version(): number { return this._version; } /** * The maximum number of glyphs that can be drawn to the page. This is currently a hard static @@ -25,21 +27,10 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla static readonly maximumGlyphCount = 5_000; private _usedArea: IBoundingBox = { left: 0, top: 0, right: 0, bottom: 0 }; - public get usedArea(): Readonly { - return this._usedArea; - } - - /** - * The version of the texture atlas. This is incremented every time the page's texture changes. - */ - get version(): number { - return this._version; - } + public get usedArea(): Readonly { return this._usedArea; } private readonly _canvas: OffscreenCanvas; - get source(): OffscreenCanvas { - return this._canvas; - } + get source(): OffscreenCanvas { return this._canvas; } private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); private readonly _glyphInOrderSet: Set = new Set(); diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts index 48bc2a2282d..a06c13739d0 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts @@ -27,7 +27,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private readonly _ctx: OffscreenCanvasRenderingContext2D; - // TODO: Is there a better way to index slabs other than an unsorted list? private _slabs: ITextureAtlasSlab[] = []; private _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts index 23f4da25469..d2e54e81fb8 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts @@ -12,9 +12,6 @@ import { FontStyle, TokenMetadata } from 'vs/editor/common/encodedTokenAttribute let nextId = 0; export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { - /** - * A unique identifier for this rasterizer. - */ public readonly id = nextId++; private _canvas: OffscreenCanvas; @@ -33,11 +30,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { y: 0, } }; - - private _workGlyphConfig: { - chars: string | undefined; - metadata: number; - } = { chars: undefined, metadata: 0 }; + private _workGlyphConfig: { chars: string | undefined; metadata: number } = { chars: undefined, metadata: 0 }; constructor( private readonly _fontSize: number, diff --git a/src/vs/editor/browser/view/gpu/raster/raster.ts b/src/vs/editor/browser/view/gpu/raster/raster.ts index b204596312d..606e84fe512 100644 --- a/src/vs/editor/browser/view/gpu/raster/raster.ts +++ b/src/vs/editor/browser/view/gpu/raster/raster.ts @@ -7,7 +7,7 @@ import type { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; export interface IGlyphRasterizer { /** - * A unique identifier for the rasterizer + * A unique identifier for the rasterizer. */ id: number; From 930d0c2ed6fa0480b1c985f713940ebde5e7c7ba Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:32:41 -0700 Subject: [PATCH 123/286] Split render strategy into own file --- .../view/gpu/fullFileRenderStrategy.ts | 365 +++++++++++++++++ src/vs/editor/browser/view/gpu/gpu.ts | 32 ++ .../editor/browser/view/gpu/gpuViewLayer.ts | 385 +----------------- 3 files changed, 399 insertions(+), 383 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts create mode 100644 src/vs/editor/browser/view/gpu/gpu.ts diff --git a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts new file mode 100644 index 00000000000..a27a8147666 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts @@ -0,0 +1,365 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; +import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { BindingId, type IRendererContext, type IRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; +import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; +import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { IVisibleLine } from 'vs/editor/browser/view/viewLayer'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; +import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; +import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; + +const enum Constants { + IndicesPerCell = 6, +} + +const fullFileRenderStrategyWgsl = /*wgsl*/` +struct GlyphInfo { + position: vec2f, + size: vec2f, + origin: vec2f, +}; + +struct Vertex { + @location(0) position: vec2f, +}; + +struct Cells { + position: vec2f, + unused1: vec2f, + glyphIndex: f32, + textureIndex: f32 +}; + +struct ScrollOffset { + offset: vec2f +} + +struct VSOutput { + @builtin(position) position: vec4f, + @location(1) layerIndex: f32, + @location(0) texcoord: vec2f, +}; + +// Uniforms +@group(0) @binding(${BindingId.CanvasDimensionsUniform}) var canvasDims: vec2f; +@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: vec2f; +@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; + +// Storage buffers +@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; +@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; +@group(0) @binding(${BindingId.Cells}) var cells: array; + +@vertex fn vs( + vert: Vertex, + @builtin(instance_index) instanceIndex: u32, + @builtin(vertex_index) vertexIndex : u32 +) -> VSOutput { + let cell = cells[instanceIndex]; + // TODO: Is there a nicer way to init this? + var glyph = glyphInfo0[0]; + let glyphIndex = u32(cell.glyphIndex); + if (u32(cell.textureIndex) == 0) { + glyph = glyphInfo0[glyphIndex]; + } else { + glyph = glyphInfo1[glyphIndex]; + } + + var vsOut: VSOutput; + // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 + vsOut.position = vec4f( + (((vert.position * vec2f(2, -2)) / canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / canvasDims) + ((scrollOffset.offset * 2) / canvasDims), + 0.0, + 1.0 + ); + + vsOut.layerIndex = cell.textureIndex; + // Textures are flipped from natural direction on the y-axis, so flip it back + vsOut.texcoord = vert.position; + vsOut.texcoord = ( + // Glyph offset (0-1) + (glyph.position / atlasDims) + + // Glyph coordinate (0-1) + (vsOut.texcoord * (glyph.size / atlasDims)) + ); + + return vsOut; +} + +@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d_array; + +@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { + return textureSample(ourTexture, ourSampler, vsOut.texcoord, u32(vsOut.layerIndex)); +} +`; + +export class FullFileRenderStrategy extends Disposable implements IRenderStrategy { + + private static _lineCount = 3000; + private static _columnCount = 200; + + readonly wgsl: string = fullFileRenderStrategyWgsl; + + private readonly _glyphRasterizer: GlyphRasterizer; + + private _cellBindBuffer!: GPUBuffer; + private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; + private _activeDoubleBufferIndex: 0 | 1 = 0; + + private readonly _upToDateLines: [Set, Set] = [new Set(), new Set()]; + private _visibleObjectCount: number = 0; + + private _scrollOffsetBindBuffer!: GPUBuffer; + private _scrollOffsetValueBuffers!: [Float32Array, Float32Array]; + + get bindGroupEntries(): GPUBindGroupEntry[] { + return [ + { binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } }, + { binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } } + ]; + } + + constructor( + private readonly _context: ViewContext, + private readonly _device: GPUDevice, + private readonly _canvas: HTMLCanvasElement, + private readonly _viewportData: ViewportData, + private readonly _atlas: TextureAtlas, + ) { + super(); + + // TODO: Detect when lines have been tokenized and clear _upToDateLines + const activeWindow = getActiveWindow(); + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = Math.ceil(this._context.configuration.options.get(EditorOption.fontSize) * activeWindow.devicePixelRatio); + + // TODO: Register this + this._glyphRasterizer = new GlyphRasterizer(fontSize, fontFamily); + } + + initBuffers(): void { + const bufferSize = FullFileRenderStrategy._lineCount * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco full file cell buffer', + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._cellValueBuffers = [ + new ArrayBuffer(bufferSize), + new ArrayBuffer(bufferSize), + ]; + + const scrollOffsetBufferSize = 2; + this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco scroll offset buffer', + size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + })).object; + this._scrollOffsetValueBuffers = [ + new Float32Array(scrollOffsetBufferSize), + new Float32Array(scrollOffsetBufferSize), + ]; + } + + update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { + // Pre-allocate variables to be shared within the loop - don't trust the JIT compiler to do + // this optimization to avoid additional blocking time in garbage collector + let chars = ''; + let y = 0; + let x = 0; + let screenAbsoluteX = 0; + let screenAbsoluteY = 0; + let zeroToOneX = 0; + let zeroToOneY = 0; + let wgslX = 0; + let wgslY = 0; + let xOffset = 0; + let glyph: Readonly; + let cellIndex = 0; + + let tokenStartIndex = 0; + let tokenEndIndex = 0; + let tokenMetadata = 0; + + let lineData: ViewLineRenderingData; + let content: string = ''; + let fillStartIndex = 0; + let fillEndIndex = 0; + + let tokens: IViewLineTokens; + + const activeWindow = getActiveWindow(); + + // Update scroll offset + // TODO: Get at ViewModel in a safe way + const scrollTop = (this._viewportData as any)._model.viewLayout.getCurrentScrollTop() * activeWindow.devicePixelRatio; + const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex]; + scrollOffsetBuffer[1] = scrollTop; + this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, scrollOffsetBuffer); + + // Update cell data + const viewportData = this._viewportData; + const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); + const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; + + const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex]; + let dirtyLineStart = Number.MAX_SAFE_INTEGER; + let dirtyLineEnd = 0; + + // const theme = this._themeService.getColorTheme() as ColorThemeData; + // const tokenStyle = theme.getTokenStyleMetadata(type, modifiers, defaultLanguage, true, definitions); + + for (y = startLineNumber; y <= stopLineNumber; y++) { + // TODO: Update on dirty lines; is this known by line before rendering? + // if (upToDateLines.has(y)) { + // continue; + // } + dirtyLineStart = Math.min(dirtyLineStart, y); + dirtyLineEnd = Math.max(dirtyLineEnd, y); + + lineData = viewportData.getViewLineRenderingData(y); + content = lineData.content; + xOffset = 0; + + // TODO: Handle colors via viewLineRenderingData.tokens + // console.log(lineData.tokens); + // console.log('fg'); + // for (let i = 0; i < lineData.tokens.getCount(); i++) { + // console.log(` ${lineData.tokens.getForeground(i)}`); + // } + + // See ViewLine#renderLine + // const renderLineInput = new RenderLineInput( + // options.useMonospaceOptimizations, + // options.canUseHalfwidthRightwardsArrow, + // lineData.content, + // lineData.continuesWithWrappedLine, + // lineData.isBasicASCII, + // lineData.containsRTL, + // lineData.minColumn - 1, + // lineData.tokens, + // actualInlineDecorations, + // lineData.tabSize, + // lineData.startVisibleColumn, + // options.spaceWidth, + // options.middotWidth, + // options.wsmiddotWidth, + // options.stopRenderingLineAfter, + // options.renderWhitespace, + // options.renderControlCharacters, + // options.fontLigatures !== EditorFontLigatures.OFF, + // selectionsOnLine + // ); + + tokens = lineData.tokens; + tokenStartIndex = lineData.minColumn - 1; + tokenEndIndex = 0; + for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { + tokenEndIndex = tokens.getEndOffset(tokenIndex); + if (tokenEndIndex <= tokenStartIndex) { + // The faux indent part of the line should have no token type + continue; + } + + + tokenMetadata = tokens.getMetadata(tokenIndex); + + // console.log(`token: start=${tokenStartIndex}, end=${tokenEndIndex}, fg=${colorMap[tokenFg]}`); + + + for (x = tokenStartIndex; x < tokenEndIndex; x++) { + // HACK: Prevent rendering past the end of the render buffer + // TODO: This needs to move to a dynamic long line rendering strategy + if (x > FullFileRenderStrategy._columnCount) { + break; + } + chars = content.charAt(x); + if (chars === ' ') { + continue; + } + if (chars === '\t') { + // TODO: Pull actual tab size + xOffset += 3; + continue; + } + + glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); + + screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); + screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); + zeroToOneX = screenAbsoluteX / this._canvas.width; + zeroToOneY = screenAbsoluteY / this._canvas.height; + wgslX = zeroToOneX * 2 - 1; + wgslY = zeroToOneY * 2 - 1; + + cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x + xOffset)) * Constants.IndicesPerCell; + cellBuffer[cellIndex + 0] = wgslX; // x + cellBuffer[cellIndex + 1] = -wgslY; // y + cellBuffer[cellIndex + 2] = 0; + cellBuffer[cellIndex + 3] = 0; + cellBuffer[cellIndex + 4] = glyph.glyphIndex; // glyphIndex + cellBuffer[cellIndex + 5] = glyph.pageIndex; // textureIndex + } + + tokenStartIndex = tokenEndIndex; + } + + // Clear to end of line + fillStartIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (tokenEndIndex + xOffset)) * Constants.IndicesPerCell; + fillEndIndex = (y * FullFileRenderStrategy._columnCount) * Constants.IndicesPerCell; + cellBuffer.fill(0, fillStartIndex, fillEndIndex); + + upToDateLines.add(y); + } + + const visibleObjectCount = (stopLineNumber - startLineNumber + 1) * lineIndexCount; + + // Only write when there is changed data + if (dirtyLineStart <= dirtyLineEnd) { + // Write buffer and swap it out to unblock writes + this._device.queue.writeBuffer( + this._cellBindBuffer, + (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + // TODO: this cell buffer actually only needs to be the size of the viewport if we are only uploading a range + // at the maximum each frame + cellBuffer.buffer, + (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + (dirtyLineEnd - dirtyLineStart + 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT + ); + } + // HACK: Replace entire buffer for testing purposes + // this._device.queue.writeBuffer( + // this._cellBindBuffer, + // 0, + // cellBuffer + // ); + + this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; + + this._visibleObjectCount = visibleObjectCount; + return visibleObjectCount; + } + + draw(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void { + if (this._visibleObjectCount <= 0) { + console.error('Attempt to draw 0 objects'); + } else { + pass.draw( + 6, // square verticies + this._visibleObjectCount, + undefined, + (startLineNumber - 1) * FullFileRenderStrategy._columnCount + ); + } + } +} diff --git a/src/vs/editor/browser/view/gpu/gpu.ts b/src/vs/editor/browser/view/gpu/gpu.ts new file mode 100644 index 00000000000..07b74530cc5 --- /dev/null +++ b/src/vs/editor/browser/view/gpu/gpu.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { IVisibleLine } from 'vs/editor/browser/view/viewLayer'; + +export const enum BindingId { + GlyphInfo0, + GlyphInfo1, + Cells, + TextureSampler, + Texture, + CanvasDimensionsUniform, + AtlasDimensionsUniform, + ScrollOffset, +} + +export interface IRendererContext { + rendLineNumberStart: number; + lines: T[]; + linesLength: number; +} + +export interface IRenderStrategy { + readonly wgsl: string; + readonly bindGroupEntries: GPUBindGroupEntry[]; + + initBuffers(): void; + update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number; + draw?(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void; +} diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index d342dee9977..0a46485efc5 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -5,33 +5,20 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; -import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; +import { FullFileRenderStrategy } from 'vs/editor/browser/view/gpu/fullFileRenderStrategy'; +import { BindingId, type IRendererContext, type IRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; import { ensureNonNullable, observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IVisibleLine, IVisibleLinesHost } from 'vs/editor/browser/view/viewLayer'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; export const disableNonGpuRendering = true; -interface IRendererContext { - rendLineNumberStart: number; - lines: T[]; - linesLength: number; -} - -const enum Constants { - IndicesPerCell = 6, -} - const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, BytesPerEntry = GlyphStorageBufferInfo.FloatsPerEntry * 4, @@ -40,17 +27,6 @@ const enum GlyphStorageBufferInfo { Offset_OriginPosition = 4, } -const enum BindingId { - GlyphInfo0, - GlyphInfo1, - Cells, - TextureSampler, - Texture, - CanvasDimensionsUniform, - AtlasDimensionsUniform, - ScrollOffset, -} - export class GpuViewLayerRenderer extends Disposable { readonly domNode: HTMLCanvasElement; @@ -374,360 +350,3 @@ export class GpuViewLayerRenderer extends Disposable { return ctx; } } - - -interface IRenderStrategy { - readonly wgsl: string; - readonly bindGroupEntries: GPUBindGroupEntry[]; - - initBuffers(): void; - update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number; - draw?(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void; -} - -// #region Full file render strategy - -const fullFileRenderStrategyWgsl = /*wgsl*/` -struct GlyphInfo { - position: vec2f, - size: vec2f, - origin: vec2f, -}; - -struct Vertex { - @location(0) position: vec2f, -}; - -struct Cells { - position: vec2f, - unused1: vec2f, - glyphIndex: f32, - textureIndex: f32 -}; - -struct ScrollOffset { - offset: vec2f -} - -struct VSOutput { - @builtin(position) position: vec4f, - @location(1) layerIndex: f32, - @location(0) texcoord: vec2f, -}; - -// Uniforms -@group(0) @binding(${BindingId.CanvasDimensionsUniform}) var canvasDims: vec2f; -@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: vec2f; -@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; - -// Storage buffers -@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; -@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; -@group(0) @binding(${BindingId.Cells}) var cells: array; - -@vertex fn vs( - vert: Vertex, - @builtin(instance_index) instanceIndex: u32, - @builtin(vertex_index) vertexIndex : u32 -) -> VSOutput { - let cell = cells[instanceIndex]; - // TODO: Is there a nicer way to init this? - var glyph = glyphInfo0[0]; - let glyphIndex = u32(cell.glyphIndex); - if (u32(cell.textureIndex) == 0) { - glyph = glyphInfo0[glyphIndex]; - } else { - glyph = glyphInfo1[glyphIndex]; - } - - var vsOut: VSOutput; - // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 - vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / canvasDims) + ((scrollOffset.offset * 2) / canvasDims), - 0.0, - 1.0 - ); - - vsOut.layerIndex = cell.textureIndex; - // Textures are flipped from natural direction on the y-axis, so flip it back - vsOut.texcoord = vert.position; - vsOut.texcoord = ( - // Glyph offset (0-1) - (glyph.position / atlasDims) + - // Glyph coordinate (0-1) - (vsOut.texcoord * (glyph.size / atlasDims)) - ); - - return vsOut; -} - -@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; -@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d_array; - -@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { - return textureSample(ourTexture, ourSampler, vsOut.texcoord, u32(vsOut.layerIndex)); -} -`; - -class FullFileRenderStrategy extends Disposable implements IRenderStrategy { - - private static _lineCount = 3000; - private static _columnCount = 200; - - readonly wgsl: string = fullFileRenderStrategyWgsl; - - private readonly _glyphRasterizer: GlyphRasterizer; - - private _cellBindBuffer!: GPUBuffer; - private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; - private _activeDoubleBufferIndex: 0 | 1 = 0; - - private readonly _upToDateLines: [Set, Set] = [new Set(), new Set()]; - private _visibleObjectCount: number = 0; - - private _scrollOffsetBindBuffer!: GPUBuffer; - private _scrollOffsetValueBuffers!: [Float32Array, Float32Array]; - - get bindGroupEntries(): GPUBindGroupEntry[] { - return [ - { binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } }, - { binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } } - ]; - } - - constructor( - private readonly _context: ViewContext, - private readonly _device: GPUDevice, - private readonly _canvas: HTMLCanvasElement, - private readonly _viewportData: ViewportData, - private readonly _atlas: TextureAtlas, - ) { - super(); - - // TODO: Detect when lines have been tokenized and clear _upToDateLines - const activeWindow = getActiveWindow(); - const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); - const fontSize = Math.ceil(this._context.configuration.options.get(EditorOption.fontSize) * activeWindow.devicePixelRatio); - - // TODO: Register this - this._glyphRasterizer = new GlyphRasterizer(fontSize, fontFamily); - } - - initBuffers(): void { - const bufferSize = FullFileRenderStrategy._lineCount * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; - this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco full file cell buffer', - size: bufferSize, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).object; - this._cellValueBuffers = [ - new ArrayBuffer(bufferSize), - new ArrayBuffer(bufferSize), - ]; - - const scrollOffsetBufferSize = 2; - this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco scroll offset buffer', - size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - })).object; - this._scrollOffsetValueBuffers = [ - new Float32Array(scrollOffsetBufferSize), - new Float32Array(scrollOffsetBufferSize), - ]; - } - - update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { - // Pre-allocate variables to be shared within the loop - don't trust the JIT compiler to do - // this optimization to avoid additional blocking time in garbage collector - let chars = ''; - let y = 0; - let x = 0; - let screenAbsoluteX = 0; - let screenAbsoluteY = 0; - let zeroToOneX = 0; - let zeroToOneY = 0; - let wgslX = 0; - let wgslY = 0; - let xOffset = 0; - let glyph: Readonly; - let cellIndex = 0; - - let tokenStartIndex = 0; - let tokenEndIndex = 0; - let tokenMetadata = 0; - - let lineData: ViewLineRenderingData; - let content: string = ''; - let fillStartIndex = 0; - let fillEndIndex = 0; - - let tokens: IViewLineTokens; - - const activeWindow = getActiveWindow(); - - // Update scroll offset - // TODO: Get at ViewModel in a safe way - const scrollTop = (this._viewportData as any)._model.viewLayout.getCurrentScrollTop() * activeWindow.devicePixelRatio; - const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex]; - scrollOffsetBuffer[1] = scrollTop; - this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, scrollOffsetBuffer); - - // Update cell data - const viewportData = this._viewportData; - const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); - const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; - - const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex]; - let dirtyLineStart = Number.MAX_SAFE_INTEGER; - let dirtyLineEnd = 0; - - // const theme = this._themeService.getColorTheme() as ColorThemeData; - // const tokenStyle = theme.getTokenStyleMetadata(type, modifiers, defaultLanguage, true, definitions); - - for (y = startLineNumber; y <= stopLineNumber; y++) { - // TODO: Update on dirty lines; is this known by line before rendering? - // if (upToDateLines.has(y)) { - // continue; - // } - dirtyLineStart = Math.min(dirtyLineStart, y); - dirtyLineEnd = Math.max(dirtyLineEnd, y); - - lineData = viewportData.getViewLineRenderingData(y); - content = lineData.content; - xOffset = 0; - - // TODO: Handle colors via viewLineRenderingData.tokens - // console.log(lineData.tokens); - // console.log('fg'); - // for (let i = 0; i < lineData.tokens.getCount(); i++) { - // console.log(` ${lineData.tokens.getForeground(i)}`); - // } - - // See ViewLine#renderLine - // const renderLineInput = new RenderLineInput( - // options.useMonospaceOptimizations, - // options.canUseHalfwidthRightwardsArrow, - // lineData.content, - // lineData.continuesWithWrappedLine, - // lineData.isBasicASCII, - // lineData.containsRTL, - // lineData.minColumn - 1, - // lineData.tokens, - // actualInlineDecorations, - // lineData.tabSize, - // lineData.startVisibleColumn, - // options.spaceWidth, - // options.middotWidth, - // options.wsmiddotWidth, - // options.stopRenderingLineAfter, - // options.renderWhitespace, - // options.renderControlCharacters, - // options.fontLigatures !== EditorFontLigatures.OFF, - // selectionsOnLine - // ); - - tokens = lineData.tokens; - tokenStartIndex = lineData.minColumn - 1; - tokenEndIndex = 0; - for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { - tokenEndIndex = tokens.getEndOffset(tokenIndex); - if (tokenEndIndex <= tokenStartIndex) { - // The faux indent part of the line should have no token type - continue; - } - - - tokenMetadata = tokens.getMetadata(tokenIndex); - - // console.log(`token: start=${tokenStartIndex}, end=${tokenEndIndex}, fg=${colorMap[tokenFg]}`); - - - for (x = tokenStartIndex; x < tokenEndIndex; x++) { - // HACK: Prevent rendering past the end of the render buffer - // TODO: This needs to move to a dynamic long line rendering strategy - if (x > FullFileRenderStrategy._columnCount) { - break; - } - chars = content.charAt(x); - if (chars === ' ') { - continue; - } - if (chars === '\t') { - // TODO: Pull actual tab size - xOffset += 3; - continue; - } - - glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); - - screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); - screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); - zeroToOneX = screenAbsoluteX / this._canvas.width; - zeroToOneY = screenAbsoluteY / this._canvas.height; - wgslX = zeroToOneX * 2 - 1; - wgslY = zeroToOneY * 2 - 1; - - cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x + xOffset)) * Constants.IndicesPerCell; - cellBuffer[cellIndex + 0] = wgslX; // x - cellBuffer[cellIndex + 1] = -wgslY; // y - cellBuffer[cellIndex + 2] = 0; - cellBuffer[cellIndex + 3] = 0; - cellBuffer[cellIndex + 4] = glyph.glyphIndex; // glyphIndex - cellBuffer[cellIndex + 5] = glyph.pageIndex; // textureIndex - } - - tokenStartIndex = tokenEndIndex; - } - - // Clear to end of line - fillStartIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (tokenEndIndex + xOffset)) * Constants.IndicesPerCell; - fillEndIndex = (y * FullFileRenderStrategy._columnCount) * Constants.IndicesPerCell; - cellBuffer.fill(0, fillStartIndex, fillEndIndex); - - upToDateLines.add(y); - } - - const visibleObjectCount = (stopLineNumber - startLineNumber + 1) * lineIndexCount; - - // Only write when there is changed data - if (dirtyLineStart <= dirtyLineEnd) { - // Write buffer and swap it out to unblock writes - this._device.queue.writeBuffer( - this._cellBindBuffer, - (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, - // TODO: this cell buffer actually only needs to be the size of the viewport if we are only uploading a range - // at the maximum each frame - cellBuffer.buffer, - (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, - (dirtyLineEnd - dirtyLineStart + 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT - ); - } - // HACK: Replace entire buffer for testing purposes - // this._device.queue.writeBuffer( - // this._cellBindBuffer, - // 0, - // cellBuffer - // ); - - this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; - - this._visibleObjectCount = visibleObjectCount; - return visibleObjectCount; - } - - draw(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void { - if (this._visibleObjectCount <= 0) { - console.error('Attempt to draw 0 objects'); - } else { - pass.draw( - 6, // square verticies - this._visibleObjectCount, - undefined, - (startLineNumber - 1) * FullFileRenderStrategy._columnCount - ); - } - } -} - -// #endregion Full file render strategy From 9f91810bb6072b9e99b62e69148807ec8245a336 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:44:18 -0700 Subject: [PATCH 124/286] Comments --- .../editor/browser/view/gpu/fullFileRenderStrategy.ts | 11 ++++++++--- src/vs/editor/browser/view/gpu/gpuViewLayer.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts index a27a8147666..97df198f404 100644 --- a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts @@ -9,6 +9,7 @@ import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/at import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; import { BindingId, type IRendererContext, type IRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; +import { quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; import type { IVisibleLine } from 'vs/editor/browser/view/viewLayer'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -113,6 +114,11 @@ export class FullFileRenderStrategy extends Disposable i private readonly _glyphRasterizer: GlyphRasterizer; private _cellBindBuffer!: GPUBuffer; + + /** + * The cell value buffers, these hold the cells and their glyphs. It's double buffers such that + * the thread doesn't block when one is being uploaded to the GPU. + */ private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; private _activeDoubleBufferIndex: 0 | 1 = 0; @@ -143,8 +149,7 @@ export class FullFileRenderStrategy extends Disposable i const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); const fontSize = Math.ceil(this._context.configuration.options.get(EditorOption.fontSize) * activeWindow.devicePixelRatio); - // TODO: Register this - this._glyphRasterizer = new GlyphRasterizer(fontSize, fontFamily); + this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily)); } initBuffers(): void { @@ -355,7 +360,7 @@ export class FullFileRenderStrategy extends Disposable i console.error('Attempt to draw 0 objects'); } else { pass.draw( - 6, // square verticies + quadVertices.length / 2, this._visibleObjectCount, undefined, (startLineNumber - 1) * FullFileRenderStrategy._columnCount diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 0a46485efc5..84a6586868d 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -272,6 +272,7 @@ export class GpuViewLayerRenderer extends Disposable { this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version); + // TODO: Reuse buffer instead of reconstructing each time // TODO: Dynamically set buffer size const values = new Float32Array(GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount); let entryOffset = 0; @@ -333,7 +334,6 @@ export class GpuViewLayerRenderer extends Disposable { pass.setVertexBuffer(0, this._vertexBuffer); pass.setBindGroup(0, this._bindGroup); - // TODO: Draws could be split by chunk, this would help minimize moving data around in arrays if (this._renderStrategy?.draw) { this._renderStrategy.draw(pass, ctx, startLineNumber, stopLineNumber, deltaTop); From 479ee1059901374c2e80f8b8ce470329f941a722 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:55:04 -0700 Subject: [PATCH 125/286] Simplify render strategy interface --- .../view/gpu/fullFileRenderStrategy.ts | 27 ++++++++++++------- src/vs/editor/browser/view/gpu/gpu.ts | 1 - .../editor/browser/view/gpu/gpuViewLayer.ts | 4 --- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts index 97df198f404..6596ce9d8a9 100644 --- a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts @@ -33,7 +33,7 @@ struct Vertex { @location(0) position: vec2f, }; -struct Cells { +struct Cell { position: vec2f, unused1: vec2f, glyphIndex: f32, @@ -58,7 +58,7 @@ struct VSOutput { // Storage buffers @group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; @group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; -@group(0) @binding(${BindingId.Cells}) var cells: array; +@group(0) @binding(${BindingId.Cells}) var cells: array; @vertex fn vs( vert: Vertex, @@ -104,6 +104,17 @@ struct VSOutput { } `; +const enum CellBufferInfo { + FloatsPerEntry = 6, + BytesPerEntry = CellBufferInfo.FloatsPerEntry * 4, + Offset_X = 0, + Offset_Y = 1, + Offset_Unused1 = 2, + Offset_Unused2 = 3, + GlyphIndex = 4, + TextureIndex = 5, +} + export class FullFileRenderStrategy extends Disposable implements IRenderStrategy { private static _lineCount = 3000; @@ -150,9 +161,7 @@ export class FullFileRenderStrategy extends Disposable i const fontSize = Math.ceil(this._context.configuration.options.get(EditorOption.fontSize) * activeWindow.devicePixelRatio); this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily)); - } - initBuffers(): void { const bufferSize = FullFileRenderStrategy._lineCount * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco full file cell buffer', @@ -308,12 +317,10 @@ export class FullFileRenderStrategy extends Disposable i wgslY = zeroToOneY * 2 - 1; cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x + xOffset)) * Constants.IndicesPerCell; - cellBuffer[cellIndex + 0] = wgslX; // x - cellBuffer[cellIndex + 1] = -wgslY; // y - cellBuffer[cellIndex + 2] = 0; - cellBuffer[cellIndex + 3] = 0; - cellBuffer[cellIndex + 4] = glyph.glyphIndex; // glyphIndex - cellBuffer[cellIndex + 5] = glyph.pageIndex; // textureIndex + cellBuffer[cellIndex + CellBufferInfo.Offset_X] = wgslX; + cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = -wgslY; + cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; + cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex; } tokenStartIndex = tokenEndIndex; diff --git a/src/vs/editor/browser/view/gpu/gpu.ts b/src/vs/editor/browser/view/gpu/gpu.ts index 07b74530cc5..a1333582ede 100644 --- a/src/vs/editor/browser/view/gpu/gpu.ts +++ b/src/vs/editor/browser/view/gpu/gpu.ts @@ -26,7 +26,6 @@ export interface IRenderStrategy { readonly wgsl: string; readonly bindGroupEntries: GPUBindGroupEntry[]; - initBuffers(): void; update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number; draw?(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void; } diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts index 84a6586868d..b99cae70d96 100644 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts @@ -211,10 +211,6 @@ export class GpuViewLayerRenderer extends Disposable { - this._renderStrategy.initBuffers(); - - - this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco vertex buffer', size: quadVertices.byteLength, From 2e48ffe8c59b190c00254fe580653d91b5753d66 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Thu, 22 Aug 2024 15:42:09 +0200 Subject: [PATCH 126/286] Setting up the viewLinesGpu --- src/vs/editor/browser/view.ts | 15 +++- .../browser/viewParts/gpu/viewLinesGpu.ts | 65 ++++++++++++++++++ .../browser/viewParts/lines/viewLine.ts | 68 +++---------------- .../viewParts/lines/viewLineOptions.ts | 62 +++++++++++++++++ .../browser/viewParts/lines/viewLines.ts | 3 +- .../browser/widget/codeEditor/editor.css | 6 ++ 6 files changed, 159 insertions(+), 60 deletions(-) create mode 100644 src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts create mode 100644 src/vs/editor/browser/viewParts/lines/viewLineOptions.ts diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 6ff5ac82b97..62e493e2ea1 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -26,6 +26,7 @@ import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from ' import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations'; import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar'; import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides'; import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers'; import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines'; @@ -57,7 +58,6 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; - export interface IContentWidgetData { widget: IContentWidget; position: IContentWidgetPosition | null; @@ -81,6 +81,7 @@ export class View extends ViewEventHandler { // The view lines private readonly _viewLines: ViewLines; + private readonly _viewLinesGpu: ViewLinesGpu; // These are parts, but we must do some API related calls on them, so we keep a reference private readonly _viewZones: ViewZones; @@ -95,6 +96,7 @@ export class View extends ViewEventHandler { // Dom nodes private readonly _linesContent: FastDomNode; + private readonly _canvas: FastDomNode; public readonly domNode: FastDomNode; private readonly _overflowGuardContainer: FastDomNode; @@ -134,6 +136,9 @@ export class View extends ViewEventHandler { this._linesContent.setClassName('lines-content' + ' monaco-editor-background'); this._linesContent.setPosition('absolute'); + this._canvas = createFastDomNode(document.createElement('canvas')); + this._canvas.setClassName('editorCanvas'); + this.domNode = createFastDomNode(document.createElement('div')); this.domNode.setClassName(this._getEditorClassName()); // Set role 'code' for better screen reader support https://github.com/microsoft/vscode/issues/93438 @@ -148,6 +153,7 @@ export class View extends ViewEventHandler { // View Lines this._viewLines = new ViewLines(this._context, this._linesContent); + this._viewLinesGpu = new ViewLinesGpu(this._context, this._canvas.domNode); // View Zones this._viewZones = new ViewZones(this._context); @@ -221,6 +227,7 @@ export class View extends ViewEventHandler { this._linesContent.appendChild(this._viewCursors.getDomNode()); this._overflowGuardContainer.appendChild(margin.getDomNode()); this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); + this._overflowGuardContainer.appendChild(this._canvas); this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); this._overflowGuardContainer.appendChild(this._textAreaHandler.textArea); this._overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover); @@ -392,6 +399,7 @@ export class View extends ViewEventHandler { this._context.removeEventHandler(this); this._viewLines.dispose(); + this._viewLinesGpu.dispose(); // Destroy view parts for (const viewPart of this._viewParts) { @@ -505,6 +513,11 @@ export class View extends ViewEventHandler { viewPartsToRender = this._getViewPartsToRender(); } + if (this._viewLinesGpu.shouldRender()) { + this._viewLinesGpu.renderText(viewportData); + this._viewLinesGpu.onDidRender(); + } + return [viewPartsToRender, new RenderingContext(this._context.viewLayout, viewportData, this._viewLines)]; }, prepareRender: (viewPartsToRender: ViewPart[], ctx: RenderingContext) => { diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts new file mode 100644 index 00000000000..60aa9ce1281 --- /dev/null +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BugIndicatingError } from 'vs/base/common/errors'; +import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; +import { ViewPart } from 'vs/editor/browser/view/viewPart'; +import { ViewLineOptions } from '../lines/viewLineOptions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ViewLinesChangedEvent, ViewScrollChangedEvent } from 'vs/editor/common/viewEvents'; +import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; +import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; + +export class ViewLinesGpu extends ViewPart { + constructor(context: ViewContext, private readonly canvas: HTMLCanvasElement) { + super(context); + } + + public static canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { + const d = viewportData.getViewLineRenderingData(lineNumber); + // TODO + return d.content.indexOf('e') !== -1; + } + + public override prepareRender(ctx: RenderingContext): void { + throw new BugIndicatingError('Should not be called'); + } + + public override render(ctx: RestrictedRenderingContext): void { + throw new BugIndicatingError('Should not be called'); + } + + override onLinesChanged(e: ViewLinesChangedEvent): boolean { + return true; + } + + override onScrollChanged(e: ViewScrollChangedEvent): boolean { + return true; + } + + // subscribe to more events + + public renderText(viewportData: ViewportData): void { + const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); + + const ctx = this.canvas.getContext('2d')!; + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + ctx.strokeStyle = 'black'; + const vp = this._context.viewLayout.getCurrentViewport(); + const left = this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft; + this.canvas.width = vp.width; + this.canvas.height = vp.height; + ctx.font = `${this._context.configuration.options.get(EditorOption.fontSize)}px monospace`; + + for (let i = viewportData.startLineNumber; i <= viewportData.endLineNumber; i++) { + if (!ViewLinesGpu.canRender(options, viewportData, i)) { + continue; + } + const line = viewportData.getViewLineRenderingData(i); + + ctx.strokeText(line.content, left, viewportData.relativeVerticalOffset[i - viewportData.startLineNumber] + viewportData.lineHeight - this._context.viewLayout.getCurrentScrollTop()); + } + } +} diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 9a5d2f556bf..573464c3192 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -9,15 +9,16 @@ import * as platform from 'vs/base/common/platform'; import { IVisibleLine } from 'vs/editor/browser/view/viewLayer'; import { RangeUtil } from 'vs/editor/browser/viewParts/lines/rangeUtil'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; import { FloatHorizontalRange, VisibleRanges } from 'vs/editor/browser/view/renderingContext'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine, LineRange, DomPosition } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { InlineDecorationType } from 'vs/editor/common/viewModel'; -import { ColorScheme, isHighContrast } from 'vs/platform/theme/common/theme'; -import { EditorOption, EditorFontLigatures } from 'vs/editor/common/config/editorOptions'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; +import { EditorFontLigatures } from 'vs/editor/common/config/editorOptions'; import { DomReadingContext } from 'vs/editor/browser/viewParts/lines/domReadingContext'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; +import { ViewLineOptions } from './viewLineOptions'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { @@ -45,61 +46,6 @@ const canUseFastRenderedViewLine = (function () { let monospaceAssumptionsAreValid = true; -export class ViewLineOptions { - public readonly themeType: ColorScheme; - public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; - public readonly renderControlCharacters: boolean; - public readonly spaceWidth: number; - public readonly middotWidth: number; - public readonly wsmiddotWidth: number; - public readonly useMonospaceOptimizations: boolean; - public readonly canUseHalfwidthRightwardsArrow: boolean; - public readonly lineHeight: number; - public readonly stopRenderingLineAfter: number; - public readonly fontLigatures: string; - - constructor(config: IEditorConfiguration, themeType: ColorScheme) { - this.themeType = themeType; - const options = config.options; - const fontInfo = options.get(EditorOption.fontInfo); - const experimentalWhitespaceRendering = options.get(EditorOption.experimentalWhitespaceRendering); - if (experimentalWhitespaceRendering === 'off') { - this.renderWhitespace = options.get(EditorOption.renderWhitespace); - } else { - // whitespace is rendered in a different layer - this.renderWhitespace = 'none'; - } - this.renderControlCharacters = options.get(EditorOption.renderControlCharacters); - this.spaceWidth = fontInfo.spaceWidth; - this.middotWidth = fontInfo.middotWidth; - this.wsmiddotWidth = fontInfo.wsmiddotWidth; - this.useMonospaceOptimizations = ( - fontInfo.isMonospace - && !options.get(EditorOption.disableMonospaceOptimizations) - ); - this.canUseHalfwidthRightwardsArrow = fontInfo.canUseHalfwidthRightwardsArrow; - this.lineHeight = options.get(EditorOption.lineHeight); - this.stopRenderingLineAfter = options.get(EditorOption.stopRenderingLineAfter); - this.fontLigatures = options.get(EditorOption.fontLigatures); - } - - public equals(other: ViewLineOptions): boolean { - return ( - this.themeType === other.themeType - && this.renderWhitespace === other.renderWhitespace - && this.renderControlCharacters === other.renderControlCharacters - && this.spaceWidth === other.spaceWidth - && this.middotWidth === other.middotWidth - && this.wsmiddotWidth === other.wsmiddotWidth - && this.useMonospaceOptimizations === other.useMonospaceOptimizations - && this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow - && this.lineHeight === other.lineHeight - && this.stopRenderingLineAfter === other.stopRenderingLineAfter - && this.fontLigatures === other.fontLigatures - ); - } -} - export class ViewLine implements IVisibleLine { public static readonly CLASS_NAME = 'view-line'; @@ -152,6 +98,12 @@ export class ViewLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { + if (ViewLinesGpu.canRender(this._options, viewportData, lineNumber)) { + this._renderedViewLine?.domNode?.domNode.remove(); + this._renderedViewLine = null; + return false; + } + if (this._isMaybeInvalid === false) { // it appears that nothing relevant has changed return false; diff --git a/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts b/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts new file mode 100644 index 00000000000..76536607852 --- /dev/null +++ b/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; + +export class ViewLineOptions { + public readonly themeType: ColorScheme; + public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; + public readonly renderControlCharacters: boolean; + public readonly spaceWidth: number; + public readonly middotWidth: number; + public readonly wsmiddotWidth: number; + public readonly useMonospaceOptimizations: boolean; + public readonly canUseHalfwidthRightwardsArrow: boolean; + public readonly lineHeight: number; + public readonly stopRenderingLineAfter: number; + public readonly fontLigatures: string; + + constructor(config: IEditorConfiguration, themeType: ColorScheme) { + this.themeType = themeType; + const options = config.options; + const fontInfo = options.get(EditorOption.fontInfo); + const experimentalWhitespaceRendering = options.get(EditorOption.experimentalWhitespaceRendering); + if (experimentalWhitespaceRendering === 'off') { + this.renderWhitespace = options.get(EditorOption.renderWhitespace); + } else { + // whitespace is rendered in a different layer + this.renderWhitespace = 'none'; + } + this.renderControlCharacters = options.get(EditorOption.renderControlCharacters); + this.spaceWidth = fontInfo.spaceWidth; + this.middotWidth = fontInfo.middotWidth; + this.wsmiddotWidth = fontInfo.wsmiddotWidth; + this.useMonospaceOptimizations = ( + fontInfo.isMonospace + && !options.get(EditorOption.disableMonospaceOptimizations) + ); + this.canUseHalfwidthRightwardsArrow = fontInfo.canUseHalfwidthRightwardsArrow; + this.lineHeight = options.get(EditorOption.lineHeight); + this.stopRenderingLineAfter = options.get(EditorOption.stopRenderingLineAfter); + this.fontLigatures = options.get(EditorOption.fontLigatures); + } + + public equals(other: ViewLineOptions): boolean { + return ( + this.themeType === other.themeType + && this.renderWhitespace === other.renderWhitespace + && this.renderControlCharacters === other.renderControlCharacters + && this.spaceWidth === other.spaceWidth + && this.middotWidth === other.middotWidth + && this.wsmiddotWidth === other.wsmiddotWidth + && this.useMonospaceOptimizations === other.useMonospaceOptimizations + && this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow + && this.lineHeight === other.lineHeight + && this.stopRenderingLineAfter === other.stopRenderingLineAfter + && this.fontLigatures === other.fontLigatures + ); + } +} diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index c31297c81ec..3d6d6b7dac7 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -14,7 +14,8 @@ import { HorizontalPosition, HorizontalRange, IViewLines, LineVisibleRanges, Vis import { VisibleLinesCollection } from 'vs/editor/browser/view/viewLayer'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { DomReadingContext } from 'vs/editor/browser/viewParts/lines/domReadingContext'; -import { ViewLine, ViewLineOptions } from 'vs/editor/browser/viewParts/lines/viewLine'; +import { ViewLine } from 'vs/editor/browser/viewParts/lines/viewLine'; +import { ViewLineOptions } from './viewLineOptions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; diff --git a/src/vs/editor/browser/widget/codeEditor/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css index a6d82d5845f..59c86e0b8a9 100644 --- a/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/src/vs/editor/browser/widget/codeEditor/editor.css @@ -45,6 +45,12 @@ border-style: dotted; } +.monaco-editor .editorCanvas { + position: absolute; + z-index: 0; + pointer-events: none; +} + /* -------------------- Misc -------------------- */ .monaco-editor .overflow-guard { From d32b22bfc436f6e72f088f79d3d8e07633965ab2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 08:43:40 -0700 Subject: [PATCH 127/286] Make monaco compile aware of webgpu types --- src/tsconfig.monaco.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index bb4fd1ccc35..910f3d232d0 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -3,6 +3,7 @@ "compilerOptions": { "noEmit": true, "types": [ + "@webgpu/types", "trusted-types", "wicg-file-system-access" ], From 1c892a972db4c134fb2688ff3c867b087eef2be4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:32:01 -0700 Subject: [PATCH 128/286] Migrate gpu rendering to ViewLinesGpu --- src/vs/editor/browser/view.ts | 5 +- .../view/gpu/fullFileRenderStrategy.ts | 25 +- src/vs/editor/browser/view/gpu/gpu.ts | 14 +- .../editor/browser/view/gpu/gpuViewLayer.ts | 348 ----------------- src/vs/editor/browser/view/viewLayer.ts | 42 +-- src/vs/editor/browser/view/viewOverlays.ts | 6 +- .../browser/viewParts/gpu/viewLinesGpu.ts | 356 +++++++++++++++++- .../browser/viewParts/lines/viewLines.ts | 4 +- .../browser/widget/codeEditor/editor.css | 2 + .../editor/contrib/gpu/browser/gpuActions.ts | 12 +- .../browser/stickyScrollWidget.ts | 2 +- 11 files changed, 385 insertions(+), 431 deletions(-) delete mode 100644 src/vs/editor/browser/view/gpu/gpuViewLayer.ts diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 696e6104ed4..eff192b87c9 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -15,7 +15,6 @@ import { PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouse import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { IVisibleRangeProvider, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; import { IContentWidget, IContentWidgetPosition, IEditorAriaOptions, IGlyphMarginWidget, IGlyphMarginWidgetPosition, IMouseTarget, IOverlayWidget, IOverlayWidgetPosition, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; -import { disableNonGpuRendering } from 'vs/editor/browser/view/gpu/gpuViewLayer'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; @@ -27,7 +26,7 @@ import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from ' import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations'; import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar'; import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; -import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; +import { disableNonGpuRendering, ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides'; import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers'; import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines'; @@ -154,7 +153,7 @@ export class View extends ViewEventHandler { // View Lines this._viewLines = this._instantiationService.createInstance(ViewLines, this._context, this._linesContent); - this._viewLinesGpu = new ViewLinesGpu(this._context, this._canvas.domNode); + this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._canvas.domNode); // View Zones this._viewZones = new ViewZones(this._context); diff --git a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts index 6596ce9d8a9..66dfa179a11 100644 --- a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts @@ -7,11 +7,10 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; -import { BindingId, type IRendererContext, type IRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; +import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; import { quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; -import type { IVisibleLine } from 'vs/editor/browser/view/viewLayer'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -78,7 +77,8 @@ struct VSOutput { var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / canvasDims) + ((scrollOffset.offset * 2) / canvasDims), + // TODO: Fix hacky scroll offset which moves the text beside the line numbers + (((vert.position * vec2f(2, -2)) / canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / canvasDims) + (((scrollOffset.offset - vec2f(-110, 0)) * 2) / canvasDims), 0.0, 1.0 ); @@ -115,7 +115,7 @@ const enum CellBufferInfo { TextureIndex = 5, } -export class FullFileRenderStrategy extends Disposable implements IRenderStrategy { +export class FullFileRenderStrategy extends Disposable implements IGpuRenderStrategy { private static _lineCount = 3000; private static _columnCount = 200; @@ -150,7 +150,6 @@ export class FullFileRenderStrategy extends Disposable i private readonly _context: ViewContext, private readonly _device: GPUDevice, private readonly _canvas: HTMLCanvasElement, - private readonly _viewportData: ViewportData, private readonly _atlas: TextureAtlas, ) { super(); @@ -185,7 +184,7 @@ export class FullFileRenderStrategy extends Disposable i ]; } - update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number { + update(viewportData: ViewportData): number { // Pre-allocate variables to be shared within the loop - don't trust the JIT compiler to do // this optimization to avoid additional blocking time in garbage collector let chars = ''; @@ -215,14 +214,12 @@ export class FullFileRenderStrategy extends Disposable i const activeWindow = getActiveWindow(); // Update scroll offset - // TODO: Get at ViewModel in a safe way - const scrollTop = (this._viewportData as any)._model.viewLayout.getCurrentScrollTop() * activeWindow.devicePixelRatio; + const scrollTop = this._context.viewLayout.getCurrentScrollTop() * activeWindow.devicePixelRatio; const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex]; scrollOffsetBuffer[1] = scrollTop; this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, scrollOffsetBuffer); // Update cell data - const viewportData = this._viewportData; const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; @@ -233,7 +230,7 @@ export class FullFileRenderStrategy extends Disposable i // const theme = this._themeService.getColorTheme() as ColorThemeData; // const tokenStyle = theme.getTokenStyleMetadata(type, modifiers, defaultLanguage, true, definitions); - for (y = startLineNumber; y <= stopLineNumber; y++) { + for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // TODO: Update on dirty lines; is this known by line before rendering? // if (upToDateLines.has(y)) { // continue; @@ -310,7 +307,7 @@ export class FullFileRenderStrategy extends Disposable i glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); - screenAbsoluteY = Math.round(deltaTop[y - startLineNumber] * activeWindow.devicePixelRatio); + screenAbsoluteY = Math.round(viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * activeWindow.devicePixelRatio); zeroToOneX = screenAbsoluteX / this._canvas.width; zeroToOneY = screenAbsoluteY / this._canvas.height; wgslX = zeroToOneX * 2 - 1; @@ -334,7 +331,7 @@ export class FullFileRenderStrategy extends Disposable i upToDateLines.add(y); } - const visibleObjectCount = (stopLineNumber - startLineNumber + 1) * lineIndexCount; + const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount; // Only write when there is changed data if (dirtyLineStart <= dirtyLineEnd) { @@ -362,7 +359,7 @@ export class FullFileRenderStrategy extends Disposable i return visibleObjectCount; } - draw(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void { + draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { if (this._visibleObjectCount <= 0) { console.error('Attempt to draw 0 objects'); } else { @@ -370,7 +367,7 @@ export class FullFileRenderStrategy extends Disposable i quadVertices.length / 2, this._visibleObjectCount, undefined, - (startLineNumber - 1) * FullFileRenderStrategy._columnCount + (viewportData.startLineNumber - 1) * FullFileRenderStrategy._columnCount ); } } diff --git a/src/vs/editor/browser/view/gpu/gpu.ts b/src/vs/editor/browser/view/gpu/gpu.ts index a1333582ede..231d8c526d4 100644 --- a/src/vs/editor/browser/view/gpu/gpu.ts +++ b/src/vs/editor/browser/view/gpu/gpu.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IVisibleLine } from 'vs/editor/browser/view/viewLayer'; +import type { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; export const enum BindingId { GlyphInfo0, @@ -16,16 +16,10 @@ export const enum BindingId { ScrollOffset, } -export interface IRendererContext { - rendLineNumberStart: number; - lines: T[]; - linesLength: number; -} - -export interface IRenderStrategy { +export interface IGpuRenderStrategy { readonly wgsl: string; readonly bindGroupEntries: GPUBindGroupEntry[]; - update(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): number; - draw?(pass: GPURenderPassEncoder, ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): void; + update(viewportData: ViewportData): number; + draw?(pass: GPURenderPassEncoder, viewportData: ViewportData): void; } diff --git a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts b/src/vs/editor/browser/view/gpu/gpuViewLayer.ts deleted file mode 100644 index 2e1a5afea37..00000000000 --- a/src/vs/editor/browser/view/gpu/gpuViewLayer.ts +++ /dev/null @@ -1,348 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getActiveWindow } from 'vs/base/browser/dom'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; -import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; -import { FullFileRenderStrategy } from 'vs/editor/browser/view/gpu/fullFileRenderStrategy'; -import { BindingId, type IRendererContext, type IRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; -import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; -import { ensureNonNullable, observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { ILineFactory, IVisibleLine } from 'vs/editor/browser/view/viewLayer'; -import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; - -export const disableNonGpuRendering = true; - -const enum GlyphStorageBufferInfo { - FloatsPerEntry = 2 + 2 + 2, - BytesPerEntry = GlyphStorageBufferInfo.FloatsPerEntry * 4, - Offset_TexturePosition = 0, - Offset_TextureSize = 2, - Offset_OriginPosition = 4, -} - -export class GpuViewLayerRenderer extends Disposable { - - readonly domNode: HTMLCanvasElement; - host: ILineFactory; - viewportData: ViewportData; - - private readonly _gpuCtx!: GPUCanvasContext; - - private _device!: GPUDevice; - private _renderPassDescriptor!: GPURenderPassDescriptor; - private _renderPassColorAttachment!: GPURenderPassColorAttachment; - private _bindGroup!: GPUBindGroup; - private _pipeline!: GPURenderPipeline; - - private _vertexBuffer!: GPUBuffer; - - static atlas: TextureAtlas; - - private readonly _glyphStorageBuffer: GPUBuffer[] = []; - private _atlasGpuTexture!: GPUTexture; - private readonly _atlasGpuTextureVersions: number[] = []; - - private _initialized = false; - - private _renderStrategy!: IRenderStrategy; - - constructor( - domNode: HTMLCanvasElement, - private readonly _context: ViewContext, - host: ILineFactory, - viewportData: ViewportData, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILogService private readonly _logService: ILogService, - ) { - super(); - - this.domNode = domNode; - this.host = host; - this.viewportData = viewportData; - - this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { - this.domNode.width = w; - this.domNode.height = h; - // TODO: Request render - })); - - this._gpuCtx = ensureNonNullable(this.domNode.getContext('webgpu')); - this.initWebgpu(); - } - - async initWebgpu() { - this._device = this._register(await GPULifecycle.requestDevice()).object; - - const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); - this._gpuCtx.configure({ - device: this._device, - format: presentationFormat, - alphaMode: 'premultiplied', - }); - - - // Create texture atlas - if (!GpuViewLayerRenderer.atlas) { - GpuViewLayerRenderer.atlas = this._instantiationService.createInstance(TextureAtlas, this._device.limits.maxTextureDimension2D, undefined); - } - const atlas = GpuViewLayerRenderer.atlas; - - - this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.domNode, this.viewportData, GpuViewLayerRenderer.atlas)); - - const module = this._device.createShaderModule({ - label: 'Monaco shader module', - code: this._renderStrategy.wgsl, - }); - - this._pipeline = this._device.createRenderPipeline({ - label: 'Monaco render pipeline', - layout: 'auto', - vertex: { - module, - entryPoint: 'vs', - buffers: [ - { - arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each - attributes: [ - { shaderLocation: 0, offset: 0, format: 'float32x2' }, // position - ], - } - ] - }, - fragment: { - module, - entryPoint: 'fs', - targets: [ - { - format: presentationFormat, - blend: { - color: { - srcFactor: 'src-alpha', - dstFactor: 'one-minus-src-alpha' - }, - alpha: { - srcFactor: 'src-alpha', - dstFactor: 'one-minus-src-alpha' - }, - }, - } - ], - }, - }); - - - - // Write standard uniforms - const enum CanvasDimensionsUniformBufferInfo { - FloatsPerEntry = 2, - BytesPerEntry = CanvasDimensionsUniformBufferInfo.FloatsPerEntry * 4, - Offset_CanvasWidth = 0, - Offset_CanvasHeight = 1 - } - const canvasDimensionsUniformBufferValues = new Float32Array(CanvasDimensionsUniformBufferInfo.FloatsPerEntry); - const canvasDimensionsUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco uniform buffer', - size: CanvasDimensionsUniformBufferInfo.BytesPerEntry, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }, () => { - canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; - canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; - return canvasDimensionsUniformBufferValues; - })).object; - this._register(observeDevicePixelDimensions(this.domNode, getActiveWindow(), (w, h) => { - canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.domNode.width; - canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.domNode.height; - this._device.queue.writeBuffer(canvasDimensionsUniformBuffer, 0, canvasDimensionsUniformBufferValues); - })); - - - - const enum AtlasInfoUniformBufferInfo { - FloatsPerEntry = 2, - BytesPerEntry = AtlasInfoUniformBufferInfo.FloatsPerEntry * 4, - Offset_Width = 0, - Offset_Height = 1, - } - const atlasInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco atlas info uniform buffer', - size: AtlasInfoUniformBufferInfo.BytesPerEntry, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }, () => { - const values = new Float32Array(AtlasInfoUniformBufferInfo.FloatsPerEntry); - values[AtlasInfoUniformBufferInfo.Offset_Width] = atlas.pageSize; - values[AtlasInfoUniformBufferInfo.Offset_Height] = atlas.pageSize; - return values; - })).object; - - - this._glyphStorageBuffer[0] = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco glyph storage buffer', - size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).object; - this._glyphStorageBuffer[1] = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco glyph storage buffer', - size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).object; - this._atlasGpuTextureVersions[0] = 0; - this._atlasGpuTextureVersions[1] = 0; - this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, { - label: 'Monaco atlas texture', - format: 'rgba8unorm', - // TODO: Dynamically grow/shrink layer count - size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: 2 }, - dimension: '2d', - usage: GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT, - })).object; - - - this._updateAtlas(); - - - - this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco vertex buffer', - size: quadVertices.byteLength, - usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - }, quadVertices)).object; - - - - const sampler = this._device.createSampler({ - label: 'Monaco atlas sampler', - magFilter: 'nearest', - minFilter: 'nearest', - }); - this._bindGroup = this._device.createBindGroup({ - label: 'Monaco bind group', - layout: this._pipeline.getBindGroupLayout(0), - entries: [ - // TODO: Pass in generically as array? - { binding: BindingId.GlyphInfo0, resource: { buffer: this._glyphStorageBuffer[0] } }, - { binding: BindingId.GlyphInfo1, resource: { buffer: this._glyphStorageBuffer[1] } }, - { binding: BindingId.TextureSampler, resource: sampler }, - { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, - { binding: BindingId.CanvasDimensionsUniform, resource: { buffer: canvasDimensionsUniformBuffer } }, - { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, - ...this._renderStrategy.bindGroupEntries - ], - }); - - this._renderPassColorAttachment = { - view: null!, // Will be filled at render time - loadOp: 'load', - storeOp: 'store', - }; - this._renderPassDescriptor = { - label: 'Monaco render pass', - colorAttachments: [this._renderPassColorAttachment], - }; - - - this._initialized = true; - } - - update(viewportData: ViewportData) { - this.viewportData = viewportData; - } - - private _updateAtlas() { - const atlas = GpuViewLayerRenderer.atlas; - - for (const [layerIndex, page] of atlas.pages.entries()) { - // Skip the update if it's already the latest version - if (page.version === this._atlasGpuTextureVersions[layerIndex]) { - continue; - } - - this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version); - - // TODO: Reuse buffer instead of reconstructing each time - // TODO: Dynamically set buffer size - const values = new Float32Array(GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount); - let entryOffset = 0; - for (const glyph of page.glyphs) { - values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition] = glyph.x; - values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; - values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize] = glyph.w; - values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; - values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; - values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; - entryOffset += GlyphStorageBufferInfo.FloatsPerEntry; - } - if (entryOffset / GlyphStorageBufferInfo.FloatsPerEntry > TextureAtlasPage.maximumGlyphCount) { - throw new Error(`Attempting to write more glyphs (${entryOffset / GlyphStorageBufferInfo.FloatsPerEntry}) than the GPUBuffer can hold (${TextureAtlasPage.maximumGlyphCount})`); - } - this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); - this._device.queue.copyExternalImageToTexture( - { source: page.source }, - { - texture: this._atlasGpuTexture, - origin: { - x: page.usedArea.left, - y: page.usedArea.top, - z: layerIndex - } - }, - { - width: page.usedArea.right - page.usedArea.left, - height: page.usedArea.bottom - page.usedArea.top - }, - ); - this._atlasGpuTextureVersions[layerIndex] = page.version; - } - } - - public render(inContext: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { - const ctx: IRendererContext = { - rendLineNumberStart: inContext.rendLineNumberStart, - lines: inContext.lines.slice(0), - linesLength: inContext.linesLength - }; - - if (!this._initialized) { - return ctx; - } - return this._render(ctx, startLineNumber, stopLineNumber, deltaTop); - } - - private _render(ctx: IRendererContext, startLineNumber: number, stopLineNumber: number, deltaTop: number[]): IRendererContext { - const visibleObjectCount = this._renderStrategy.update(ctx, startLineNumber, stopLineNumber, deltaTop); - - this._updateAtlas(); - - const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' }); - - this._renderPassColorAttachment.view = this._gpuCtx.getCurrentTexture().createView({ label: 'Monaco canvas texture view' }); - const pass = encoder.beginRenderPass(this._renderPassDescriptor); - pass.setPipeline(this._pipeline); - pass.setVertexBuffer(0, this._vertexBuffer); - - pass.setBindGroup(0, this._bindGroup); - - if (this._renderStrategy?.draw) { - this._renderStrategy.draw(pass, ctx, startLineNumber, stopLineNumber, deltaTop); - } else { - pass.draw(quadVertices.length / 2, visibleObjectCount); - } - - pass.end(); - - const commandBuffer = encoder.finish(); - - this._device.queue.submit([commandBuffer]); - - return ctx; - } -} diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index fce30c26464..84f730c895e 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -6,14 +6,10 @@ import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; import { BugIndicatingError } from 'vs/base/common/errors'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { GpuViewLayerRenderer } from 'vs/editor/browser/view/gpu/gpuViewLayer'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; /** * Represents a visible line @@ -253,7 +249,7 @@ export class RenderedLinesCollection { } } -export class VisibleLinesCollection extends Disposable { +export class VisibleLinesCollection { public readonly domNode: FastDomNode = this._createDomNode(); private readonly _linesCollection: RenderedLinesCollection = new RenderedLinesCollection(this._lineFactory); @@ -261,12 +257,8 @@ export class VisibleLinesCollection extends Disposable { private readonly _canvas: HTMLCanvasElement; constructor( - // TODO: Remove when GPU renderer is moved into view part - private readonly _context: ViewContext, - private readonly _lineFactory: ILineFactory, - @IInstantiationService private readonly _instantiationService: IInstantiationService + private readonly _lineFactory: ILineFactory ) { - super(); this._canvas = document.createElement('canvas'); this._canvas.style.height = '100%'; this._canvas.style.width = '100%'; @@ -352,26 +344,24 @@ export class VisibleLinesCollection extends Disposable { return this._linesCollection.getLine(lineNumber); } - private _gpuRenderer: GpuViewLayerRenderer | undefined; - public renderLines(viewportData: ViewportData, viewOverlays?: boolean): void { const inp = this._linesCollection._get(); - let renderer; - if (viewOverlays) { - renderer = new ViewLayerRenderer(this.domNode.domNode, this._lineFactory, viewportData); - } else { - // If not yet attached, listen for device pixel size and attach - if (!this._canvas.parentElement) { - this.domNode.domNode.appendChild(this._canvas); - } + // let renderer; + // if (viewOverlays) { + const renderer = new ViewLayerRenderer(this.domNode.domNode, this._lineFactory, viewportData); + // } else { + // // If not yet attached, listen for device pixel size and attach + // if (!this._canvas.parentElement) { + // this.domNode.domNode.appendChild(this._canvas); + // } - if (!this._gpuRenderer) { - this._gpuRenderer = this._register(this._instantiationService.createInstance(GpuViewLayerRenderer, this._canvas, this._context, this._lineFactory, viewportData)); - } - renderer = this._gpuRenderer; - renderer.update(viewportData); - } + // if (!this._gpuRenderer) { + // this._gpuRenderer = this._register(this._instantiationService.createInstance(GpuViewLayerRenderer, this._canvas, this._context, this._lineFactory, viewportData)); + // } + // renderer = this._gpuRenderer; + // renderer.update(viewportData); + // } const ctx: IRendererContext = { rendLineNumberStart: inp.rendLineNumberStart, diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 19ca61dc577..189ff3daeba 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -15,7 +15,7 @@ import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { disableNonGpuRendering } from 'vs/editor/browser/view/gpu/gpuViewLayer'; +import { disableNonGpuRendering } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; export class ViewOverlays extends ViewPart { private readonly _visibleLines: VisibleLinesCollection; @@ -29,9 +29,9 @@ export class ViewOverlays extends ViewPart { ) { super(context); - this._visibleLines = this._register(instantiationService.createInstance(VisibleLinesCollection, context, { + this._visibleLines = new VisibleLinesCollection({ createLine: () => new ViewOverlayLine(this._dynamicOverlays) - })); + }); this.domNode = this._visibleLines.domNode; const options = this._context.configuration.options; diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index 60aa9ce1281..625993303d0 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -4,17 +4,303 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from 'vs/base/common/errors'; +import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; +import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; -import { ViewLineOptions } from '../lines/viewLineOptions'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ViewLinesChangedEvent, ViewScrollChangedEvent } from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; +import { ViewLineOptions } from '../lines/viewLineOptions'; +import { ensureNonNullable, observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { getActiveWindow } from 'vs/base/browser/dom'; +import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { FullFileRenderStrategy } from 'vs/editor/browser/view/gpu/fullFileRenderStrategy'; +import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; + +export const disableNonGpuRendering = false; + +const enum GlyphStorageBufferInfo { + FloatsPerEntry = 2 + 2 + 2, + BytesPerEntry = GlyphStorageBufferInfo.FloatsPerEntry * 4, + Offset_TexturePosition = 0, + Offset_TextureSize = 2, + Offset_OriginPosition = 4, +} export class ViewLinesGpu extends ViewPart { - constructor(context: ViewContext, private readonly canvas: HTMLCanvasElement) { + + private readonly _gpuCtx!: GPUCanvasContext; + + private _device!: GPUDevice; + private _renderPassDescriptor!: GPURenderPassDescriptor; + private _renderPassColorAttachment!: GPURenderPassColorAttachment; + private _bindGroup!: GPUBindGroup; + private _pipeline!: GPURenderPipeline; + + private _vertexBuffer!: GPUBuffer; + + static atlas: TextureAtlas; + + private readonly _glyphStorageBuffer: GPUBuffer[] = []; + private _atlasGpuTexture!: GPUTexture; + private readonly _atlasGpuTextureVersions: number[] = []; + + private _initialized = false; + + private _renderStrategy!: IGpuRenderStrategy; + + constructor( + context: ViewContext, + private readonly canvas: HTMLCanvasElement, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + ) { super(context); + + // TODO: Add canvas device pixel resize event to gpu context + const win = getActiveWindow(); + this.canvas.width = this.canvas.clientWidth * win.devicePixelRatio; + this.canvas.height = this.canvas.clientHeight * win.devicePixelRatio; + this._register(observeDevicePixelDimensions(canvas, getActiveWindow(), (w, h) => { + this.canvas.width = w; + this.canvas.height = h; + // TODO: Request render, should this just call renderText with the last viewportData + })); + + this._gpuCtx = ensureNonNullable(canvas.getContext('webgpu')); + + // TODO: It would be nice if the async part of this (requesting device) was done before + // ViewLinesGpu was constructed + this.initWebgpu(); + } + + async initWebgpu() { + this._device = this._register(await GPULifecycle.requestDevice()).object; + + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + this._gpuCtx.configure({ + device: this._device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + + + // TODO: Should the texture atlas (shared across all editors) should be part of the gpu context (shared across view parts of this editor)? + // Create texture atlas + if (!ViewLinesGpu.atlas) { + ViewLinesGpu.atlas = this._instantiationService.createInstance(TextureAtlas, this._device.limits.maxTextureDimension2D, undefined); + } + const atlas = ViewLinesGpu.atlas; + + // TODO: Remove viewportData arg, this is passed into the render call + this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.canvas, ViewLinesGpu.atlas)); + + const module = this._device.createShaderModule({ + label: 'Monaco shader module', + code: this._renderStrategy.wgsl, + }); + + this._pipeline = this._device.createRenderPipeline({ + label: 'Monaco render pipeline', + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + buffers: [ + { + arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each + attributes: [ + { shaderLocation: 0, offset: 0, format: 'float32x2' }, // position + ], + } + ] + }, + fragment: { + module, + entryPoint: 'fs', + targets: [ + { + format: presentationFormat, + blend: { + color: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha' + }, + alpha: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha' + }, + }, + } + ], + }, + }); + + + + // Write standard uniforms + const enum CanvasDimensionsUniformBufferInfo { + FloatsPerEntry = 2, + BytesPerEntry = CanvasDimensionsUniformBufferInfo.FloatsPerEntry * 4, + Offset_CanvasWidth = 0, + Offset_CanvasHeight = 1 + } + const canvasDimensionsUniformBufferValues = new Float32Array(CanvasDimensionsUniformBufferInfo.FloatsPerEntry); + const canvasDimensionsUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco uniform buffer', + size: CanvasDimensionsUniformBufferInfo.BytesPerEntry, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }, () => { + canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.canvas.width; + canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.canvas.height; + return canvasDimensionsUniformBufferValues; + })).object; + this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => { + canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.canvas.width; + canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.canvas.height; + this._device.queue.writeBuffer(canvasDimensionsUniformBuffer, 0, canvasDimensionsUniformBufferValues); + })); + + + + const enum AtlasInfoUniformBufferInfo { + FloatsPerEntry = 2, + BytesPerEntry = AtlasInfoUniformBufferInfo.FloatsPerEntry * 4, + Offset_Width = 0, + Offset_Height = 1, + } + const atlasInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco atlas info uniform buffer', + size: AtlasInfoUniformBufferInfo.BytesPerEntry, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }, () => { + const values = new Float32Array(AtlasInfoUniformBufferInfo.FloatsPerEntry); + values[AtlasInfoUniformBufferInfo.Offset_Width] = atlas.pageSize; + values[AtlasInfoUniformBufferInfo.Offset_Height] = atlas.pageSize; + return values; + })).object; + + + this._glyphStorageBuffer[0] = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco glyph storage buffer', + size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._glyphStorageBuffer[1] = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco glyph storage buffer', + size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._atlasGpuTextureVersions[0] = 0; + this._atlasGpuTextureVersions[1] = 0; + this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, { + label: 'Monaco atlas texture', + format: 'rgba8unorm', + // TODO: Dynamically grow/shrink layer count + size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: 2 }, + dimension: '2d', + usage: GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + })).object; + + + this._updateAtlas(); + + + + this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco vertex buffer', + size: quadVertices.byteLength, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }, quadVertices)).object; + + + + const sampler = this._device.createSampler({ + label: 'Monaco atlas sampler', + magFilter: 'nearest', + minFilter: 'nearest', + }); + this._bindGroup = this._device.createBindGroup({ + label: 'Monaco bind group', + layout: this._pipeline.getBindGroupLayout(0), + entries: [ + // TODO: Pass in generically as array? + { binding: BindingId.GlyphInfo0, resource: { buffer: this._glyphStorageBuffer[0] } }, + { binding: BindingId.GlyphInfo1, resource: { buffer: this._glyphStorageBuffer[1] } }, + { binding: BindingId.TextureSampler, resource: sampler }, + { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, + { binding: BindingId.CanvasDimensionsUniform, resource: { buffer: canvasDimensionsUniformBuffer } }, + { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, + ...this._renderStrategy.bindGroupEntries + ], + }); + + this._renderPassColorAttachment = { + view: null!, // Will be filled at render time + loadOp: 'load', + storeOp: 'store', + }; + this._renderPassDescriptor = { + label: 'Monaco render pass', + colorAttachments: [this._renderPassColorAttachment], + }; + + + this._initialized = true; + } + + private _updateAtlas() { + const atlas = ViewLinesGpu.atlas; + + for (const [layerIndex, page] of atlas.pages.entries()) { + // Skip the update if it's already the latest version + if (page.version === this._atlasGpuTextureVersions[layerIndex]) { + continue; + } + + this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version); + + // TODO: Reuse buffer instead of reconstructing each time + // TODO: Dynamically set buffer size + const values = new Float32Array(GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount); + let entryOffset = 0; + for (const glyph of page.glyphs) { + values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition] = glyph.x; + values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; + values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize] = glyph.w; + values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; + values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; + values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; + entryOffset += GlyphStorageBufferInfo.FloatsPerEntry; + } + if (entryOffset / GlyphStorageBufferInfo.FloatsPerEntry > TextureAtlasPage.maximumGlyphCount) { + throw new Error(`Attempting to write more glyphs (${entryOffset / GlyphStorageBufferInfo.FloatsPerEntry}) than the GPUBuffer can hold (${TextureAtlasPage.maximumGlyphCount})`); + } + this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); + if (page.usedArea.right - page.usedArea.left > 0 && page.usedArea.bottom - page.usedArea.top > 0) { + this._device.queue.copyExternalImageToTexture( + { source: page.source }, + { + texture: this._atlasGpuTexture, + origin: { + x: page.usedArea.left, + y: page.usedArea.top, + z: layerIndex + } + }, + { + width: page.usedArea.right - page.usedArea.left, + height: page.usedArea.bottom - page.usedArea.top + }, + ); + } + this._atlasGpuTextureVersions[layerIndex] = page.version; + } } public static canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { @@ -42,24 +328,58 @@ export class ViewLinesGpu extends ViewPart { // subscribe to more events public renderText(viewportData: ViewportData): void { - const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); + // TODO: Remove this simple canvas rendering when webgpu stuff is hooked up + // const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); - const ctx = this.canvas.getContext('2d')!; - ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - ctx.strokeStyle = 'black'; - const vp = this._context.viewLayout.getCurrentViewport(); - const left = this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft; - this.canvas.width = vp.width; - this.canvas.height = vp.height; - ctx.font = `${this._context.configuration.options.get(EditorOption.fontSize)}px monospace`; + // const ctxSimple = this.canvas.getContext('2d')!; + // ctxSimple.clearRect(0, 0, this.canvas.width, this.canvas.height); + // ctxSimple.strokeStyle = 'black'; + // const vp = this._context.viewLayout.getCurrentViewport(); + // const left = this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft; + // this.canvas.width = vp.width; + // this.canvas.height = vp.height; + // ctxSimple.font = `${this._context.configuration.options.get(EditorOption.fontSize)}px monospace`; - for (let i = viewportData.startLineNumber; i <= viewportData.endLineNumber; i++) { - if (!ViewLinesGpu.canRender(options, viewportData, i)) { - continue; - } - const line = viewportData.getViewLineRenderingData(i); + // for (let i = viewportData.startLineNumber; i <= viewportData.endLineNumber; i++) { + // if (!ViewLinesGpu.canRender(options, viewportData, i)) { + // continue; + // } + // const line = viewportData.getViewLineRenderingData(i); - ctx.strokeText(line.content, left, viewportData.relativeVerticalOffset[i - viewportData.startLineNumber] + viewportData.lineHeight - this._context.viewLayout.getCurrentScrollTop()); + // ctxSimple.strokeText(line.content, left, viewportData.relativeVerticalOffset[i - viewportData.startLineNumber] + viewportData.lineHeight - this._context.viewLayout.getCurrentScrollTop()); + // } + + + + if (this._initialized) { + return this._renderText(viewportData); } } + + private _renderText(viewportData: ViewportData): void { + const visibleObjectCount = this._renderStrategy.update(viewportData); + + this._updateAtlas(); + + const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' }); + + this._renderPassColorAttachment.view = this._gpuCtx.getCurrentTexture().createView({ label: 'Monaco canvas texture view' }); + const pass = encoder.beginRenderPass(this._renderPassDescriptor); + pass.setPipeline(this._pipeline); + pass.setVertexBuffer(0, this._vertexBuffer); + + pass.setBindGroup(0, this._bindGroup); + + if (this._renderStrategy?.draw) { + this._renderStrategy.draw(pass, viewportData); + } else { + pass.draw(quadVertices.length / 2, visibleObjectCount); + } + + pass.end(); + + const commandBuffer = encoder.finish(); + + this._device.queue.submit([commandBuffer]); + } } diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 0eae4975b38..421680d29ed 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -145,9 +145,9 @@ export class ViewLines extends ViewPart implements IViewLines { this._linesContent = linesContent; this._textRangeRestingSpot = document.createElement('div'); - this._visibleLines = this._register(instantiationService.createInstance(VisibleLinesCollection, context, { + this._visibleLines = new VisibleLinesCollection({ createLine: () => new ViewLine(this._viewLineOptions), - })); + }); this.domNode = this._visibleLines.domNode; PartFingerprints.write(this.domNode, PartFingerprint.ViewLines); diff --git a/src/vs/editor/browser/widget/codeEditor/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css index 59c86e0b8a9..d33122122de 100644 --- a/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/src/vs/editor/browser/widget/codeEditor/editor.css @@ -47,6 +47,8 @@ .monaco-editor .editorCanvas { position: absolute; + width: 100%; + height: 100%; z-index: 0; pointer-events: none; } diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index df1e5f77b1b..a533a2c39b5 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -8,8 +8,8 @@ import { URI } from 'vs/base/common/uri'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, type ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { GpuViewLayerRenderer } from 'vs/editor/browser/view/gpu/gpuViewLayer'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -33,8 +33,8 @@ class LogTextureAtlasStatsAction extends EditorAction { async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const logService = accessor.get(ILogService); - const atlas = GpuViewLayerRenderer.atlas; - if (!GpuViewLayerRenderer.atlas) { + const atlas = ViewLinesGpu.atlas; + if (!ViewLinesGpu.atlas) { logService.error('No texture atlas found'); return; } @@ -60,7 +60,7 @@ class SaveTextureAtlasAction extends EditorAction { const fileService = accessor.get(IFileService); const folders = workspaceContextService.getWorkspace().folders; if (folders.length > 0) { - const atlas = GpuViewLayerRenderer.atlas; + const atlas = ViewLinesGpu.atlas; const promises = []; for (const [layerIndex, page] of atlas.pages.entries()) { promises.push(...[ @@ -102,8 +102,8 @@ class DrawGlyphAction extends EditorAction { return; } - const atlas = GpuViewLayerRenderer.atlas; - if (!GpuViewLayerRenderer.atlas) { + const atlas = ViewLinesGpu.atlas; + if (!ViewLinesGpu.atlas) { logService.error('No texture atlas found'); return; } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 5e8f0ff94ff..cf7fb935e58 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -9,8 +9,8 @@ import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; -import { disableNonGpuRendering } from 'vs/editor/browser/view/gpu/gpuViewLayer'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { disableNonGpuRendering } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; From a03124692d1ffe4cac3fd6f895078cf082342d88 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:06:45 -0700 Subject: [PATCH 129/286] Add text viewport offset to uniform --- .../view/gpu/fullFileRenderStrategy.ts | 10 ++++- src/vs/editor/browser/view/gpu/gpu.ts | 2 +- .../browser/viewParts/gpu/viewLinesGpu.ts | 42 ++++++++++++------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts index 66dfa179a11..9121c154dbe 100644 --- a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts @@ -39,6 +39,12 @@ struct Cell { textureIndex: f32 }; +struct LayoutInfo { + canvasDims: vec2f, + viewportOffset: vec2f, + viewportDims: vec2f, +} + struct ScrollOffset { offset: vec2f } @@ -50,7 +56,7 @@ struct VSOutput { }; // Uniforms -@group(0) @binding(${BindingId.CanvasDimensionsUniform}) var canvasDims: vec2f; +@group(0) @binding(${BindingId.ViewportUniform}) var layoutInfo: LayoutInfo; @group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: vec2f; @group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; @@ -78,7 +84,7 @@ struct VSOutput { // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( // TODO: Fix hacky scroll offset which moves the text beside the line numbers - (((vert.position * vec2f(2, -2)) / canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / canvasDims) + (((scrollOffset.offset - vec2f(-110, 0)) * 2) / canvasDims), + (((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) + (((scrollOffset.offset + layoutInfo.viewportOffset) * 2) / layoutInfo.canvasDims), 0.0, 1.0 ); diff --git a/src/vs/editor/browser/view/gpu/gpu.ts b/src/vs/editor/browser/view/gpu/gpu.ts index 231d8c526d4..743382aeac4 100644 --- a/src/vs/editor/browser/view/gpu/gpu.ts +++ b/src/vs/editor/browser/view/gpu/gpu.ts @@ -11,7 +11,7 @@ export const enum BindingId { Cells, TextureSampler, Texture, - CanvasDimensionsUniform, + ViewportUniform, AtlasDimensionsUniform, ScrollOffset, } diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index 625993303d0..317d7f0450a 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -19,6 +19,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILogService } from 'vs/platform/log/common/log'; import { FullFileRenderStrategy } from 'vs/editor/browser/view/gpu/fullFileRenderStrategy'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; export const disableNonGpuRendering = false; @@ -140,28 +141,39 @@ export class ViewLinesGpu extends ViewPart { }); - // Write standard uniforms - const enum CanvasDimensionsUniformBufferInfo { - FloatsPerEntry = 2, - BytesPerEntry = CanvasDimensionsUniformBufferInfo.FloatsPerEntry * 4, + const enum LayoutInfoUniformBufferInfo { + FloatsPerEntry = 6, + BytesPerEntry = LayoutInfoUniformBufferInfo.FloatsPerEntry * 4, Offset_CanvasWidth = 0, - Offset_CanvasHeight = 1 + Offset_CanvasHeight = 1, + Offset_ViewportOffsetX = 2, + Offset_ViewportOffsetY = 3, + Offset_ViewportWidth = 4, + Offset_ViewportHeight = 5, } - const canvasDimensionsUniformBufferValues = new Float32Array(CanvasDimensionsUniformBufferInfo.FloatsPerEntry); - const canvasDimensionsUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { + const layoutInfoUniformBufferValues = new Float32Array(LayoutInfoUniformBufferInfo.FloatsPerEntry); + const layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco uniform buffer', - size: CanvasDimensionsUniformBufferInfo.BytesPerEntry, + size: LayoutInfoUniformBufferInfo.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }, () => { - canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.canvas.width; - canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.canvas.height; - return canvasDimensionsUniformBufferValues; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_CanvasWidth] = this.canvas.width; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_CanvasHeight] = this.canvas.height; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetY] = 0; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportWidth] = this.canvas.width - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetX]; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportHeight] = this.canvas.height - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetY]; + return layoutInfoUniformBufferValues; })).object; this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => { - canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasWidth] = this.canvas.width; - canvasDimensionsUniformBufferValues[CanvasDimensionsUniformBufferInfo.Offset_CanvasHeight] = this.canvas.height; - this._device.queue.writeBuffer(canvasDimensionsUniformBuffer, 0, canvasDimensionsUniformBufferValues); + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_CanvasWidth] = this.canvas.width; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_CanvasHeight] = this.canvas.height; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetY] = 0; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportWidth] = this.canvas.width - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetX]; + layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportHeight] = this.canvas.height - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetY]; + this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, layoutInfoUniformBufferValues); })); @@ -234,7 +246,7 @@ export class ViewLinesGpu extends ViewPart { { binding: BindingId.GlyphInfo1, resource: { buffer: this._glyphStorageBuffer[1] } }, { binding: BindingId.TextureSampler, resource: sampler }, { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, - { binding: BindingId.CanvasDimensionsUniform, resource: { buffer: canvasDimensionsUniformBuffer } }, + { binding: BindingId.ViewportUniform, resource: { buffer: layoutInfoUniformBuffer } }, { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, ...this._renderStrategy.bindGroupEntries ], From f45f30de7cc5c851b404b2ebc2dacb73a0f38b99 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:21:15 -0700 Subject: [PATCH 130/286] Split webgpu setup into regions --- .../browser/viewParts/gpu/viewLinesGpu.ts | 242 ++++++++++-------- 1 file changed, 132 insertions(+), 110 deletions(-) diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index 317d7f0450a..017801617d0 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -79,6 +79,8 @@ export class ViewLinesGpu extends ViewPart { } async initWebgpu() { + // #region General + this._device = this._register(await GPULifecycle.requestDevice()).object; const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); @@ -88,7 +90,6 @@ export class ViewLinesGpu extends ViewPart { alphaMode: 'premultiplied', }); - // TODO: Should the texture atlas (shared across all editors) should be part of the gpu context (shared across view parts of this editor)? // Create texture atlas if (!ViewLinesGpu.atlas) { @@ -96,14 +97,131 @@ export class ViewLinesGpu extends ViewPart { } const atlas = ViewLinesGpu.atlas; - // TODO: Remove viewportData arg, this is passed into the render call + this._renderPassColorAttachment = { + view: null!, // Will be filled at render time + loadOp: 'load', + storeOp: 'store', + }; + this._renderPassDescriptor = { + label: 'Monaco render pass', + colorAttachments: [this._renderPassColorAttachment], + }; + + // #endregion General + + // #region Uniforms + + let layoutInfoUniformBuffer: GPUBuffer; + { + const enum Info { + FloatsPerEntry = 6, + BytesPerEntry = Info.FloatsPerEntry * 4, + Offset_CanvasWidth____ = 0, + Offset_CanvasHeight___ = 1, + Offset_ViewportOffsetX = 2, + Offset_ViewportOffsetY = 3, + Offset_ViewportWidth__ = 4, + Offset_ViewportHeight_ = 5, + } + const bufferValues = new Float32Array(Info.FloatsPerEntry); + layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco uniform buffer', + size: Info.BytesPerEntry, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }, () => { + bufferValues[Info.Offset_CanvasWidth____] = this.canvas.width; + bufferValues[Info.Offset_CanvasHeight___] = this.canvas.height; + bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); + bufferValues[Info.Offset_ViewportOffsetY] = 0; + bufferValues[Info.Offset_ViewportWidth__] = this.canvas.width - bufferValues[Info.Offset_ViewportOffsetX]; + bufferValues[Info.Offset_ViewportHeight_] = this.canvas.height - bufferValues[Info.Offset_ViewportOffsetY]; + return bufferValues; + })).object; + this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => { + bufferValues[Info.Offset_CanvasWidth____] = this.canvas.width; + bufferValues[Info.Offset_CanvasHeight___] = this.canvas.height; + bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); + bufferValues[Info.Offset_ViewportOffsetY] = 0; + bufferValues[Info.Offset_ViewportWidth__] = this.canvas.width - bufferValues[Info.Offset_ViewportOffsetX]; + bufferValues[Info.Offset_ViewportHeight_] = this.canvas.height - bufferValues[Info.Offset_ViewportOffsetY]; + this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, bufferValues); + })); + } + + let atlasInfoUniformBuffer: GPUBuffer; + { + const enum Info { + FloatsPerEntry = 2, + BytesPerEntry = Info.FloatsPerEntry * 4, + Offset_Width_ = 0, + Offset_Height = 1, + } + atlasInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco atlas info uniform buffer', + size: Info.BytesPerEntry, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }, () => { + const values = new Float32Array(Info.FloatsPerEntry); + values[Info.Offset_Width_] = atlas.pageSize; + values[Info.Offset_Height] = atlas.pageSize; + return values; + })).object; + } + + // #endregion Uniforms + + // #region Storage buffers + this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.canvas, ViewLinesGpu.atlas)); + this._glyphStorageBuffer[0] = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco glyph storage buffer', + size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._glyphStorageBuffer[1] = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco glyph storage buffer', + size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._atlasGpuTextureVersions[0] = 0; + this._atlasGpuTextureVersions[1] = 0; + this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, { + label: 'Monaco atlas texture', + format: 'rgba8unorm', + // TODO: Dynamically grow/shrink layer count + size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: 2 }, + dimension: '2d', + usage: GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + })).object; + + this._updateAtlasStorageBufferAndTexture(); + + // #endregion Storage buffers + + // #region Vertex buffer + + this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco vertex buffer', + size: quadVertices.byteLength, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }, quadVertices)).object; + + // #endregion Vertex buffer + + // #region Shader module + const module = this._device.createShaderModule({ label: 'Monaco shader module', code: this._renderStrategy.wgsl, }); + // #endregion Shader module + + // #region Pipeline + this._pipeline = this._device.createRenderPipeline({ label: 'Monaco render pipeline', layout: 'auto', @@ -140,103 +258,10 @@ export class ViewLinesGpu extends ViewPart { }, }); + // #endregion Pipeline - // Write standard uniforms - const enum LayoutInfoUniformBufferInfo { - FloatsPerEntry = 6, - BytesPerEntry = LayoutInfoUniformBufferInfo.FloatsPerEntry * 4, - Offset_CanvasWidth = 0, - Offset_CanvasHeight = 1, - Offset_ViewportOffsetX = 2, - Offset_ViewportOffsetY = 3, - Offset_ViewportWidth = 4, - Offset_ViewportHeight = 5, - } - const layoutInfoUniformBufferValues = new Float32Array(LayoutInfoUniformBufferInfo.FloatsPerEntry); - const layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco uniform buffer', - size: LayoutInfoUniformBufferInfo.BytesPerEntry, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }, () => { - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_CanvasWidth] = this.canvas.width; - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_CanvasHeight] = this.canvas.height; - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetY] = 0; - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportWidth] = this.canvas.width - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetX]; - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportHeight] = this.canvas.height - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetY]; - return layoutInfoUniformBufferValues; - })).object; - this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => { - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_CanvasWidth] = this.canvas.width; - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_CanvasHeight] = this.canvas.height; - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetY] = 0; - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportWidth] = this.canvas.width - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetX]; - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportHeight] = this.canvas.height - layoutInfoUniformBufferValues[LayoutInfoUniformBufferInfo.Offset_ViewportOffsetY]; - this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, layoutInfoUniformBufferValues); - })); + // #region Bind group - - - const enum AtlasInfoUniformBufferInfo { - FloatsPerEntry = 2, - BytesPerEntry = AtlasInfoUniformBufferInfo.FloatsPerEntry * 4, - Offset_Width = 0, - Offset_Height = 1, - } - const atlasInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco atlas info uniform buffer', - size: AtlasInfoUniformBufferInfo.BytesPerEntry, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }, () => { - const values = new Float32Array(AtlasInfoUniformBufferInfo.FloatsPerEntry); - values[AtlasInfoUniformBufferInfo.Offset_Width] = atlas.pageSize; - values[AtlasInfoUniformBufferInfo.Offset_Height] = atlas.pageSize; - return values; - })).object; - - - this._glyphStorageBuffer[0] = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco glyph storage buffer', - size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).object; - this._glyphStorageBuffer[1] = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco glyph storage buffer', - size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, - usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, - })).object; - this._atlasGpuTextureVersions[0] = 0; - this._atlasGpuTextureVersions[1] = 0; - this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, { - label: 'Monaco atlas texture', - format: 'rgba8unorm', - // TODO: Dynamically grow/shrink layer count - size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: 2 }, - dimension: '2d', - usage: GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT, - })).object; - - - this._updateAtlas(); - - - - this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, { - label: 'Monaco vertex buffer', - size: quadVertices.byteLength, - usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - }, quadVertices)).object; - - - - const sampler = this._device.createSampler({ - label: 'Monaco atlas sampler', - magFilter: 'nearest', - minFilter: 'nearest', - }); this._bindGroup = this._device.createBindGroup({ label: 'Monaco bind group', layout: this._pipeline.getBindGroupLayout(0), @@ -244,7 +269,13 @@ export class ViewLinesGpu extends ViewPart { // TODO: Pass in generically as array? { binding: BindingId.GlyphInfo0, resource: { buffer: this._glyphStorageBuffer[0] } }, { binding: BindingId.GlyphInfo1, resource: { buffer: this._glyphStorageBuffer[1] } }, - { binding: BindingId.TextureSampler, resource: sampler }, + { + binding: BindingId.TextureSampler, resource: this._device.createSampler({ + label: 'Monaco atlas sampler', + magFilter: 'nearest', + minFilter: 'nearest', + }) + }, { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, { binding: BindingId.ViewportUniform, resource: { buffer: layoutInfoUniformBuffer } }, { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, @@ -252,21 +283,12 @@ export class ViewLinesGpu extends ViewPart { ], }); - this._renderPassColorAttachment = { - view: null!, // Will be filled at render time - loadOp: 'load', - storeOp: 'store', - }; - this._renderPassDescriptor = { - label: 'Monaco render pass', - colorAttachments: [this._renderPassColorAttachment], - }; - + // endregion Bind group this._initialized = true; } - private _updateAtlas() { + private _updateAtlasStorageBufferAndTexture() { const atlas = ViewLinesGpu.atlas; for (const [layerIndex, page] of atlas.pages.entries()) { @@ -371,7 +393,7 @@ export class ViewLinesGpu extends ViewPart { private _renderText(viewportData: ViewportData): void { const visibleObjectCount = this._renderStrategy.update(viewportData); - this._updateAtlas(); + this._updateAtlasStorageBufferAndTexture(); const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' }); From 7fcd5a52035de7d0b3cf00fea186b5b1b769f947 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:29:06 -0700 Subject: [PATCH 131/286] DRY buffer values --- .../browser/viewParts/gpu/viewLinesGpu.ts | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index 017801617d0..b34425e68c7 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -124,27 +124,23 @@ export class ViewLinesGpu extends ViewPart { Offset_ViewportHeight_ = 5, } const bufferValues = new Float32Array(Info.FloatsPerEntry); + const updateBufferValues = (canvasDevicePixelWidth: number = this.canvas.width, canvasDevicePixelHeight: number = this.canvas.height) => { + bufferValues[Info.Offset_CanvasWidth____] = canvasDevicePixelWidth; + bufferValues[Info.Offset_CanvasHeight___] = canvasDevicePixelHeight; + bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); + // TODO: This value should probably be 0 if the text is rendered to the baseline + bufferValues[Info.Offset_ViewportOffsetY] = -4; + bufferValues[Info.Offset_ViewportWidth__] = bufferValues[Info.Offset_CanvasWidth____] - bufferValues[Info.Offset_ViewportOffsetX]; + bufferValues[Info.Offset_ViewportHeight_] = bufferValues[Info.Offset_CanvasHeight___] - bufferValues[Info.Offset_ViewportOffsetY]; + return bufferValues; + }; layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { label: 'Monaco uniform buffer', size: Info.BytesPerEntry, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }, () => { - bufferValues[Info.Offset_CanvasWidth____] = this.canvas.width; - bufferValues[Info.Offset_CanvasHeight___] = this.canvas.height; - bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); - bufferValues[Info.Offset_ViewportOffsetY] = 0; - bufferValues[Info.Offset_ViewportWidth__] = this.canvas.width - bufferValues[Info.Offset_ViewportOffsetX]; - bufferValues[Info.Offset_ViewportHeight_] = this.canvas.height - bufferValues[Info.Offset_ViewportOffsetY]; - return bufferValues; - })).object; + }, () => updateBufferValues())).object; this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => { - bufferValues[Info.Offset_CanvasWidth____] = this.canvas.width; - bufferValues[Info.Offset_CanvasHeight___] = this.canvas.height; - bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); - bufferValues[Info.Offset_ViewportOffsetY] = 0; - bufferValues[Info.Offset_ViewportWidth__] = this.canvas.width - bufferValues[Info.Offset_ViewportOffsetX]; - bufferValues[Info.Offset_ViewportHeight_] = this.canvas.height - bufferValues[Info.Offset_ViewportOffsetY]; - this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, bufferValues); + this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(w, h)); })); } @@ -405,6 +401,7 @@ export class ViewLinesGpu extends ViewPart { pass.setBindGroup(0, this._bindGroup); if (this._renderStrategy?.draw) { + // TODO: Don't draw lines if ViewLinesGpu.canRender is false this._renderStrategy.draw(pass, viewportData); } else { pass.draw(quadVertices.length / 2, visibleObjectCount); From fbabed411d67b1c8d58513e109e22b66f0ebf67d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 11:48:33 -0700 Subject: [PATCH 132/286] Get line y position close while still using top baseline --- .../editor/browser/view/gpu/fullFileRenderStrategy.ts | 10 +++++++++- src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts | 3 +-- src/vs/editor/contrib/gpu/browser/gpuActions.ts | 6 +++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts index 9121c154dbe..9ae10a58a27 100644 --- a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts @@ -312,8 +312,16 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); + // TODO: Proper char width screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); - screenAbsoluteY = Math.round(viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * activeWindow.devicePixelRatio); + screenAbsoluteY = ( + Math.ceil(( + // Top of line including line height + viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] + + // Delta to top of line after line height + Math.floor((viewportData.lineHeight - this._context.configuration.options.get(EditorOption.fontSize)) / 2) + ) * activeWindow.devicePixelRatio) + ); zeroToOneX = screenAbsoluteX / this._canvas.width; zeroToOneY = screenAbsoluteY / this._canvas.height; wgslX = zeroToOneX * 2 - 1; diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index b34425e68c7..36bee06b074 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -128,8 +128,7 @@ export class ViewLinesGpu extends ViewPart { bufferValues[Info.Offset_CanvasWidth____] = canvasDevicePixelWidth; bufferValues[Info.Offset_CanvasHeight___] = canvasDevicePixelHeight; bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); - // TODO: This value should probably be 0 if the text is rendered to the baseline - bufferValues[Info.Offset_ViewportOffsetY] = -4; + bufferValues[Info.Offset_ViewportOffsetY] = 0; bufferValues[Info.Offset_ViewportWidth__] = bufferValues[Info.Offset_CanvasWidth____] - bufferValues[Info.Offset_ViewportOffsetX]; bufferValues[Info.Offset_ViewportHeight_] = bufferValues[Info.Offset_CanvasHeight___] - bufferValues[Info.Offset_ViewportOffsetY]; return bufferValues; diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index a533a2c39b5..6699da06c68 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -117,9 +117,9 @@ class DrawGlyphAction extends EditorAction { if (!chars) { return; } - const codePoint = chars.match(/0x(?\d+)/)?.groups?.codePoint; + const codePoint = chars.match(/0x(?[0-9a-f]+)/i)?.groups?.codePoint; if (codePoint !== undefined) { - chars = String.fromCodePoint(parseInt(codePoint)); + chars = String.fromCodePoint(parseInt(codePoint, 16)); } const metadata = 0; const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, metadata); @@ -139,7 +139,7 @@ class DrawGlyphAction extends EditorAction { const ctx = ensureNonNullable(canvas.getContext('2d')); ctx.putImageData(imageData, 0, 0); const blob = await canvas.convertToBlob({ type: 'image/png' }); - const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${metadata}_${fontSize}px_${fontFamily.replaceAll(/[,\.'\s]/g, '_')}.png`); + const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${metadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`); await fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer()))); } } From 5bd86417d9ebcab7ec94bb2b4deb1abf6c9ae17f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 12:11:42 -0700 Subject: [PATCH 133/286] Use standard character width for positioning --- src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts | 7 ++++--- src/vs/editor/browser/view/gpu/gpu.ts | 3 ++- src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts index 9ae10a58a27..34ef96c741d 100644 --- a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts @@ -11,6 +11,7 @@ import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/view/gpu/g import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; import { quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { ViewLineOptions } from 'vs/editor/browser/viewParts/lines/viewLineOptions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -190,7 +191,7 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra ]; } - update(viewportData: ViewportData): number { + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { // Pre-allocate variables to be shared within the loop - don't trust the JIT compiler to do // this optimization to avoid additional blocking time in garbage collector let chars = ''; @@ -312,8 +313,8 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); - // TODO: Proper char width - screenAbsoluteX = Math.round((x + xOffset) * 7 * activeWindow.devicePixelRatio); + // TODO: Support non-standard character widths + screenAbsoluteX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * activeWindow.devicePixelRatio); screenAbsoluteY = ( Math.ceil(( // Top of line including line height diff --git a/src/vs/editor/browser/view/gpu/gpu.ts b/src/vs/editor/browser/view/gpu/gpu.ts index 743382aeac4..16635f1c16d 100644 --- a/src/vs/editor/browser/view/gpu/gpu.ts +++ b/src/vs/editor/browser/view/gpu/gpu.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { ViewLineOptions } from 'vs/editor/browser/viewParts/lines/viewLineOptions'; import type { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; export const enum BindingId { @@ -20,6 +21,6 @@ export interface IGpuRenderStrategy { readonly wgsl: string; readonly bindGroupEntries: GPUBindGroupEntry[]; - update(viewportData: ViewportData): number; + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; draw?(pass: GPURenderPassEncoder, viewportData: ViewportData): void; } diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index 36bee06b074..b184b249d0c 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -386,7 +386,9 @@ export class ViewLinesGpu extends ViewPart { } private _renderText(viewportData: ViewportData): void { - const visibleObjectCount = this._renderStrategy.update(viewportData); + const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); + + const visibleObjectCount = this._renderStrategy.update(viewportData, options); this._updateAtlasStorageBufferAndTexture(); From 8d21bffd81e37f1afb43d1e3a632a278e9449658 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:16:23 -0700 Subject: [PATCH 134/286] Create viewGpuContext stub --- src/vs/editor/browser/view.ts | 12 ++++++------ src/vs/editor/browser/view/gpu/viewGpuContext.ts | 15 +++++++++++++++ .../editor/browser/viewParts/gpu/viewLinesGpu.ts | 11 ++++++++--- 3 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 src/vs/editor/browser/view/gpu/viewGpuContext.ts diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index eff192b87c9..f54c971fe78 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -15,6 +15,7 @@ import { PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouse import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { IVisibleRangeProvider, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; import { IContentWidget, IContentWidgetPosition, IEditorAriaOptions, IGlyphMarginWidget, IGlyphMarginWidgetPosition, IMouseTarget, IOverlayWidget, IOverlayWidgetPosition, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; +import { ViewGpuContext } from 'vs/editor/browser/view/gpu/viewGpuContext'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; @@ -77,6 +78,7 @@ export class View extends ViewEventHandler { private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; + private readonly _viewGpuContext: ViewGpuContext; private _selections: Selection[]; // The view lines @@ -96,7 +98,6 @@ export class View extends ViewEventHandler { // Dom nodes private readonly _linesContent: FastDomNode; - private readonly _canvas: FastDomNode; public readonly domNode: FastDomNode; private readonly _overflowGuardContainer: FastDomNode; @@ -136,14 +137,13 @@ export class View extends ViewEventHandler { this._linesContent.setClassName('lines-content' + ' monaco-editor-background'); this._linesContent.setPosition('absolute'); - this._canvas = createFastDomNode(document.createElement('canvas')); - this._canvas.setClassName('editorCanvas'); - this.domNode = createFastDomNode(document.createElement('div')); this.domNode.setClassName(this._getEditorClassName()); // Set role 'code' for better screen reader support https://github.com/microsoft/vscode/issues/93438 this.domNode.setAttribute('role', 'code'); + this._viewGpuContext = new ViewGpuContext(); + this._overflowGuardContainer = createFastDomNode(document.createElement('div')); PartFingerprints.write(this._overflowGuardContainer, PartFingerprint.OverflowGuard); this._overflowGuardContainer.setClassName('overflow-guard'); @@ -153,7 +153,7 @@ export class View extends ViewEventHandler { // View Lines this._viewLines = this._instantiationService.createInstance(ViewLines, this._context, this._linesContent); - this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._canvas.domNode); + this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext); // View Zones this._viewZones = new ViewZones(this._context); @@ -227,7 +227,7 @@ export class View extends ViewEventHandler { this._linesContent.appendChild(this._viewCursors.getDomNode()); this._overflowGuardContainer.appendChild(margin.getDomNode()); this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); - this._overflowGuardContainer.appendChild(this._canvas); + this._overflowGuardContainer.appendChild(this._viewGpuContext.canvas); this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); this._overflowGuardContainer.appendChild(this._textAreaHandler.textArea); this._overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover); diff --git a/src/vs/editor/browser/view/gpu/viewGpuContext.ts b/src/vs/editor/browser/view/gpu/viewGpuContext.ts new file mode 100644 index 00000000000..365902f673a --- /dev/null +++ b/src/vs/editor/browser/view/gpu/viewGpuContext.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createFastDomNode, type FastDomNode } from 'vs/base/browser/fastDomNode'; + +export class ViewGpuContext { + readonly canvas: FastDomNode; + + constructor() { + this.canvas = createFastDomNode(document.createElement('canvas')); + this.canvas.setClassName('editorCanvas'); + } +} diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index b184b249d0c..dff066a7bb6 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -20,6 +20,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { FullFileRenderStrategy } from 'vs/editor/browser/view/gpu/fullFileRenderStrategy'; import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import type { ViewGpuContext } from 'vs/editor/browser/view/gpu/viewGpuContext'; export const disableNonGpuRendering = false; @@ -35,6 +36,8 @@ export class ViewLinesGpu extends ViewPart { private readonly _gpuCtx!: GPUCanvasContext; + private readonly canvas: HTMLCanvasElement; + private _device!: GPUDevice; private _renderPassDescriptor!: GPURenderPassDescriptor; private _renderPassColorAttachment!: GPURenderPassColorAttachment; @@ -55,23 +58,25 @@ export class ViewLinesGpu extends ViewPart { constructor( context: ViewContext, - private readonly canvas: HTMLCanvasElement, + private readonly viewGpuContext: ViewGpuContext, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { super(context); + this.canvas = this.viewGpuContext.canvas.domNode; + // TODO: Add canvas device pixel resize event to gpu context const win = getActiveWindow(); this.canvas.width = this.canvas.clientWidth * win.devicePixelRatio; this.canvas.height = this.canvas.clientHeight * win.devicePixelRatio; - this._register(observeDevicePixelDimensions(canvas, getActiveWindow(), (w, h) => { + this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => { this.canvas.width = w; this.canvas.height = h; // TODO: Request render, should this just call renderText with the last viewportData })); - this._gpuCtx = ensureNonNullable(canvas.getContext('webgpu')); + this._gpuCtx = ensureNonNullable(this.canvas.getContext('webgpu')); // TODO: It would be nice if the async part of this (requesting device) was done before // ViewLinesGpu was constructed From 7a8f74f7527b47d763a60732211ea107bc51bc65 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:24:48 -0700 Subject: [PATCH 135/286] Add gpu device and ctx onto viewgpucontext --- src/vs/editor/browser/view.ts | 1 + .../editor/browser/view/gpu/viewGpuContext.ts | 25 ++++++++++++++++++- .../browser/viewParts/gpu/viewLinesGpu.ts | 24 ++++++------------ 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index f54c971fe78..41684dec771 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -397,6 +397,7 @@ export class View extends ViewEventHandler { this._contentWidgets.overflowingContentWidgetsDomNode.domNode.remove(); this._context.removeEventHandler(this); + this._viewGpuContext.dispose(); this._viewLines.dispose(); this._viewLinesGpu.dispose(); diff --git a/src/vs/editor/browser/view/gpu/viewGpuContext.ts b/src/vs/editor/browser/view/gpu/viewGpuContext.ts index 365902f673a..4a29bf41cf5 100644 --- a/src/vs/editor/browser/view/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/view/gpu/viewGpuContext.ts @@ -3,13 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { getActiveWindow } from 'vs/base/browser/dom'; import { createFastDomNode, type FastDomNode } from 'vs/base/browser/fastDomNode'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; +import { ensureNonNullable, observeDevicePixelDimensions } from 'vs/editor/browser/view/gpu/gpuUtils'; -export class ViewGpuContext { +export class ViewGpuContext extends Disposable { readonly canvas: FastDomNode; + readonly ctx: GPUCanvasContext; + + readonly device: Promise; + + private readonly _onDidChangeCanvasDevicePixelDimensions = this._register(new Emitter<{ width: number; height: number }>()); + readonly onDidChangeCanvasDevicePixelDimensions = this._onDidChangeCanvasDevicePixelDimensions.event; constructor() { + super(); + this.canvas = createFastDomNode(document.createElement('canvas')); this.canvas.setClassName('editorCanvas'); + + this.ctx = ensureNonNullable(this.canvas.domNode.getContext('webgpu')); + + this.device = GPULifecycle.requestDevice().then(ref => this._register(ref).object); + + this._register(observeDevicePixelDimensions(this.canvas.domNode, getActiveWindow(), (width, height) => { + this.canvas.domNode.width = width; + this.canvas.domNode.height = height; + this._onDidChangeCanvasDevicePixelDimensions.fire({ width, height }); + })); } } diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index dff066a7bb6..a766a89b049 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -12,7 +12,7 @@ import { ViewLinesChangedEvent, ViewScrollChangedEvent } from 'vs/editor/common/ import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { ViewLineOptions } from '../lines/viewLineOptions'; -import { ensureNonNullable, observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; import { getActiveWindow } from 'vs/base/browser/dom'; import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -34,8 +34,6 @@ const enum GlyphStorageBufferInfo { export class ViewLinesGpu extends ViewPart { - private readonly _gpuCtx!: GPUCanvasContext; - private readonly canvas: HTMLCanvasElement; private _device!: GPUDevice; @@ -58,26 +56,18 @@ export class ViewLinesGpu extends ViewPart { constructor( context: ViewContext, - private readonly viewGpuContext: ViewGpuContext, + private readonly _viewGpuContext: ViewGpuContext, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { super(context); - this.canvas = this.viewGpuContext.canvas.domNode; + this.canvas = this._viewGpuContext.canvas.domNode; - // TODO: Add canvas device pixel resize event to gpu context - const win = getActiveWindow(); - this.canvas.width = this.canvas.clientWidth * win.devicePixelRatio; - this.canvas.height = this.canvas.clientHeight * win.devicePixelRatio; - this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => { - this.canvas.width = w; - this.canvas.height = h; + this._register(this._viewGpuContext.onDidChangeCanvasDevicePixelDimensions(({ width, height }) => { // TODO: Request render, should this just call renderText with the last viewportData })); - this._gpuCtx = ensureNonNullable(this.canvas.getContext('webgpu')); - // TODO: It would be nice if the async part of this (requesting device) was done before // ViewLinesGpu was constructed this.initWebgpu(); @@ -86,10 +76,10 @@ export class ViewLinesGpu extends ViewPart { async initWebgpu() { // #region General - this._device = this._register(await GPULifecycle.requestDevice()).object; + this._device = await this._viewGpuContext.device; const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); - this._gpuCtx.configure({ + this._viewGpuContext.ctx.configure({ device: this._device, format: presentationFormat, alphaMode: 'premultiplied', @@ -399,7 +389,7 @@ export class ViewLinesGpu extends ViewPart { const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' }); - this._renderPassColorAttachment.view = this._gpuCtx.getCurrentTexture().createView({ label: 'Monaco canvas texture view' }); + this._renderPassColorAttachment.view = this._viewGpuContext.ctx.getCurrentTexture().createView({ label: 'Monaco canvas texture view' }); const pass = encoder.beginRenderPass(this._renderPassDescriptor); pass.setPipeline(this._pipeline); pass.setVertexBuffer(0, this._vertexBuffer); From 27d6205a0454169930b0a609701bd286f5c321d8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:12:10 -0700 Subject: [PATCH 136/286] Reduce diff outside view part/gpu --- src/tsconfig.json | 6 +++--- src/vs/editor/browser/view.ts | 8 +------- src/vs/editor/browser/view/viewOverlays.ts | 19 ------------------- .../browser/viewParts/gpu/viewLinesGpu.ts | 2 -- .../browser/stickyScrollWidget.ts | 11 +++-------- 5 files changed, 7 insertions(+), 39 deletions(-) diff --git a/src/tsconfig.json b/src/tsconfig.json index 6d25031233e..a99e1814479 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -10,13 +10,13 @@ "isolatedModules": true, "outDir": "../out/vs", "types": [ + "@webgpu/types", "mocha", "semver", "sinon", - "winreg", "trusted-types", - "wicg-file-system-access", - "@webgpu/types" + "winreg", + "wicg-file-system-access" ], "plugins": [ { diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 41684dec771..e57e52e62c4 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -27,7 +27,7 @@ import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from ' import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations'; import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar'; import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; -import { disableNonGpuRendering, ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides'; import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers'; import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines'; @@ -438,18 +438,12 @@ export class View extends ViewEventHandler { if (this._store.isDisposed) { throw new BugIndicatingError(); } - if (disableNonGpuRendering) { - return; - } return rendering.prepareRender(viewParts, ctx); }, render: (viewParts: ViewPart[], ctx: RestrictedRenderingContext) => { if (this._store.isDisposed) { throw new BugIndicatingError(); } - if (disableNonGpuRendering) { - return; - } return rendering.render(viewParts, ctx); } }); diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 189ff3daeba..31c84238d34 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -15,7 +15,6 @@ import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { disableNonGpuRendering } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; export class ViewOverlays extends ViewPart { private readonly _visibleLines: VisibleLinesCollection; @@ -114,9 +113,6 @@ export class ViewOverlays extends ViewPart { // ----- end event handlers public prepareRender(ctx: RenderingContext): void { - if (disableNonGpuRendering) { - return; - } const toRender = this._dynamicOverlays.filter(overlay => overlay.shouldRender()); for (let i = 0, len = toRender.length; i < len; i++) { @@ -127,9 +123,6 @@ export class ViewOverlays extends ViewPart { } public render(ctx: RestrictedRenderingContext): void { - if (disableNonGpuRendering) { - return; - } // Overwriting to bypass `shouldRender` flag this._viewOverlaysRender(ctx); @@ -137,9 +130,6 @@ export class ViewOverlays extends ViewPart { } _viewOverlaysRender(ctx: RestrictedRenderingContext): void { - if (disableNonGpuRendering) { - return; - } this._visibleLines.renderLines(ctx.viewportData, true); } } @@ -175,9 +165,6 @@ export class ViewOverlayLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { - if (disableNonGpuRendering) { - return false; - } let result = ''; for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) { const dynamicOverlay = this._dynamicOverlays[i]; @@ -242,9 +229,6 @@ export class ContentViewOverlays extends ViewOverlays { override _viewOverlaysRender(ctx: RestrictedRenderingContext): void { super._viewOverlaysRender(ctx); - if (disableNonGpuRendering) { - return; - } this.domNode.setWidth(Math.max(ctx.scrollWidth, this._contentWidth)); } @@ -284,9 +268,6 @@ export class MarginViewOverlays extends ViewOverlays { override _viewOverlaysRender(ctx: RestrictedRenderingContext): void { super._viewOverlaysRender(ctx); - if (disableNonGpuRendering) { - return; - } const height = Math.min(ctx.scrollHeight, 1000000); this.domNode.setHeight(height); this.domNode.setWidth(this._contentLeft); diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index a766a89b049..2fbf8aeb41f 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -22,8 +22,6 @@ import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasP import { EditorOption } from 'vs/editor/common/config/editorOptions'; import type { ViewGpuContext } from 'vs/editor/browser/view/gpu/viewGpuContext'; -export const disableNonGpuRendering = false; - const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, BytesPerEntry = GlyphStorageBufferInfo.FloatsPerEntry * 4, diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index cf7fb935e58..969688c9832 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -10,7 +10,6 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; -import { disableNonGpuRendering } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -80,10 +79,8 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { this._rootDomNode.className = 'sticky-widget'; this._rootDomNode.classList.toggle('peek', _editor instanceof EmbeddedCodeEditorWidget); - if (!disableNonGpuRendering) { - this._rootDomNode.appendChild(this._lineNumbersDomNode); - this._rootDomNode.appendChild(this._linesDomNodeScrollable); - } + this._rootDomNode.appendChild(this._lineNumbersDomNode); + this._rootDomNode.appendChild(this._linesDomNodeScrollable); const updateScrollLeftPosition = () => { this._linesDomNode.style.left = this._editor.getOption(EditorOption.stickyScroll).scrollWithEditor ? `-${this._editor.getScrollLeft()}px` : '0px'; @@ -143,9 +140,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { const state = isWidgetHeightZero ? undefined : _state; const rebuildFromLine = isWidgetHeightZero ? 0 : this._findLineToRebuildWidgetFrom(_state, _rebuildFromLine); - if (!disableNonGpuRendering) { - this._renderRootNode(state, foldingModel, rebuildFromLine); - } + this._renderRootNode(state, foldingModel, rebuildFromLine); this._previousState = _state; } From c13bf51e60ee8ca5f00536ed603e93fe5761b16a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:19:46 -0700 Subject: [PATCH 137/286] Further reduce diff --- src/vs/editor/browser/view.ts | 2 +- src/vs/editor/browser/view/viewLayer.ts | 23 ++----------------- src/vs/editor/browser/view/viewOverlays.ts | 2 +- .../browser/viewParts/lines/viewLines.ts | 16 +++++-------- 4 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index e57e52e62c4..dc6ddae6b0a 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -152,7 +152,7 @@ export class View extends ViewEventHandler { this._viewParts.push(this._scrollbar); // View Lines - this._viewLines = this._instantiationService.createInstance(ViewLines, this._context, this._linesContent); + this._viewLines = new ViewLines(this._context, this._linesContent); this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext); // View Zones diff --git a/src/vs/editor/browser/view/viewLayer.ts b/src/vs/editor/browser/view/viewLayer.ts index 84f730c895e..308e0ff54d9 100644 --- a/src/vs/editor/browser/view/viewLayer.ts +++ b/src/vs/editor/browser/view/viewLayer.ts @@ -254,14 +254,9 @@ export class VisibleLinesCollection { public readonly domNode: FastDomNode = this._createDomNode(); private readonly _linesCollection: RenderedLinesCollection = new RenderedLinesCollection(this._lineFactory); - private readonly _canvas: HTMLCanvasElement; - constructor( private readonly _lineFactory: ILineFactory ) { - this._canvas = document.createElement('canvas'); - this._canvas.style.height = '100%'; - this._canvas.style.width = '100%'; } private _createDomNode(): FastDomNode { @@ -344,24 +339,11 @@ export class VisibleLinesCollection { return this._linesCollection.getLine(lineNumber); } - public renderLines(viewportData: ViewportData, viewOverlays?: boolean): void { + public renderLines(viewportData: ViewportData): void { + const inp = this._linesCollection._get(); - // let renderer; - // if (viewOverlays) { const renderer = new ViewLayerRenderer(this.domNode.domNode, this._lineFactory, viewportData); - // } else { - // // If not yet attached, listen for device pixel size and attach - // if (!this._canvas.parentElement) { - // this.domNode.domNode.appendChild(this._canvas); - // } - - // if (!this._gpuRenderer) { - // this._gpuRenderer = this._register(this._instantiationService.createInstance(GpuViewLayerRenderer, this._canvas, this._context, this._lineFactory, viewportData)); - // } - // renderer = this._gpuRenderer; - // renderer.update(viewportData); - // } const ctx: IRendererContext = { rendLineNumberStart: inp.rendLineNumberStart, @@ -624,4 +606,3 @@ class ViewLayerRenderer { } } } - diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 31c84238d34..dd76a270cd7 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -130,7 +130,7 @@ export class ViewOverlays extends ViewPart { } _viewOverlaysRender(ctx: RestrictedRenderingContext): void { - this._visibleLines.renderLines(ctx.viewportData, true); + this._visibleLines.renderLines(ctx.viewportData); } } diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 421680d29ed..3d6d6b7dac7 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -25,7 +25,6 @@ import * as viewEvents from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { Viewport } from 'vs/editor/common/viewModel'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; class LastRenderedData { @@ -122,11 +121,7 @@ export class ViewLines extends ViewPart implements IViewLines { private _stickyScrollEnabled: boolean; private _maxNumberStickyLines: number; - constructor( - context: ViewContext, - linesContent: FastDomNode, - @IInstantiationService instantiationService: IInstantiationService - ) { + constructor(context: ViewContext, linesContent: FastDomNode) { super(context); const conf = this._context.configuration; @@ -606,8 +601,8 @@ export class ViewLines extends ViewPart implements IViewLines { // (1) render lines - ensures lines are in the DOM this._visibleLines.renderLines(viewportData); this._lastRenderedData.setCurrentVisibleRange(viewportData.visibleRange); - this.domNode.setWidth(this._context.viewLayout.getCurrentViewport().width); - this.domNode.setHeight(Math.min(this._context.viewLayout.getCurrentViewport().height, 1000000)); + this.domNode.setWidth(this._context.viewLayout.getScrollWidth()); + this.domNode.setHeight(Math.min(this._context.viewLayout.getScrollHeight(), 1000000)); // (2) compute horizontal scroll position: // - this must happen after the lines are in the DOM since it might need a line that rendered just now @@ -663,8 +658,9 @@ export class ViewLines extends ViewPart implements IViewLines { // (3) handle scrolling this._linesContent.setLayerHinting(this._canUseLayerHinting); this._linesContent.setContain('strict'); - // this._linesContent.setTop(-adjustedScrollTop); - // this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft()); + const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta; + this._linesContent.setTop(-adjustedScrollTop); + this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft()); } // --- width From ae53297bf2d73ec443a598ce553ff6ea1a26c139 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:25:10 -0700 Subject: [PATCH 138/286] Move gpu/ up one level --- .../editor/browser/{view => }/gpu/atlas/atlas.ts | 2 +- .../browser/{view => }/gpu/atlas/textureAtlas.ts | 10 +++++----- .../{view => }/gpu/atlas/textureAtlasPage.ts | 8 ++++---- .../gpu/atlas/textureAtlasShelfAllocator.ts | 6 +++--- .../gpu/atlas/textureAtlasSlabAllocator.ts | 6 +++--- .../{view => }/gpu/fullFileRenderStrategy.ts | 12 ++++++------ src/vs/editor/browser/{view => }/gpu/gpu.ts | 0 .../editor/browser/{view => }/gpu/gpuDisposable.ts | 0 src/vs/editor/browser/{view => }/gpu/gpuUtils.ts | 0 .../{view => }/gpu/objectCollectionBuffer.ts | 0 .../{view => }/gpu/raster/glyphRasterizer.ts | 4 ++-- .../editor/browser/{view => }/gpu/raster/raster.ts | 0 src/vs/editor/browser/{view => }/gpu/taskQueue.ts | 0 .../browser/{view => }/gpu/viewGpuContext.ts | 4 ++-- src/vs/editor/browser/view.ts | 2 +- .../editor/browser/viewParts/gpu/viewLinesGpu.ts | 14 +++++++------- src/vs/editor/contrib/gpu/browser/gpuActions.ts | 4 ++-- .../editor/test/browser/view/gpu/atlas/testUtil.ts | 6 +++--- .../browser/view/gpu/atlas/textureAtlas.test.ts | 8 ++++---- .../view/gpu/atlas/textureAtlasAllocator.test.ts | 10 +++++----- .../view/gpu/objectCollectionBuffer.test.ts | 2 +- 21 files changed, 49 insertions(+), 49 deletions(-) rename src/vs/editor/browser/{view => }/gpu/atlas/atlas.ts (98%) rename src/vs/editor/browser/{view => }/gpu/atlas/textureAtlas.ts (95%) rename src/vs/editor/browser/{view => }/gpu/atlas/textureAtlasPage.ts (93%) rename src/vs/editor/browser/{view => }/gpu/atlas/textureAtlasShelfAllocator.ts (95%) rename src/vs/editor/browser/{view => }/gpu/atlas/textureAtlasSlabAllocator.ts (98%) rename src/vs/editor/browser/{view => }/gpu/fullFileRenderStrategy.ts (97%) rename src/vs/editor/browser/{view => }/gpu/gpu.ts (100%) rename src/vs/editor/browser/{view => }/gpu/gpuDisposable.ts (100%) rename src/vs/editor/browser/{view => }/gpu/gpuUtils.ts (100%) rename src/vs/editor/browser/{view => }/gpu/objectCollectionBuffer.ts (100%) rename src/vs/editor/browser/{view => }/gpu/raster/glyphRasterizer.ts (98%) rename src/vs/editor/browser/{view => }/gpu/raster/raster.ts (100%) rename src/vs/editor/browser/{view => }/gpu/taskQueue.ts (100%) rename src/vs/editor/browser/{view => }/gpu/viewGpuContext.ts (93%) diff --git a/src/vs/editor/browser/view/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts similarity index 98% rename from src/vs/editor/browser/view/gpu/atlas/atlas.ts rename to src/vs/editor/browser/gpu/atlas/atlas.ts index b8b4fbf9c8c..a1b3457be92 100644 --- a/src/vs/editor/browser/view/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IBoundingBox, IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; +import type { IBoundingBox, IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; /** * Information about a {@link IRasterizedGlyph rasterized glyph} that has been drawn to a texture diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts similarity index 95% rename from src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts rename to src/vs/editor/browser/gpu/atlas/textureAtlas.ts index b176e01fa89..502dfc29ab9 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -7,11 +7,11 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { TwoKeyMap } from 'vs/base/common/map'; -import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; -import { TextureAtlasPage, type AllocatorType } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; -import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; -import type { IGlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/raster'; -import { IdleTaskQueue } from 'vs/editor/browser/view/gpu/taskQueue'; +import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; +import { TextureAtlasPage, type AllocatorType } from 'vs/editor/browser/gpu/atlas/textureAtlasPage'; +import { GlyphRasterizer } from 'vs/editor/browser/gpu/raster/glyphRasterizer'; +import type { IGlyphRasterizer } from 'vs/editor/browser/gpu/raster/raster'; +import { IdleTaskQueue } from 'vs/editor/browser/gpu/taskQueue'; import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts similarity index 93% rename from src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts rename to src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index a721307beea..54c21ab6812 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -6,10 +6,10 @@ import { Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { TwoKeyMap } from 'vs/base/common/map'; -import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; -import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; -import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; -import type { IBoundingBox, IGlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/raster'; +import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; +import { TextureAtlasShelfAllocator } from 'vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator'; +import { TextureAtlasSlabAllocator } from 'vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator'; +import type { IBoundingBox, IGlyphRasterizer } from 'vs/editor/browser/gpu/raster/raster'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IThemeService } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts similarity index 95% rename from src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts rename to src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts index 247cfa960b7..bda3815c5d0 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; +import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; +import type { IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; /** * The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the diff --git a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts similarity index 98% rename from src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts rename to src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index a06c13739d0..98863a44fbc 100644 --- a/src/vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -5,9 +5,9 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { TwoKeyMap } from 'vs/base/common/map'; -import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; +import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; +import type { IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; export interface TextureAtlasSlabAllocatorOptions { slabW?: number; diff --git a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts similarity index 97% rename from src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts rename to src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 34ef96c741d..e35713435b3 100644 --- a/src/vs/editor/browser/view/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -5,12 +5,12 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; -import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; -import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; -import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; -import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; -import { quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; +import { TextureAtlas } from 'vs/editor/browser/gpu/atlas/textureAtlas'; +import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/gpu/gpu'; +import { GPULifecycle } from 'vs/editor/browser/gpu/gpuDisposable'; +import { quadVertices } from 'vs/editor/browser/gpu/gpuUtils'; +import { GlyphRasterizer } from 'vs/editor/browser/gpu/raster/glyphRasterizer'; import type { ViewLineOptions } from 'vs/editor/browser/viewParts/lines/viewLineOptions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; diff --git a/src/vs/editor/browser/view/gpu/gpu.ts b/src/vs/editor/browser/gpu/gpu.ts similarity index 100% rename from src/vs/editor/browser/view/gpu/gpu.ts rename to src/vs/editor/browser/gpu/gpu.ts diff --git a/src/vs/editor/browser/view/gpu/gpuDisposable.ts b/src/vs/editor/browser/gpu/gpuDisposable.ts similarity index 100% rename from src/vs/editor/browser/view/gpu/gpuDisposable.ts rename to src/vs/editor/browser/gpu/gpuDisposable.ts diff --git a/src/vs/editor/browser/view/gpu/gpuUtils.ts b/src/vs/editor/browser/gpu/gpuUtils.ts similarity index 100% rename from src/vs/editor/browser/view/gpu/gpuUtils.ts rename to src/vs/editor/browser/gpu/gpuUtils.ts diff --git a/src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/gpu/objectCollectionBuffer.ts similarity index 100% rename from src/vs/editor/browser/view/gpu/objectCollectionBuffer.ts rename to src/vs/editor/browser/gpu/objectCollectionBuffer.ts diff --git a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts similarity index 98% rename from src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts rename to src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index d2e54e81fb8..d739eaf4114 100644 --- a/src/vs/editor/browser/view/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; +import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; +import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; import { FontStyle, TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; diff --git a/src/vs/editor/browser/view/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts similarity index 100% rename from src/vs/editor/browser/view/gpu/raster/raster.ts rename to src/vs/editor/browser/gpu/raster/raster.ts diff --git a/src/vs/editor/browser/view/gpu/taskQueue.ts b/src/vs/editor/browser/gpu/taskQueue.ts similarity index 100% rename from src/vs/editor/browser/view/gpu/taskQueue.ts rename to src/vs/editor/browser/gpu/taskQueue.ts diff --git a/src/vs/editor/browser/view/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts similarity index 93% rename from src/vs/editor/browser/view/gpu/viewGpuContext.ts rename to src/vs/editor/browser/gpu/viewGpuContext.ts index 4a29bf41cf5..4810412e3be 100644 --- a/src/vs/editor/browser/view/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -7,8 +7,8 @@ import { getActiveWindow } from 'vs/base/browser/dom'; import { createFastDomNode, type FastDomNode } from 'vs/base/browser/fastDomNode'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; -import { ensureNonNullable, observeDevicePixelDimensions } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { GPULifecycle } from 'vs/editor/browser/gpu/gpuDisposable'; +import { ensureNonNullable, observeDevicePixelDimensions } from 'vs/editor/browser/gpu/gpuUtils'; export class ViewGpuContext extends Disposable { readonly canvas: FastDomNode; diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index dc6ddae6b0a..ed78364c578 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -15,7 +15,7 @@ import { PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouse import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { IVisibleRangeProvider, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; import { IContentWidget, IContentWidgetPosition, IEditorAriaOptions, IGlyphMarginWidget, IGlyphMarginWidgetPosition, IMouseTarget, IOverlayWidget, IOverlayWidgetPosition, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; -import { ViewGpuContext } from 'vs/editor/browser/view/gpu/viewGpuContext'; +import { ViewGpuContext } from 'vs/editor/browser/gpu/viewGpuContext'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts index 2fbf8aeb41f..d6c5e833f7c 100644 --- a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts @@ -4,23 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from 'vs/base/common/errors'; -import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; -import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/view/gpu/gpu'; +import { TextureAtlas } from 'vs/editor/browser/gpu/atlas/textureAtlas'; +import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/gpu/gpu'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { ViewLinesChangedEvent, ViewScrollChangedEvent } from 'vs/editor/common/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { ViewLineOptions } from '../lines/viewLineOptions'; -import { observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/view/gpu/gpuUtils'; +import { observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/gpu/gpuUtils'; import { getActiveWindow } from 'vs/base/browser/dom'; -import { GPULifecycle } from 'vs/editor/browser/view/gpu/gpuDisposable'; +import { GPULifecycle } from 'vs/editor/browser/gpu/gpuDisposable'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { FullFileRenderStrategy } from 'vs/editor/browser/view/gpu/fullFileRenderStrategy'; -import { TextureAtlasPage } from 'vs/editor/browser/view/gpu/atlas/textureAtlasPage'; +import { FullFileRenderStrategy } from 'vs/editor/browser/gpu/fullFileRenderStrategy'; +import { TextureAtlasPage } from 'vs/editor/browser/gpu/atlas/textureAtlasPage'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import type { ViewGpuContext } from 'vs/editor/browser/view/gpu/viewGpuContext'; +import type { ViewGpuContext } from 'vs/editor/browser/gpu/viewGpuContext'; const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 6699da06c68..2f8c9db9552 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -7,8 +7,8 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, type ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import { GlyphRasterizer } from 'vs/editor/browser/view/gpu/raster/glyphRasterizer'; +import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; +import { GlyphRasterizer } from 'vs/editor/browser/gpu/raster/glyphRasterizer'; import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts b/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts index bb3d3c2b509..8ff88f9a04a 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts @@ -5,9 +5,9 @@ import { fail, ok } from 'assert'; import { isNumber } from 'vs/base/common/types'; -import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/view/gpu/atlas/atlas'; -import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; +import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; +import { TextureAtlas } from 'vs/editor/browser/gpu/atlas/textureAtlas'; +import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; export function assertIsValidGlyph(glyph: Readonly | undefined, atlasOrSource: TextureAtlas | OffscreenCanvas) { if (glyph === undefined) { diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index c617a66001b..b2181d3c2c6 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -5,10 +5,10 @@ import { strictEqual, throws } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { TextureAtlas } from 'vs/editor/browser/view/gpu/atlas/textureAtlas'; -import { TextureAtlasSlabAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; +import { TextureAtlas } from 'vs/editor/browser/gpu/atlas/textureAtlas'; +import { TextureAtlasSlabAllocator } from 'vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator'; +import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; +import type { IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; import { createCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; import { assertIsValidGlyph } from 'vs/editor/test/browser/view/gpu/atlas/testUtil'; import type { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 9036c90759e..140610e4286 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -5,11 +5,11 @@ import { deepStrictEqual, strictEqual, throws } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import type { ITextureAtlasAllocator } from 'vs/editor/browser/view/gpu/atlas/atlas'; -import { TextureAtlasShelfAllocator } from 'vs/editor/browser/view/gpu/atlas/textureAtlasShelfAllocator'; -import { TextureAtlasSlabAllocator, TextureAtlasSlabAllocatorOptions } from 'vs/editor/browser/view/gpu/atlas/textureAtlasSlabAllocator'; -import { ensureNonNullable } from 'vs/editor/browser/view/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/view/gpu/raster/raster'; +import type { ITextureAtlasAllocator } from 'vs/editor/browser/gpu/atlas/atlas'; +import { TextureAtlasShelfAllocator } from 'vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator'; +import { TextureAtlasSlabAllocator, TextureAtlasSlabAllocatorOptions } from 'vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator'; +import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; +import type { IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; import { assertIsValidGlyph } from 'vs/editor/test/browser/view/gpu/atlas/testUtil'; const blackArr = [0x00, 0x00, 0x00, 0xFF]; diff --git a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts index f70572ec735..d7ef616fe24 100644 --- a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts +++ b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts @@ -5,7 +5,7 @@ import { deepStrictEqual, strictEqual, throws } from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from 'vs/editor/browser/view/gpu/objectCollectionBuffer'; +import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from 'vs/editor/browser/gpu/objectCollectionBuffer'; suite('ObjectCollectionBuffer', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); From 19862244b0fb061b9b6019904b38e877db0801a0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:27:25 -0700 Subject: [PATCH 139/286] Move gpu/ to linesGpu/ Matches lines/ which contains related viewLines.ts --- src/vs/editor/browser/view.ts | 2 +- src/vs/editor/browser/viewParts/lines/viewLine.ts | 2 +- .../editor/browser/viewParts/{gpu => linesGpu}/viewLinesGpu.ts | 0 src/vs/editor/contrib/gpu/browser/gpuActions.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/vs/editor/browser/viewParts/{gpu => linesGpu}/viewLinesGpu.ts (100%) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index ed78364c578..6accfb2ac98 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -27,7 +27,7 @@ import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from ' import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations'; import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar'; import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; -import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides'; import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers'; import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines'; diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 573464c3192..abdea584317 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -17,7 +17,7 @@ import { InlineDecorationType } from 'vs/editor/common/viewModel'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { EditorFontLigatures } from 'vs/editor/common/config/editorOptions'; import { DomReadingContext } from 'vs/editor/browser/viewParts/lines/domReadingContext'; -import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; import { ViewLineOptions } from './viewLineOptions'; const canUseFastRenderedViewLine = (function () { diff --git a/src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts similarity index 100% rename from src/vs/editor/browser/viewParts/gpu/viewLinesGpu.ts rename to src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 2f8c9db9552..c7dbe267d6f 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -9,7 +9,7 @@ import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, type ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/gpu/raster/glyphRasterizer'; -import { ViewLinesGpu } from 'vs/editor/browser/viewParts/gpu/viewLinesGpu'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; From 35fd24ffbf64bfce0c669fa5973e38fb4acdb1d5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:43:23 -0700 Subject: [PATCH 140/286] Add gpu acceleration setting and use in most relevant places --- src/vs/editor/browser/view.ts | 25 +- .../browser/viewParts/lines/viewLine.ts | 3 +- src/vs/editor/common/config/editorOptions.ts | 18 ++ .../common/standalone/standaloneEnums.ts | 227 ++++++++--------- src/vs/monaco.d.ts | 233 +++++++++--------- 5 files changed, 270 insertions(+), 236 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 6accfb2ac98..1e64f03c769 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -27,11 +27,11 @@ import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from ' import { DecorationsOverlay } from 'vs/editor/browser/viewParts/decorations/decorations'; import { EditorScrollbar } from 'vs/editor/browser/viewParts/editorScrollbar/editorScrollbar'; import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; -import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; import { IndentGuidesOverlay } from 'vs/editor/browser/viewParts/indentGuides/indentGuides'; import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers'; import { ViewLines } from 'vs/editor/browser/viewParts/lines/viewLines'; import { LinesDecorationsOverlay } from 'vs/editor/browser/viewParts/linesDecorations/linesDecorations'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; import { Margin } from 'vs/editor/browser/viewParts/margin/margin'; import { MarginViewLineDecorationsOverlay } from 'vs/editor/browser/viewParts/marginDecorations/marginDecorations'; import { Minimap } from 'vs/editor/browser/viewParts/minimap/minimap'; @@ -78,12 +78,12 @@ export class View extends ViewEventHandler { private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; - private readonly _viewGpuContext: ViewGpuContext; + private readonly _viewGpuContext?: ViewGpuContext; private _selections: Selection[]; // The view lines private readonly _viewLines: ViewLines; - private readonly _viewLinesGpu: ViewLinesGpu; + private readonly _viewLinesGpu?: ViewLinesGpu; // These are parts, but we must do some API related calls on them, so we keep a reference private readonly _viewZones: ViewZones; @@ -142,7 +142,9 @@ export class View extends ViewEventHandler { // Set role 'code' for better screen reader support https://github.com/microsoft/vscode/issues/93438 this.domNode.setAttribute('role', 'code'); - this._viewGpuContext = new ViewGpuContext(); + if (this._context.configuration.options.get(EditorOption.experimentalGpuAcceleration) === 'on') { + this._viewGpuContext = new ViewGpuContext(); + } this._overflowGuardContainer = createFastDomNode(document.createElement('div')); PartFingerprints.write(this._overflowGuardContainer, PartFingerprint.OverflowGuard); @@ -153,7 +155,10 @@ export class View extends ViewEventHandler { // View Lines this._viewLines = new ViewLines(this._context, this._linesContent); - this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext); + this._viewParts.push(this._viewLines); + if (this._viewGpuContext) { + this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext); + } // View Zones this._viewZones = new ViewZones(this._context); @@ -227,7 +232,9 @@ export class View extends ViewEventHandler { this._linesContent.appendChild(this._viewCursors.getDomNode()); this._overflowGuardContainer.appendChild(margin.getDomNode()); this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); - this._overflowGuardContainer.appendChild(this._viewGpuContext.canvas); + if (this._viewGpuContext) { + this._overflowGuardContainer.appendChild(this._viewGpuContext.canvas); + } this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); this._overflowGuardContainer.appendChild(this._textAreaHandler.textArea); this._overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover); @@ -397,10 +404,10 @@ export class View extends ViewEventHandler { this._contentWidgets.overflowingContentWidgetsDomNode.domNode.remove(); this._context.removeEventHandler(this); - this._viewGpuContext.dispose(); + this._viewGpuContext?.dispose(); this._viewLines.dispose(); - this._viewLinesGpu.dispose(); + this._viewLinesGpu?.dispose(); // Destroy view parts for (const viewPart of this._viewParts) { @@ -514,7 +521,7 @@ export class View extends ViewEventHandler { viewPartsToRender = this._getViewPartsToRender(); } - if (this._viewLinesGpu.shouldRender()) { + if (this._viewLinesGpu?.shouldRender()) { this._viewLinesGpu.renderText(viewportData); this._viewLinesGpu.onDidRender(); } diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index abdea584317..ed32dc833f0 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -17,8 +17,8 @@ import { InlineDecorationType } from 'vs/editor/common/viewModel'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { EditorFontLigatures } from 'vs/editor/common/config/editorOptions'; import { DomReadingContext } from 'vs/editor/browser/viewParts/lines/domReadingContext'; -import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; import { ViewLineOptions } from './viewLineOptions'; +import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { @@ -98,6 +98,7 @@ export class ViewLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { + // TODO: Only do this if the experimentalGpuAcceleration setting is 'on'. if (ViewLinesGpu.canRender(this._options, viewportData, lineNumber)) { this._renderedViewLine?.domNode?.domNode.remove(); this._renderedViewLine = null; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 0c099aa07e3..2d7c37afbc6 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -634,6 +634,11 @@ export interface IEditorOptions { * Defaults to 'always'. */ matchBrackets?: 'never' | 'near' | 'always'; + /** + * Enable experimental rendering using WebGPU. + * Defaults to 'off'. + */ + experimentalGpuAcceleration?: 'on' | 'off'; /** * Enable experimental whitespace rendering. * Defaults to 'svg'. @@ -5353,6 +5358,7 @@ export const enum EditorOption { dragAndDrop, dropIntoEditor, emptySelectionClipboard, + experimentalGpuAcceleration, experimentalWhitespaceRendering, extraEditorClassName, fastScrollSensitivity, @@ -5723,6 +5729,18 @@ export const EditorOptions = { emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), dropIntoEditor: register(new EditorDropIntoEditor()), stickyScroll: register(new EditorStickyScroll()), + experimentalGpuAcceleration: register(new EditorStringEnumOption( + EditorOption.experimentalGpuAcceleration, 'experimentalGpuAcceleration', + 'on' as 'on' | 'off', + ['on', 'off'] as const, + { + enumDescriptions: [ + nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), + nls.localize('experimentalGpuAcceleration.off', "Use regular DOM-based rendering."), + ], + description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the (very) experimental GPU acceleration to render the editor.") + } + )), experimentalWhitespaceRendering: register(new EditorStringEnumOption( EditorOption.experimentalWhitespaceRendering, 'experimentalWhitespaceRendering', 'svg' as 'svg' | 'font' | 'off', diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 7cd16daae8b..418d9055291 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -211,119 +211,120 @@ export enum EditorOption { dragAndDrop = 35, dropIntoEditor = 36, emptySelectionClipboard = 37, - experimentalWhitespaceRendering = 38, - extraEditorClassName = 39, - fastScrollSensitivity = 40, - find = 41, - fixedOverflowWidgets = 42, - folding = 43, - foldingStrategy = 44, - foldingHighlight = 45, - foldingImportsByDefault = 46, - foldingMaximumRegions = 47, - unfoldOnClickAfterEndOfLine = 48, - fontFamily = 49, - fontInfo = 50, - fontLigatures = 51, - fontSize = 52, - fontWeight = 53, - fontVariations = 54, - formatOnPaste = 55, - formatOnType = 56, - glyphMargin = 57, - gotoLocation = 58, - hideCursorInOverviewRuler = 59, - hover = 60, - inDiffEditor = 61, - inlineSuggest = 62, - inlineEdit = 63, - letterSpacing = 64, - lightbulb = 65, - lineDecorationsWidth = 66, - lineHeight = 67, - lineNumbers = 68, - lineNumbersMinChars = 69, - linkedEditing = 70, - links = 71, - matchBrackets = 72, - minimap = 73, - mouseStyle = 74, - mouseWheelScrollSensitivity = 75, - mouseWheelZoom = 76, - multiCursorMergeOverlapping = 77, - multiCursorModifier = 78, - multiCursorPaste = 79, - multiCursorLimit = 80, - occurrencesHighlight = 81, - overviewRulerBorder = 82, - overviewRulerLanes = 83, - padding = 84, - pasteAs = 85, - parameterHints = 86, - peekWidgetDefaultFocus = 87, - placeholder = 88, - definitionLinkOpensInPeek = 89, - quickSuggestions = 90, - quickSuggestionsDelay = 91, - readOnly = 92, - readOnlyMessage = 93, - renameOnType = 94, - renderControlCharacters = 95, - renderFinalNewline = 96, - renderLineHighlight = 97, - renderLineHighlightOnlyWhenFocus = 98, - renderValidationDecorations = 99, - renderWhitespace = 100, - revealHorizontalRightPadding = 101, - roundedSelection = 102, - rulers = 103, - scrollbar = 104, - scrollBeyondLastColumn = 105, - scrollBeyondLastLine = 106, - scrollPredominantAxis = 107, - selectionClipboard = 108, - selectionHighlight = 109, - selectOnLineNumbers = 110, - showFoldingControls = 111, - showUnused = 112, - snippetSuggestions = 113, - smartSelect = 114, - smoothScrolling = 115, - stickyScroll = 116, - stickyTabStops = 117, - stopRenderingLineAfter = 118, - suggest = 119, - suggestFontSize = 120, - suggestLineHeight = 121, - suggestOnTriggerCharacters = 122, - suggestSelection = 123, - tabCompletion = 124, - tabIndex = 125, - unicodeHighlighting = 126, - unusualLineTerminators = 127, - useShadowDOM = 128, - useTabStops = 129, - wordBreak = 130, - wordSegmenterLocales = 131, - wordSeparators = 132, - wordWrap = 133, - wordWrapBreakAfterCharacters = 134, - wordWrapBreakBeforeCharacters = 135, - wordWrapColumn = 136, - wordWrapOverride1 = 137, - wordWrapOverride2 = 138, - wrappingIndent = 139, - wrappingStrategy = 140, - showDeprecated = 141, - inlayHints = 142, - editorClassName = 143, - pixelRatio = 144, - tabFocusMode = 145, - layoutInfo = 146, - wrappingInfo = 147, - defaultColorDecorators = 148, - colorDecoratorsActivatedOn = 149, - inlineCompletionsAccessibilityVerbose = 150 + experimentalGpuAcceleration = 38, + experimentalWhitespaceRendering = 39, + extraEditorClassName = 40, + fastScrollSensitivity = 41, + find = 42, + fixedOverflowWidgets = 43, + folding = 44, + foldingStrategy = 45, + foldingHighlight = 46, + foldingImportsByDefault = 47, + foldingMaximumRegions = 48, + unfoldOnClickAfterEndOfLine = 49, + fontFamily = 50, + fontInfo = 51, + fontLigatures = 52, + fontSize = 53, + fontWeight = 54, + fontVariations = 55, + formatOnPaste = 56, + formatOnType = 57, + glyphMargin = 58, + gotoLocation = 59, + hideCursorInOverviewRuler = 60, + hover = 61, + inDiffEditor = 62, + inlineSuggest = 63, + inlineEdit = 64, + letterSpacing = 65, + lightbulb = 66, + lineDecorationsWidth = 67, + lineHeight = 68, + lineNumbers = 69, + lineNumbersMinChars = 70, + linkedEditing = 71, + links = 72, + matchBrackets = 73, + minimap = 74, + mouseStyle = 75, + mouseWheelScrollSensitivity = 76, + mouseWheelZoom = 77, + multiCursorMergeOverlapping = 78, + multiCursorModifier = 79, + multiCursorPaste = 80, + multiCursorLimit = 81, + occurrencesHighlight = 82, + overviewRulerBorder = 83, + overviewRulerLanes = 84, + padding = 85, + pasteAs = 86, + parameterHints = 87, + peekWidgetDefaultFocus = 88, + placeholder = 89, + definitionLinkOpensInPeek = 90, + quickSuggestions = 91, + quickSuggestionsDelay = 92, + readOnly = 93, + readOnlyMessage = 94, + renameOnType = 95, + renderControlCharacters = 96, + renderFinalNewline = 97, + renderLineHighlight = 98, + renderLineHighlightOnlyWhenFocus = 99, + renderValidationDecorations = 100, + renderWhitespace = 101, + revealHorizontalRightPadding = 102, + roundedSelection = 103, + rulers = 104, + scrollbar = 105, + scrollBeyondLastColumn = 106, + scrollBeyondLastLine = 107, + scrollPredominantAxis = 108, + selectionClipboard = 109, + selectionHighlight = 110, + selectOnLineNumbers = 111, + showFoldingControls = 112, + showUnused = 113, + snippetSuggestions = 114, + smartSelect = 115, + smoothScrolling = 116, + stickyScroll = 117, + stickyTabStops = 118, + stopRenderingLineAfter = 119, + suggest = 120, + suggestFontSize = 121, + suggestLineHeight = 122, + suggestOnTriggerCharacters = 123, + suggestSelection = 124, + tabCompletion = 125, + tabIndex = 126, + unicodeHighlighting = 127, + unusualLineTerminators = 128, + useShadowDOM = 129, + useTabStops = 130, + wordBreak = 131, + wordSegmenterLocales = 132, + wordSeparators = 133, + wordWrap = 134, + wordWrapBreakAfterCharacters = 135, + wordWrapBreakBeforeCharacters = 136, + wordWrapColumn = 137, + wordWrapOverride1 = 138, + wordWrapOverride2 = 139, + wrappingIndent = 140, + wrappingStrategy = 141, + showDeprecated = 142, + inlayHints = 143, + editorClassName = 144, + pixelRatio = 145, + tabFocusMode = 146, + layoutInfo = 147, + wrappingInfo = 148, + defaultColorDecorators = 149, + colorDecoratorsActivatedOn = 150, + inlineCompletionsAccessibilityVerbose = 151 } /** diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index be2c930e9eb..e26028840f9 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3680,6 +3680,11 @@ declare namespace monaco.editor { * Defaults to 'always'. */ matchBrackets?: 'never' | 'near' | 'always'; + /** + * Enable experimental rendering using WebGPU. + * Defaults to 'off'. + */ + experimentalGpuAcceleration?: 'on' | 'off'; /** * Enable experimental whitespace rendering. * Defaults to 'svg'. @@ -4897,119 +4902,120 @@ declare namespace monaco.editor { dragAndDrop = 35, dropIntoEditor = 36, emptySelectionClipboard = 37, - experimentalWhitespaceRendering = 38, - extraEditorClassName = 39, - fastScrollSensitivity = 40, - find = 41, - fixedOverflowWidgets = 42, - folding = 43, - foldingStrategy = 44, - foldingHighlight = 45, - foldingImportsByDefault = 46, - foldingMaximumRegions = 47, - unfoldOnClickAfterEndOfLine = 48, - fontFamily = 49, - fontInfo = 50, - fontLigatures = 51, - fontSize = 52, - fontWeight = 53, - fontVariations = 54, - formatOnPaste = 55, - formatOnType = 56, - glyphMargin = 57, - gotoLocation = 58, - hideCursorInOverviewRuler = 59, - hover = 60, - inDiffEditor = 61, - inlineSuggest = 62, - inlineEdit = 63, - letterSpacing = 64, - lightbulb = 65, - lineDecorationsWidth = 66, - lineHeight = 67, - lineNumbers = 68, - lineNumbersMinChars = 69, - linkedEditing = 70, - links = 71, - matchBrackets = 72, - minimap = 73, - mouseStyle = 74, - mouseWheelScrollSensitivity = 75, - mouseWheelZoom = 76, - multiCursorMergeOverlapping = 77, - multiCursorModifier = 78, - multiCursorPaste = 79, - multiCursorLimit = 80, - occurrencesHighlight = 81, - overviewRulerBorder = 82, - overviewRulerLanes = 83, - padding = 84, - pasteAs = 85, - parameterHints = 86, - peekWidgetDefaultFocus = 87, - placeholder = 88, - definitionLinkOpensInPeek = 89, - quickSuggestions = 90, - quickSuggestionsDelay = 91, - readOnly = 92, - readOnlyMessage = 93, - renameOnType = 94, - renderControlCharacters = 95, - renderFinalNewline = 96, - renderLineHighlight = 97, - renderLineHighlightOnlyWhenFocus = 98, - renderValidationDecorations = 99, - renderWhitespace = 100, - revealHorizontalRightPadding = 101, - roundedSelection = 102, - rulers = 103, - scrollbar = 104, - scrollBeyondLastColumn = 105, - scrollBeyondLastLine = 106, - scrollPredominantAxis = 107, - selectionClipboard = 108, - selectionHighlight = 109, - selectOnLineNumbers = 110, - showFoldingControls = 111, - showUnused = 112, - snippetSuggestions = 113, - smartSelect = 114, - smoothScrolling = 115, - stickyScroll = 116, - stickyTabStops = 117, - stopRenderingLineAfter = 118, - suggest = 119, - suggestFontSize = 120, - suggestLineHeight = 121, - suggestOnTriggerCharacters = 122, - suggestSelection = 123, - tabCompletion = 124, - tabIndex = 125, - unicodeHighlighting = 126, - unusualLineTerminators = 127, - useShadowDOM = 128, - useTabStops = 129, - wordBreak = 130, - wordSegmenterLocales = 131, - wordSeparators = 132, - wordWrap = 133, - wordWrapBreakAfterCharacters = 134, - wordWrapBreakBeforeCharacters = 135, - wordWrapColumn = 136, - wordWrapOverride1 = 137, - wordWrapOverride2 = 138, - wrappingIndent = 139, - wrappingStrategy = 140, - showDeprecated = 141, - inlayHints = 142, - editorClassName = 143, - pixelRatio = 144, - tabFocusMode = 145, - layoutInfo = 146, - wrappingInfo = 147, - defaultColorDecorators = 148, - colorDecoratorsActivatedOn = 149, - inlineCompletionsAccessibilityVerbose = 150 + experimentalGpuAcceleration = 38, + experimentalWhitespaceRendering = 39, + extraEditorClassName = 40, + fastScrollSensitivity = 41, + find = 42, + fixedOverflowWidgets = 43, + folding = 44, + foldingStrategy = 45, + foldingHighlight = 46, + foldingImportsByDefault = 47, + foldingMaximumRegions = 48, + unfoldOnClickAfterEndOfLine = 49, + fontFamily = 50, + fontInfo = 51, + fontLigatures = 52, + fontSize = 53, + fontWeight = 54, + fontVariations = 55, + formatOnPaste = 56, + formatOnType = 57, + glyphMargin = 58, + gotoLocation = 59, + hideCursorInOverviewRuler = 60, + hover = 61, + inDiffEditor = 62, + inlineSuggest = 63, + inlineEdit = 64, + letterSpacing = 65, + lightbulb = 66, + lineDecorationsWidth = 67, + lineHeight = 68, + lineNumbers = 69, + lineNumbersMinChars = 70, + linkedEditing = 71, + links = 72, + matchBrackets = 73, + minimap = 74, + mouseStyle = 75, + mouseWheelScrollSensitivity = 76, + mouseWheelZoom = 77, + multiCursorMergeOverlapping = 78, + multiCursorModifier = 79, + multiCursorPaste = 80, + multiCursorLimit = 81, + occurrencesHighlight = 82, + overviewRulerBorder = 83, + overviewRulerLanes = 84, + padding = 85, + pasteAs = 86, + parameterHints = 87, + peekWidgetDefaultFocus = 88, + placeholder = 89, + definitionLinkOpensInPeek = 90, + quickSuggestions = 91, + quickSuggestionsDelay = 92, + readOnly = 93, + readOnlyMessage = 94, + renameOnType = 95, + renderControlCharacters = 96, + renderFinalNewline = 97, + renderLineHighlight = 98, + renderLineHighlightOnlyWhenFocus = 99, + renderValidationDecorations = 100, + renderWhitespace = 101, + revealHorizontalRightPadding = 102, + roundedSelection = 103, + rulers = 104, + scrollbar = 105, + scrollBeyondLastColumn = 106, + scrollBeyondLastLine = 107, + scrollPredominantAxis = 108, + selectionClipboard = 109, + selectionHighlight = 110, + selectOnLineNumbers = 111, + showFoldingControls = 112, + showUnused = 113, + snippetSuggestions = 114, + smartSelect = 115, + smoothScrolling = 116, + stickyScroll = 117, + stickyTabStops = 118, + stopRenderingLineAfter = 119, + suggest = 120, + suggestFontSize = 121, + suggestLineHeight = 122, + suggestOnTriggerCharacters = 123, + suggestSelection = 124, + tabCompletion = 125, + tabIndex = 126, + unicodeHighlighting = 127, + unusualLineTerminators = 128, + useShadowDOM = 129, + useTabStops = 130, + wordBreak = 131, + wordSegmenterLocales = 132, + wordSeparators = 133, + wordWrap = 134, + wordWrapBreakAfterCharacters = 135, + wordWrapBreakBeforeCharacters = 136, + wordWrapColumn = 137, + wordWrapOverride1 = 138, + wordWrapOverride2 = 139, + wrappingIndent = 140, + wrappingStrategy = 141, + showDeprecated = 142, + inlayHints = 143, + editorClassName = 144, + pixelRatio = 145, + tabFocusMode = 146, + layoutInfo = 147, + wrappingInfo = 148, + defaultColorDecorators = 149, + colorDecoratorsActivatedOn = 150, + inlineCompletionsAccessibilityVerbose = 151 } export const EditorOptions: { @@ -5054,6 +5060,7 @@ declare namespace monaco.editor { emptySelectionClipboard: IEditorOption; dropIntoEditor: IEditorOption>>; stickyScroll: IEditorOption>>; + experimentalGpuAcceleration: IEditorOption; experimentalWhitespaceRendering: IEditorOption; extraEditorClassName: IEditorOption; fastScrollSensitivity: IEditorOption; From f94e3cd041d8d0bab6ea7d8493da093f3d104c70 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:50:04 -0700 Subject: [PATCH 141/286] Default gpu value to off --- src/vs/editor/browser/view.ts | 1 + src/vs/editor/common/config/editorOptions.ts | 6 +++--- src/vs/editor/contrib/gpu/browser/gpuActions.ts | 9 +++++---- src/vs/monaco.d.ts | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 1e64f03c769..11a509a2389 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -59,6 +59,7 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IColorTheme, getThemeTypeSelector } from 'vs/platform/theme/common/themeService'; + export interface IContentWidgetData { widget: IContentWidget; position: IContentWidgetPosition | null; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 2d7c37afbc6..fd8f862ff05 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -5731,12 +5731,12 @@ export const EditorOptions = { stickyScroll: register(new EditorStickyScroll()), experimentalGpuAcceleration: register(new EditorStringEnumOption( EditorOption.experimentalGpuAcceleration, 'experimentalGpuAcceleration', - 'on' as 'on' | 'off', - ['on', 'off'] as const, + 'off' as 'off' | 'on', + ['off', 'on'] as const, { enumDescriptions: [ - nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), nls.localize('experimentalGpuAcceleration.off', "Use regular DOM-based rendering."), + nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), ], description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the (very) experimental GPU acceleration to render the editor.") } diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index c7dbe267d6f..89233f14719 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -10,7 +10,6 @@ import { EditorAction, registerEditorAction, type ServicesAccessor } from 'vs/ed import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; import { GlyphRasterizer } from 'vs/editor/browser/gpu/raster/glyphRasterizer'; import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -19,6 +18,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +const precondition = ContextKeyExpr.equals('config:editor.experimentalGpuAcceleration', 'on'); + class LogTextureAtlasStatsAction extends EditorAction { constructor() { @@ -26,7 +27,7 @@ class LogTextureAtlasStatsAction extends EditorAction { id: 'editor.action.logTextureAtlasStats', label: localize('logTextureAtlasStats.label', "Log Texture Atlas States"), alias: 'Log Texture Atlas States', - precondition: ContextKeyExpr.and(EditorContextKeys.notInCompositeEditor), + precondition, }); } @@ -51,7 +52,7 @@ class SaveTextureAtlasAction extends EditorAction { id: 'editor.action.saveTextureAtlas', label: localize('saveTextureAtlas.label', "Save Texture Atlas"), alias: 'Save Texture Atlas', - precondition: ContextKeyExpr.and(EditorContextKeys.notInCompositeEditor), + precondition, }); } @@ -86,7 +87,7 @@ class DrawGlyphAction extends EditorAction { id: 'editor.action.drawGlyph', label: localize('drawGlyph.label', "Draw Glyph"), alias: 'Draw Glyph', - precondition: ContextKeyExpr.true(), + precondition, }); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e26028840f9..cbbaf7984d2 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5060,7 +5060,7 @@ declare namespace monaco.editor { emptySelectionClipboard: IEditorOption; dropIntoEditor: IEditorOption>>; stickyScroll: IEditorOption>>; - experimentalGpuAcceleration: IEditorOption; + experimentalGpuAcceleration: IEditorOption; experimentalWhitespaceRendering: IEditorOption; extraEditorClassName: IEditorOption; fastScrollSensitivity: IEditorOption; From c60f1429402b800fddd596428151aa8392c77965 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:53:58 -0700 Subject: [PATCH 142/286] Only use canRender when gpu setting is on --- .../editor/browser/viewParts/lines/viewLine.ts | 3 +-- .../browser/viewParts/lines/viewLineOptions.ts | 3 +++ src/vs/editor/common/config/editorOptions.ts | 16 +++++++++------- src/vs/monaco.d.ts | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index ed32dc833f0..b7ddda6ace5 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -98,8 +98,7 @@ export class ViewLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { - // TODO: Only do this if the experimentalGpuAcceleration setting is 'on'. - if (ViewLinesGpu.canRender(this._options, viewportData, lineNumber)) { + if (this._options.useGpu && ViewLinesGpu.canRender(this._options, viewportData, lineNumber)) { this._renderedViewLine?.domNode?.domNode.remove(); this._renderedViewLine = null; return false; diff --git a/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts b/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts index 76536607852..ec83930bf03 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts @@ -18,6 +18,7 @@ export class ViewLineOptions { public readonly lineHeight: number; public readonly stopRenderingLineAfter: number; public readonly fontLigatures: string; + public readonly useGpu: boolean; constructor(config: IEditorConfiguration, themeType: ColorScheme) { this.themeType = themeType; @@ -42,6 +43,7 @@ export class ViewLineOptions { this.lineHeight = options.get(EditorOption.lineHeight); this.stopRenderingLineAfter = options.get(EditorOption.stopRenderingLineAfter); this.fontLigatures = options.get(EditorOption.fontLigatures); + this.useGpu = options.get(EditorOption.experimentalGpuAcceleration) === 'on'; } public equals(other: ViewLineOptions): boolean { @@ -57,6 +59,7 @@ export class ViewLineOptions { && this.lineHeight === other.lineHeight && this.stopRenderingLineAfter === other.stopRenderingLineAfter && this.fontLigatures === other.fontLigatures + && this.useGpu === other.useGpu ); } } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index fd8f862ff05..459983c50b6 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -5733,13 +5733,15 @@ export const EditorOptions = { EditorOption.experimentalGpuAcceleration, 'experimentalGpuAcceleration', 'off' as 'off' | 'on', ['off', 'on'] as const, - { - enumDescriptions: [ - nls.localize('experimentalGpuAcceleration.off', "Use regular DOM-based rendering."), - nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), - ], - description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the (very) experimental GPU acceleration to render the editor.") - } + undefined + // TODO: Uncomment when we want to expose the setting to VS Code users + // { + // enumDescriptions: [ + // nls.localize('experimentalGpuAcceleration.off', "Use regular DOM-based rendering."), + // nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), + // ], + // description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the (very) experimental GPU acceleration to render the editor.") + // } )), experimentalWhitespaceRendering: register(new EditorStringEnumOption( EditorOption.experimentalWhitespaceRendering, 'experimentalWhitespaceRendering', diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index cbbaf7984d2..e26028840f9 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5060,7 +5060,7 @@ declare namespace monaco.editor { emptySelectionClipboard: IEditorOption; dropIntoEditor: IEditorOption>>; stickyScroll: IEditorOption>>; - experimentalGpuAcceleration: IEditorOption; + experimentalGpuAcceleration: IEditorOption; experimentalWhitespaceRendering: IEditorOption; extraEditorClassName: IEditorOption; fastScrollSensitivity: IEditorOption; From bd21f3c8f268b741edac137fc109d75a555c0541 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:48:56 -0700 Subject: [PATCH 143/286] Move all gpu actions into a single action with a picker --- .../editor/browser/gpu/atlas/textureAtlas.ts | 1 + .../editor/contrib/gpu/browser/gpuActions.ts | 231 +++++++++--------- 2 files changed, 117 insertions(+), 115 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 502dfc29ab9..a5f4cf35372 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -68,6 +68,7 @@ export class TextureAtlas extends Disposable { // IMPORTANT: The first glyph on the first page must be an empty glyph such that zeroed out // cells end up rendering nothing + // TODO: This currently means the first slab is for 0x0 glyphs and is wasted firstPage.getGlyph(new GlyphRasterizer(1, ''), '', 0); this._register(toDisposable(() => dispose(this._pages))); diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 89233f14719..729ca827aea 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -14,137 +14,138 @@ import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -const precondition = ContextKeyExpr.equals('config:editor.experimentalGpuAcceleration', 'on'); - -class LogTextureAtlasStatsAction extends EditorAction { +class DebugEditorGpuRendererAction extends EditorAction { constructor() { super({ - id: 'editor.action.logTextureAtlasStats', - label: localize('logTextureAtlasStats.label', "Log Texture Atlas States"), - alias: 'Log Texture Atlas States', - precondition, + id: 'editor.action.debugEditorGpuRenderer', + label: localize('gpuDebug.label', "Developer: Debug Editor GPU Renderer"), + alias: 'Developer: Debug Editor GPU Renderer', + // TODO: Why doesn't `ContextKeyExpr.equals('config:editor.experimentalGpuAcceleration', 'on')` work? + precondition: ContextKeyExpr.true(), }); } async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - const logService = accessor.get(ILogService); - - const atlas = ViewLinesGpu.atlas; - if (!ViewLinesGpu.atlas) { - logService.error('No texture atlas found'); - return; - } - - const stats = atlas.getStats(); - logService.info(['Texture atlas stats', ...stats].join('\n\n')); - } -} - -class SaveTextureAtlasAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.saveTextureAtlas', - label: localize('saveTextureAtlas.label', "Save Texture Atlas"), - alias: 'Save Texture Atlas', - precondition, - }); - } - - async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - const workspaceContextService = accessor.get(IWorkspaceContextService); - const fileService = accessor.get(IFileService); - const folders = workspaceContextService.getWorkspace().folders; - if (folders.length > 0) { - const atlas = ViewLinesGpu.atlas; - const promises = []; - for (const [layerIndex, page] of atlas.pages.entries()) { - promises.push(...[ - fileService.writeFile( - URI.joinPath(folders[0].uri, `textureAtlasPage${layerIndex}_actual.png`), - VSBuffer.wrap(new Uint8Array(await (await page.source.convertToBlob()).arrayBuffer())) - ), - fileService.writeFile( - URI.joinPath(folders[0].uri, `textureAtlasPage${layerIndex}_usage.png`), - VSBuffer.wrap(new Uint8Array(await (await page.getUsagePreview()).arrayBuffer())) - ), - ]); - } - await promises; - } - } -} - -class DrawGlyphAction extends EditorAction { - - constructor() { - super({ - id: 'editor.action.drawGlyph', - label: localize('drawGlyph.label', "Draw Glyph"), - alias: 'Draw Glyph', - precondition, - }); - } - - async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - const configurationService = accessor.get(IConfigurationService); - const fileService = accessor.get(IFileService); - const logService = accessor.get(ILogService); + const instantiationService = accessor.get(IInstantiationService); const quickInputService = accessor.get(IQuickInputService); - const workspaceContextService = accessor.get(IWorkspaceContextService); + const choice = await quickInputService.pick([ + { + label: localize('logTextureAtlasStats.label', "Log Texture Atlas Stats"), + id: 'logTextureAtlasStats', + }, + { + label: localize('saveTextureAtlas.label', "Save Texture Atlas"), + id: 'saveTextureAtlas', + }, + { + label: localize('drawGlyph.label', "Draw Glyph"), + id: 'drawGlyph', + }, + ], { canPickMany: false }); + if (!choice) { + return; + } + switch (choice.id) { + case 'logTextureAtlasStats': + instantiationService.invokeFunction(accessor => { + const logService = accessor.get(ILogService); - const folders = workspaceContextService.getWorkspace().folders; - if (folders.length === 0) { - return; - } + const atlas = ViewLinesGpu.atlas; + if (!ViewLinesGpu.atlas) { + logService.error('No texture atlas found'); + return; + } - const atlas = ViewLinesGpu.atlas; - if (!ViewLinesGpu.atlas) { - logService.error('No texture atlas found'); - return; - } + const stats = atlas.getStats(); + logService.info(['Texture atlas stats', ...stats].join('\n\n')); + }); + break; + case 'saveTextureAtlas': + instantiationService.invokeFunction(async accessor => { + const workspaceContextService = accessor.get(IWorkspaceContextService); + const fileService = accessor.get(IFileService); + const folders = workspaceContextService.getWorkspace().folders; + if (folders.length > 0) { + const atlas = ViewLinesGpu.atlas; + const promises = []; + for (const [layerIndex, page] of atlas.pages.entries()) { + promises.push(...[ + fileService.writeFile( + URI.joinPath(folders[0].uri, `textureAtlasPage${layerIndex}_actual.png`), + VSBuffer.wrap(new Uint8Array(await (await page.source.convertToBlob()).arrayBuffer())) + ), + fileService.writeFile( + URI.joinPath(folders[0].uri, `textureAtlasPage${layerIndex}_usage.png`), + VSBuffer.wrap(new Uint8Array(await (await page.getUsagePreview()).arrayBuffer())) + ), + ]); + } + await Promise.all(promises); + } + }); + break; + case 'drawGlyph': + instantiationService.invokeFunction(async accessor => { + const configurationService = accessor.get(IConfigurationService); + const fileService = accessor.get(IFileService); + const logService = accessor.get(ILogService); + const quickInputService = accessor.get(IQuickInputService); + const workspaceContextService = accessor.get(IWorkspaceContextService); - const fontFamily = configurationService.getValue('editor.fontFamily'); - const fontSize = configurationService.getValue('editor.fontSize'); - const rasterizer = new GlyphRasterizer(fontSize, fontFamily); - let chars = await quickInputService.input({ - prompt: 'Enter a character to draw (prefix with 0x for code point))' - }); - if (!chars) { - return; + const folders = workspaceContextService.getWorkspace().folders; + if (folders.length === 0) { + return; + } + + const atlas = ViewLinesGpu.atlas; + if (!ViewLinesGpu.atlas) { + logService.error('No texture atlas found'); + return; + } + + const fontFamily = configurationService.getValue('editor.fontFamily'); + const fontSize = configurationService.getValue('editor.fontSize'); + const rasterizer = new GlyphRasterizer(fontSize, fontFamily); + let chars = await quickInputService.input({ + prompt: 'Enter a character to draw (prefix with 0x for code point))' + }); + if (!chars) { + return; + } + const codePoint = chars.match(/0x(?[0-9a-f]+)/i)?.groups?.codePoint; + if (codePoint !== undefined) { + chars = String.fromCodePoint(parseInt(codePoint, 16)); + } + const metadata = 0; + const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, metadata); + if (!rasterizedGlyph) { + return; + } + const imageData = atlas.pages[rasterizedGlyph.pageIndex].source.getContext('2d')?.getImageData( + rasterizedGlyph.x, + rasterizedGlyph.y, + rasterizedGlyph.w, + rasterizedGlyph.h + ); + if (!imageData) { + return; + } + const canvas = new OffscreenCanvas(imageData.width, imageData.height); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.putImageData(imageData, 0, 0); + const blob = await canvas.convertToBlob({ type: 'image/png' }); + const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${metadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`); + await fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer()))); + }); + break; } - const codePoint = chars.match(/0x(?[0-9a-f]+)/i)?.groups?.codePoint; - if (codePoint !== undefined) { - chars = String.fromCodePoint(parseInt(codePoint, 16)); - } - const metadata = 0; - const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, metadata); - if (!rasterizedGlyph) { - return; - } - const imageData = atlas.pages[rasterizedGlyph.pageIndex].source.getContext('2d')?.getImageData( - rasterizedGlyph.x, - rasterizedGlyph.y, - rasterizedGlyph.w, - rasterizedGlyph.h - ); - if (!imageData) { - return; - } - const canvas = new OffscreenCanvas(imageData.width, imageData.height); - const ctx = ensureNonNullable(canvas.getContext('2d')); - ctx.putImageData(imageData, 0, 0); - const blob = await canvas.convertToBlob({ type: 'image/png' }); - const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${metadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`); - await fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer()))); } } -registerEditorAction(LogTextureAtlasStatsAction); -registerEditorAction(SaveTextureAtlasAction); -registerEditorAction(DrawGlyphAction); +registerEditorAction(DebugEditorGpuRendererAction); From 945d4cdda8a4eb3af6b2e04988f64cddf0a34330 Mon Sep 17 00:00:00 2001 From: Anees Date: Sat, 24 Aug 2024 02:41:24 +0100 Subject: [PATCH 144/286] Use separate div for image preview transparency grid --- extensions/media-preview/media/imagePreview.css | 11 ++++++----- extensions/media-preview/media/imagePreview.js | 8 ++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/extensions/media-preview/media/imagePreview.css b/extensions/media-preview/media/imagePreview.css index 49a01b8d969..67c73e39595 100644 --- a/extensions/media-preview/media/imagePreview.css +++ b/extensions/media-preview/media/imagePreview.css @@ -12,6 +12,7 @@ html, body { body img { max-width: none; max-height: none; + vertical-align: middle; } .container:focus { @@ -31,20 +32,20 @@ body img { box-sizing: border-box; } -.container.image img { +.container.image .transparency-grid { padding: 0; background-position: 0 0, 8px 8px; background-size: 16px 16px; border: 1px solid var(--vscode-imagePreview-border); } -.container.image img { +.container.image .transparency-grid { background-image: linear-gradient(45deg, rgb(230, 230, 230) 25%, transparent 25%, transparent 75%, rgb(230, 230, 230) 75%, rgb(230, 230, 230)), linear-gradient(45deg, rgb(230, 230, 230) 25%, transparent 25%, transparent 75%, rgb(230, 230, 230) 75%, rgb(230, 230, 230)); } -.vscode-dark.container.image img { +.vscode-dark.container.image .transparency-grid { background-image: linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)), linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)); @@ -54,13 +55,13 @@ body img { image-rendering: pixelated; } -.container img.scale-to-fit { +.container .transparency-grid.scale-to-fit { max-width: calc(100% - 20px); max-height: calc(100% - 20px); object-fit: contain; } -.container img { +.container .transparency-grid { margin: auto; } diff --git a/extensions/media-preview/media/imagePreview.js b/extensions/media-preview/media/imagePreview.js index ab8ad542a2d..7179283d052 100644 --- a/extensions/media-preview/media/imagePreview.js +++ b/extensions/media-preview/media/imagePreview.js @@ -76,6 +76,8 @@ // Elements const container = document.body; + const transparencyGrid = document.createElement('div'); + transparencyGrid.classList.add('transparency-grid'); const image = document.createElement('img'); function updateScale(newScale) { @@ -85,7 +87,7 @@ if (newScale === 'fit') { scale = 'fit'; - image.classList.add('scale-to-fit'); + transparencyGrid.classList.add('scale-to-fit'); image.classList.remove('pixelated'); // @ts-ignore Non-standard CSS property image.style.zoom = 'normal'; @@ -292,7 +294,9 @@ document.body.classList.remove('loading'); document.body.classList.add('ready'); - document.body.append(image); + + document.body.append(transparencyGrid); + transparencyGrid.appendChild(image); updateScale(scale); From b97dc796498648db409fc082a5a3032c4c0cc642 Mon Sep 17 00:00:00 2001 From: Ritam Date: Sun, 25 Aug 2024 16:10:27 +0530 Subject: [PATCH 145/286] feat: serve-web now checks for update every 1 hour by default and can be configured by the flag "update_check_interval" --- cli/src/commands/args.rs | 4 ++ cli/src/commands/serve_web.rs | 72 +++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 101f1eac29f..895dad08f95 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -222,6 +222,10 @@ pub struct ServeWebArgs { /// Set the root path for extensions. #[clap(long)] pub extensions_dir: Option, + /// Set update check interval in seconds, defaults to 3600 seconds. Set to 0 to disable update checks + #[clap(long)] + pub update_check_interval: Option, + } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 3528bcb7ab6..1e227402808 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -15,7 +15,7 @@ use std::time::{Duration, Instant}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::pin; +use tokio::{pin,time}; use crate::async_pipe::{ get_socket_name, get_socket_rw_stream, listen_socket_rw_stream, AsyncPipe, @@ -86,7 +86,15 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result = ConnectionManager::new(&ctx, platform, args.clone()); + + let update_check_interval = args.update_check_interval.unwrap_or(3600); + + if update_check_interval > 0 { + // Start the update checker + cm.clone().start_update_checker(Duration::from_secs(update_check_interval)); + } + let key = get_server_key_half(&ctx.paths); let make_svc = move || { let ctx = HandleContext { @@ -175,7 +183,7 @@ async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response r, Err(e) => { error!(ctx.log, "error getting latest version: {}", e); @@ -582,6 +590,33 @@ impl ConnectionManager { }) } + // spawns a task that checks for updates every n seconds duration + pub fn start_update_checker(self: Arc, duration: Duration) { + debug!(self.log, "starting update checker"); + tokio::spawn(async move { + let mut interval = time::interval(duration); + loop { + interval.tick().await; + debug!(self.log, "checking for updates"); + match self.get_latest_release().await { + Ok(_) => {}, + Err(e) => { + error!(self.log, "error getting latest version: {}", e); + } + }; + } + }); + } + + // Returns the latest release, available on the cache + pub async fn get_release_from_cache(&self) -> Result { + let latest = self.latest_version.lock().await; + if let Some((_, release)) = &*latest { + return Ok(release.clone()); + } + Err(CodeError::ServerNotYetDownloaded) + } + /// Gets a connection to a server version pub async fn get_connection( &self, @@ -601,11 +636,6 @@ impl ConnectionManager { let mut latest = self.latest_version.lock().await; let now = Instant::now(); let target_kind = TargetKind::Web; - if let Some((checked_at, release)) = &*latest { - if checked_at.elapsed() < Duration::from_secs(RELEASE_CACHE_SECS) { - return Ok(release.clone()); - } - } let quality = VSCODE_CLI_QUALITY .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) @@ -626,6 +656,32 @@ impl ConnectionManager { return Ok(previous.clone()); } + // If the new release and previous release are different, download the new version + if let Ok(new_release) = &release { + let (_, previous_release) = latest.clone().unwrap_or((Instant::now(), Release { + name: String::from("0.0.0"), + commit: String::from("0.0.0"), + platform:self.platform, + target: target_kind, + quality + })); + if new_release.commit != previous_release.commit { + match self.get_version_data_inner(new_release.clone()) { + Ok(mut r) => { + match r.wait().await { + Ok(_) => {}, + Err(e) => { + info!(self.log, "{}", e); + } + }; + }, + Err(e) => { + info!(self.log, "{}", e); + } + } + } + } + let release = release?; debug!(self.log, "refreshed latest release: {}", release); *latest = Some((now, release.clone())); From a3b109f779e7fdbb5e212611c8becc0c0c1f5ee2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 26 Aug 2024 13:33:45 -0700 Subject: [PATCH 146/286] Remove stale comments/todos --- .../gpu/atlas/textureAtlasSlabAllocator.ts | 44 ------------------- .../browser/gpu/fullFileRenderStrategy.ts | 19 -------- .../browser/gpu/raster/glyphRasterizer.ts | 4 -- .../viewParts/linesGpu/viewLinesGpu.ts | 2 - 4 files changed, 69 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index 98863a44fbc..0fb0c93263e 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -199,54 +199,11 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { } } } - // for (const [i, r] of this._unusedRects.entries()) { - // if (r.w < r.h) { - // if (r.w >= glyphWidth && r.h >= glyphHeight) { - // dx = r.x; - // dy = r.y; - // if (glyphWidth < r.w) { - // this._unusedRects.push({ - // x: r.x + glyphWidth, - // y: r.y, - // w: r.w - glyphWidth, - // h: glyphHeight - // }); - // } - // r.y += glyphHeight; - // r.h -= glyphHeight; - // if (r.h === 0) { - // // TODO: This is slow - // this._unusedRects.splice(i, 1); - // } - // break; - // } - // } else { - // if (r.w >= glyphWidth && r.h >= glyphHeight) { - // dx = r.x; - // dy = r.y; - // if (glyphHeight < r.h) { - // this._unusedRects.push({ - // x: r.x, - // y: r.y + glyphHeight, - // w: glyphWidth, - // h: r.h - glyphHeight - // }); - // } - // r.x += glyphWidth; - // r.w -= glyphWidth; - // if (r.w === 0) { - // // TODO: This is slow - // this._unusedRects.splice(i, 1); - // } - // } - // } - // } } // Create a new slab if (dx === undefined || dy === undefined) { if (!slab) { - // TODO: Return undefined if there isn't any room left if (this._slabs.length >= this._slabsPerRow * this._slabsPerColumn) { return undefined; } @@ -354,7 +311,6 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { x = 0; y += slab.entryH; } - // TODO: This doesn't visualize wasted space between entries - draw glyphs on top? ctx.fillStyle = UsagePreviewColors.Wasted; ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH); diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index e35713435b3..c28de1110f7 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -84,7 +84,6 @@ struct VSOutput { var vsOut: VSOutput; // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 vsOut.position = vec4f( - // TODO: Fix hacky scroll offset which moves the text beside the line numbers (((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) + (((scrollOffset.offset + layoutInfo.viewportOffset) * 2) / layoutInfo.canvasDims), 0.0, 1.0 @@ -234,9 +233,6 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra let dirtyLineStart = Number.MAX_SAFE_INTEGER; let dirtyLineEnd = 0; - // const theme = this._themeService.getColorTheme() as ColorThemeData; - // const tokenStyle = theme.getTokenStyleMetadata(type, modifiers, defaultLanguage, true, definitions); - for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { // TODO: Update on dirty lines; is this known by line before rendering? // if (upToDateLines.has(y)) { @@ -249,13 +245,6 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra content = lineData.content; xOffset = 0; - // TODO: Handle colors via viewLineRenderingData.tokens - // console.log(lineData.tokens); - // console.log('fg'); - // for (let i = 0; i < lineData.tokens.getCount(); i++) { - // console.log(` ${lineData.tokens.getForeground(i)}`); - // } - // See ViewLine#renderLine // const renderLineInput = new RenderLineInput( // options.useMonospaceOptimizations, @@ -354,19 +343,11 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra this._device.queue.writeBuffer( this._cellBindBuffer, (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, - // TODO: this cell buffer actually only needs to be the size of the viewport if we are only uploading a range - // at the maximum each frame cellBuffer.buffer, (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, (dirtyLineEnd - dirtyLineStart + 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT ); } - // HACK: Replace entire buffer for testing purposes - // this._device.queue.writeBuffer( - // this._cellBindBuffer, - // 0, - // cellBuffer - // ); this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index d739eaf4114..505bfd6ddb7 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -47,7 +47,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { } // TODO: Support drawing multiple fonts and sizes - // TODO: Should pull in the font size from config instead of random dom node /** * Rasterizes a glyph. Note that the returned object is reused across different glyphs and * therefore is only safe for synchronous access. @@ -91,7 +90,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // need to be drawn manually to the canvas. See xterm.js for "dodging" the text for // underlines. - // TODO: Draw in middle using alphabetical baseline const originX = this._fontSize; const originY = this._fontSize; this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; @@ -154,9 +152,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { } // TODO: Does this even need to happen when measure text is used? - // TODO: Pass back origin offset private _findGlyphBoundingBox(imageData: ImageData, outBoundingBox: IBoundingBox) { - // TODO: This could be optimized to be aware of the font size padding on all sides const height = this._canvas.height; const width = this._canvas.width; let found = false; diff --git a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts index d6c5e833f7c..6be44ca345b 100644 --- a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts @@ -66,8 +66,6 @@ export class ViewLinesGpu extends ViewPart { // TODO: Request render, should this just call renderText with the last viewportData })); - // TODO: It would be nice if the async part of this (requesting device) was done before - // ViewLinesGpu was constructed this.initWebgpu(); } From 958284e877c80cb78466cb53a8acec97c4a1a63e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:25:29 -0700 Subject: [PATCH 147/286] Remove more unneeded commented out code --- .../viewParts/linesGpu/viewLinesGpu.ts | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts index 6be44ca345b..0a81f89a6da 100644 --- a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts @@ -348,29 +348,6 @@ export class ViewLinesGpu extends ViewPart { // subscribe to more events public renderText(viewportData: ViewportData): void { - // TODO: Remove this simple canvas rendering when webgpu stuff is hooked up - // const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); - - // const ctxSimple = this.canvas.getContext('2d')!; - // ctxSimple.clearRect(0, 0, this.canvas.width, this.canvas.height); - // ctxSimple.strokeStyle = 'black'; - // const vp = this._context.viewLayout.getCurrentViewport(); - // const left = this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft; - // this.canvas.width = vp.width; - // this.canvas.height = vp.height; - // ctxSimple.font = `${this._context.configuration.options.get(EditorOption.fontSize)}px monospace`; - - // for (let i = viewportData.startLineNumber; i <= viewportData.endLineNumber; i++) { - // if (!ViewLinesGpu.canRender(options, viewportData, i)) { - // continue; - // } - // const line = viewportData.getViewLineRenderingData(i); - - // ctxSimple.strokeText(line.content, left, viewportData.relativeVerticalOffset[i - viewportData.startLineNumber] + viewportData.lineHeight - this._context.viewLayout.getCurrentScrollTop()); - // } - - - if (this._initialized) { return this._renderText(viewportData); } From f4ed2566c5baf0e92c1d4683f5bf28eae39a4025 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:04:42 -0700 Subject: [PATCH 148/286] More relative imports --- .../editor/browser/gpu/atlas/textureAtlas.ts | 24 ++++++------ .../browser/gpu/atlas/textureAtlasPage.ts | 18 ++++----- .../gpu/atlas/textureAtlasSlabAllocator.ts | 10 ++--- .../browser/gpu/fullFileRenderStrategy.ts | 29 +++++++------- src/vs/editor/browser/gpu/gpuDisposable.ts | 4 +- src/vs/editor/browser/gpu/gpuUtils.ts | 2 +- .../browser/gpu/objectCollectionBuffer.ts | 6 +-- .../browser/gpu/raster/glyphRasterizer.ts | 10 ++--- src/vs/editor/browser/gpu/taskQueue.ts | 4 +- src/vs/editor/browser/gpu/viewGpuContext.ts | 12 +++--- .../viewParts/linesGpu/viewLinesGpu.ts | 39 ++++++++++--------- .../test/browser/view/gpu/atlas/testUtil.ts | 8 ++-- .../view/gpu/atlas/textureAtlas.test.ts | 16 ++++---- .../gpu/atlas/textureAtlasAllocator.test.ts | 14 +++---- .../view/gpu/objectCollectionBuffer.test.ts | 4 +- 15 files changed, 101 insertions(+), 99 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index a5f4cf35372..8c439a1bbed 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; -import { Event } from 'vs/base/common/event'; -import { Disposable, dispose, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { TwoKeyMap } from 'vs/base/common/map'; -import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; -import { TextureAtlasPage, type AllocatorType } from 'vs/editor/browser/gpu/atlas/textureAtlasPage'; -import { GlyphRasterizer } from 'vs/editor/browser/gpu/raster/glyphRasterizer'; -import type { IGlyphRasterizer } from 'vs/editor/browser/gpu/raster/raster'; -import { IdleTaskQueue } from 'vs/editor/browser/gpu/taskQueue'; -import { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { Event } from '../../../../base/common/event.js'; +import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { TwoKeyMap } from '../../../../base/common/map.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; +import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; +import type { IGlyphRasterizer } from '../raster/raster.js'; +import { IdleTaskQueue } from '../taskQueue.js'; +import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from './atlas.js'; +import { AllocatorType, TextureAtlasPage } from './textureAtlasPage.js'; export interface ITextureAtlasOptions { allocatorType?: AllocatorType; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index 54c21ab6812..9d92a5a588c 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { TwoKeyMap } from 'vs/base/common/map'; -import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; -import { TextureAtlasShelfAllocator } from 'vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator'; -import { TextureAtlasSlabAllocator } from 'vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator'; -import type { IBoundingBox, IGlyphRasterizer } from 'vs/editor/browser/gpu/raster/raster'; -import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Event } from '../../../../base/common/event.js'; +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { TwoKeyMap } from '../../../../base/common/map.js'; +import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import type { IBoundingBox, IGlyphRasterizer } from '../raster/raster.js'; +import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPageGlyph } from './atlas.js'; +import { TextureAtlasShelfAllocator } from './textureAtlasShelfAllocator.js'; +import { TextureAtlasSlabAllocator } from './textureAtlasSlabAllocator.js'; export type AllocatorType = 'shelf' | 'slab' | ((canvas: OffscreenCanvas, textureIndex: number) => ITextureAtlasAllocator); diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index 0fb0c93263e..cfc8b231695 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; -import { TwoKeyMap } from 'vs/base/common/map'; -import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; -import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { TwoKeyMap } from '../../../../base/common/map.js'; +import { ensureNonNullable } from '../gpuUtils.js'; +import type { IRasterizedGlyph } from '../raster/raster.js'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js'; export interface TextureAtlasSlabAllocatorOptions { slabW?: number; diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index c28de1110f7..c05ae43b66f 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -3,20 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; -import { Disposable } from 'vs/base/common/lifecycle'; -import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; -import { TextureAtlas } from 'vs/editor/browser/gpu/atlas/textureAtlas'; -import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/gpu/gpu'; -import { GPULifecycle } from 'vs/editor/browser/gpu/gpuDisposable'; -import { quadVertices } from 'vs/editor/browser/gpu/gpuUtils'; -import { GlyphRasterizer } from 'vs/editor/browser/gpu/raster/glyphRasterizer'; -import type { ViewLineOptions } from 'vs/editor/browser/viewParts/lines/viewLineOptions'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import type { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens'; -import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import type { ViewLineRenderingData } from 'vs/editor/common/viewModel'; -import type { ViewContext } from 'vs/editor/common/viewModel/viewContext'; +import { getActiveWindow } from '../../../base/browser/dom.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { EditorOption } from '../../common/config/editorOptions.js'; +import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; +import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; +import type { ViewLineRenderingData } from '../../common/viewModel.js'; +import type { ViewContext } from '../../common/viewModel/viewContext.js'; +import type { ViewLineOptions } from '../viewParts/lines/viewLineOptions.js'; +import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; +import type { TextureAtlas } from './atlas/textureAtlas.js'; +import { BindingId, type IGpuRenderStrategy } from './gpu.js'; +import { GPULifecycle } from './gpuDisposable.js'; +import { quadVertices } from './gpuUtils.js'; +import { GlyphRasterizer } from './raster/glyphRasterizer.js'; + const enum Constants { IndicesPerCell = 6, diff --git a/src/vs/editor/browser/gpu/gpuDisposable.ts b/src/vs/editor/browser/gpu/gpuDisposable.ts index 37ee97563b3..7837dbb9934 100644 --- a/src/vs/editor/browser/gpu/gpuDisposable.ts +++ b/src/vs/editor/browser/gpu/gpuDisposable.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type IReference } from 'vs/base/common/lifecycle'; -import { isFunction } from 'vs/base/common/types'; +import type { IReference } from '../../../base/common/lifecycle.js'; +import { isFunction } from '../../../base/common/types.js'; export namespace GPULifecycle { export async function requestDevice(): Promise> { diff --git a/src/vs/editor/browser/gpu/gpuUtils.ts b/src/vs/editor/browser/gpu/gpuUtils.ts index 52224c53643..b7035cbb920 100644 --- a/src/vs/editor/browser/gpu/gpuUtils.ts +++ b/src/vs/editor/browser/gpu/gpuUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { toDisposable, type IDisposable } from 'vs/base/common/lifecycle'; +import { toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; export const quadVertices = new Float32Array([ 1, 0, diff --git a/src/vs/editor/browser/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/gpu/objectCollectionBuffer.ts index e7f7ed27435..947c04172a2 100644 --- a/src/vs/editor/browser/gpu/objectCollectionBuffer.ts +++ b/src/vs/editor/browser/gpu/objectCollectionBuffer.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, dispose, toDisposable, type IDisposable } from 'vs/base/common/lifecycle'; -import { LinkedList } from 'vs/base/common/linkedList'; +import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable, dispose, toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; +import { LinkedList } from '../../../base/common/linkedList.js'; export interface ObjectCollectionBufferPropertySpec { name: string; diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 505bfd6ddb7..120e99ac566 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; -import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; -import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; -import { StringBuilder } from 'vs/editor/common/core/stringBuilder'; -import { FontStyle, TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { StringBuilder } from '../../../common/core/stringBuilder.js'; +import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; +import { ensureNonNullable } from '../gpuUtils.js'; +import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from './raster.js'; let nextId = 0; diff --git a/src/vs/editor/browser/gpu/taskQueue.ts b/src/vs/editor/browser/gpu/taskQueue.ts index e682c5fa58d..27d64d01fe2 100644 --- a/src/vs/editor/browser/gpu/taskQueue.ts +++ b/src/vs/editor/browser/gpu/taskQueue.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { getActiveWindow } from '../../../base/browser/dom.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; /** * Copyright (c) 2022 The xterm.js authors. All rights reserved. diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 4810412e3be..cfd877e46f2 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from 'vs/base/browser/dom'; -import { createFastDomNode, type FastDomNode } from 'vs/base/browser/fastDomNode'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { GPULifecycle } from 'vs/editor/browser/gpu/gpuDisposable'; -import { ensureNonNullable, observeDevicePixelDimensions } from 'vs/editor/browser/gpu/gpuUtils'; +import { getActiveWindow } from '../../../base/browser/dom.js'; +import { createFastDomNode, type FastDomNode } from '../../../base/browser/fastDomNode.js'; +import { Emitter } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { GPULifecycle } from './gpuDisposable.js'; +import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; export class ViewGpuContext extends Disposable { readonly canvas: FastDomNode; diff --git a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts index 0a81f89a6da..dc58a1592d9 100644 --- a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts @@ -3,24 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BugIndicatingError } from 'vs/base/common/errors'; -import { TextureAtlas } from 'vs/editor/browser/gpu/atlas/textureAtlas'; -import { BindingId, type IGpuRenderStrategy } from 'vs/editor/browser/gpu/gpu'; -import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; -import { ViewPart } from 'vs/editor/browser/view/viewPart'; -import { ViewLinesChangedEvent, ViewScrollChangedEvent } from 'vs/editor/common/viewEvents'; -import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; -import { ViewLineOptions } from '../lines/viewLineOptions'; -import { observeDevicePixelDimensions, quadVertices } from 'vs/editor/browser/gpu/gpuUtils'; -import { getActiveWindow } from 'vs/base/browser/dom'; -import { GPULifecycle } from 'vs/editor/browser/gpu/gpuDisposable'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; -import { FullFileRenderStrategy } from 'vs/editor/browser/gpu/fullFileRenderStrategy'; -import { TextureAtlasPage } from 'vs/editor/browser/gpu/atlas/textureAtlasPage'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import type { ViewGpuContext } from 'vs/editor/browser/gpu/viewGpuContext'; +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; +import type { ViewLinesChangedEvent, ViewScrollChangedEvent } from '../../../common/viewEvents.js'; +import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; +import type { ViewContext } from '../../../common/viewModel/viewContext.js'; +import { TextureAtlas } from '../../gpu/atlas/textureAtlas.js'; +import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js'; +import { FullFileRenderStrategy } from '../../gpu/fullFileRenderStrategy.js'; +import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js'; +import { GPULifecycle } from '../../gpu/gpuDisposable.js'; +import { observeDevicePixelDimensions, quadVertices } from '../../gpu/gpuUtils.js'; +import type { ViewGpuContext } from '../../gpu/viewGpuContext.js'; +import type { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js'; +import { ViewPart } from '../../view/viewPart.js'; +import { ViewLineOptions } from '../lines/viewLineOptions.js'; + const enum GlyphStorageBufferInfo { FloatsPerEntry = 2 + 2 + 2, @@ -329,7 +330,7 @@ export class ViewLinesGpu extends ViewPart { return d.content.indexOf('e') !== -1; } - public override prepareRender(ctx: RenderingContext): void { + public prepareRender(ctx: RenderingContext): void { throw new BugIndicatingError('Should not be called'); } diff --git a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts b/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts index 8ff88f9a04a..73d1e167f1e 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { fail, ok } from 'assert'; -import { isNumber } from 'vs/base/common/types'; -import type { ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; -import { TextureAtlas } from 'vs/editor/browser/gpu/atlas/textureAtlas'; -import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; +import type { ITextureAtlasPageGlyph } from '../../../../../browser/gpu/atlas/atlas.js'; +import { TextureAtlas } from '../../../../../browser/gpu/atlas/textureAtlas.js'; +import { isNumber } from '../../../../../../base/common/types.js'; +import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; export function assertIsValidGlyph(glyph: Readonly | undefined, atlasOrSource: TextureAtlas | OffscreenCanvas) { if (glyph === undefined) { diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index b2181d3c2c6..6b96783c3b1 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { strictEqual, throws } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { TextureAtlas } from 'vs/editor/browser/gpu/atlas/textureAtlas'; -import { TextureAtlasSlabAllocator } from 'vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator'; -import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; -import type { IGlyphRasterizer, IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; -import { createCodeEditorServices } from 'vs/editor/test/browser/testCodeEditor'; -import { assertIsValidGlyph } from 'vs/editor/test/browser/view/gpu/atlas/testUtil'; -import type { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import type { IGlyphRasterizer, IRasterizedGlyph } from '../../../../../browser/gpu/raster/raster.js'; +import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; +import type { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { TextureAtlas } from '../../../../../browser/gpu/atlas/textureAtlas.js'; +import { createCodeEditorServices } from '../../../testCodeEditor.js'; +import { assertIsValidGlyph } from './testUtil.js'; +import { TextureAtlasSlabAllocator } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; const blackInt = 0x000000FF; diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 140610e4286..3c34b51a492 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, strictEqual, throws } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import type { ITextureAtlasAllocator } from 'vs/editor/browser/gpu/atlas/atlas'; -import { TextureAtlasShelfAllocator } from 'vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator'; -import { TextureAtlasSlabAllocator, TextureAtlasSlabAllocatorOptions } from 'vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator'; -import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; -import { assertIsValidGlyph } from 'vs/editor/test/browser/view/gpu/atlas/testUtil'; +import type { IRasterizedGlyph } from '../../../../../browser/gpu/raster/raster.js'; +import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; +import type { ITextureAtlasAllocator } from '../../../../../browser/gpu/atlas/atlas.js'; +import { TextureAtlasShelfAllocator } from '../../../../../browser/gpu/atlas/textureAtlasShelfAllocator.js'; +import { TextureAtlasSlabAllocator, type TextureAtlasSlabAllocatorOptions } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { assertIsValidGlyph } from './testUtil.js'; const blackArr = [0x00, 0x00, 0x00, 0xFF]; diff --git a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts index d7ef616fe24..a0f112c9656 100644 --- a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts +++ b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, strictEqual, throws } from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from 'vs/editor/browser/gpu/objectCollectionBuffer'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from '../../../../browser/gpu/objectCollectionBuffer.js'; suite('ObjectCollectionBuffer', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); From ec37a80943f1d655c39cee35460fc3d797ece2d2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:08:10 -0700 Subject: [PATCH 149/286] More relative imports --- .../gpu/atlas/textureAtlasShelfAllocator.ts | 6 ++-- .../editor/contrib/gpu/browser/gpuActions.ts | 30 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts index bda3815c5d0..82b7777fac4 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from 'vs/editor/browser/gpu/atlas/atlas'; -import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; -import type { IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; +import { ensureNonNullable } from '../gpuUtils.js'; +import type { IRasterizedGlyph } from '../raster/raster.js'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js'; /** * The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts index 729ca827aea..b28a6cf6915 100644 --- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -3,21 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { VSBuffer } from 'vs/base/common/buffer'; -import { URI } from 'vs/base/common/uri'; -import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, registerEditorAction, type ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { ensureNonNullable } from 'vs/editor/browser/gpu/gpuUtils'; -import { GlyphRasterizer } from 'vs/editor/browser/gpu/raster/glyphRasterizer'; -import { ViewLinesGpu } from 'vs/editor/browser/viewParts/linesGpu/viewLinesGpu'; -import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import type { ICodeEditor } from '../../../browser/editorBrowser.js'; +import { EditorAction, registerEditorAction, type ServicesAccessor } from '../../../browser/editorExtensions.js'; +import { ensureNonNullable } from '../../../browser/gpu/gpuUtils.js'; +import { GlyphRasterizer } from '../../../browser/gpu/raster/glyphRasterizer.js'; +import { ViewLinesGpu } from '../../../browser/viewParts/linesGpu/viewLinesGpu.js'; class DebugEditorGpuRendererAction extends EditorAction { From 3fca5b17688c4389a360a8f774f8458fa3c587c2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:34:41 -0700 Subject: [PATCH 150/286] Fix texture atlas tests --- src/vs/editor/browser/gpu/atlas/textureAtlas.ts | 4 +++- src/vs/editor/browser/gpu/raster/glyphRasterizer.ts | 7 +++++++ .../test/browser/view/gpu/atlas/textureAtlas.test.ts | 9 +++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 8c439a1bbed..67eaa2d8a4c 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -69,7 +69,9 @@ export class TextureAtlas extends Disposable { // IMPORTANT: The first glyph on the first page must be an empty glyph such that zeroed out // cells end up rendering nothing // TODO: This currently means the first slab is for 0x0 glyphs and is wasted - firstPage.getGlyph(new GlyphRasterizer(1, ''), '', 0); + const nullRasterizer = new GlyphRasterizer(1, ''); + firstPage.getGlyph(nullRasterizer, '', 0); + nullRasterizer.dispose(); this._register(toDisposable(() => dispose(this._pages))); } diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index 120e99ac566..4d33abcd8f6 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -56,6 +56,13 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { metadata: number, colorMap: string[], ): Readonly { + if (chars === '') { + return { + source: this._canvas, + boundingBox: { top: 0, left: 0, bottom: -1, right: -1 }, + originOffset: { x: 0, y: 0 } + }; + } // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary // work when the rasterizer is called multiple times like when the glyph doesn't fit into a // page. diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts index 6b96783c3b1..c745bb7b4cd 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -90,12 +90,17 @@ suite('TextureAtlas', () => { }); test('adding glyph to full page creates new page', () => { + let pageCount: number | undefined; for (let i = 0; i < 4; i++) { assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + if (pageCount === undefined) { + pageCount = atlas.pages.length; + } else { + strictEqual(atlas.pages.length, pageCount, 'the number of pages should not change when the page is being filled'); + } } - strictEqual(atlas.pages.length, 1); assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); - strictEqual(atlas.pages.length, 2, 'the 5th glyph should overflow to a new page'); + strictEqual(atlas.pages.length, pageCount! + 1, 'the 5th glyph should overflow to a new page'); }); test('adding a glyph larger than the atlas', () => { From 51d3a8f24fa7c5ab081f57679b701a67c37ba36f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 4 Sep 2024 09:39:58 -0700 Subject: [PATCH 151/286] More relative imports --- src/vs/editor/browser/gpu/atlas/atlas.ts | 2 +- src/vs/editor/browser/gpu/gpu.ts | 4 ++-- src/vs/editor/browser/gpu/raster/raster.ts | 2 +- src/vs/editor/browser/viewParts/lines/viewLineOptions.ts | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index a1b3457be92..397924d9971 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IBoundingBox, IRasterizedGlyph } from 'vs/editor/browser/gpu/raster/raster'; +import type { IBoundingBox, IRasterizedGlyph } from '../raster/raster.js'; /** * Information about a {@link IRasterizedGlyph rasterized glyph} that has been drawn to a texture diff --git a/src/vs/editor/browser/gpu/gpu.ts b/src/vs/editor/browser/gpu/gpu.ts index 16635f1c16d..c0a5fc6845c 100644 --- a/src/vs/editor/browser/gpu/gpu.ts +++ b/src/vs/editor/browser/gpu/gpu.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ViewLineOptions } from 'vs/editor/browser/viewParts/lines/viewLineOptions'; -import type { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; +import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; +import type { ViewLineOptions } from '../viewParts/lines/viewLineOptions.js'; export const enum BindingId { GlyphInfo0, diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts index 606e84fe512..1a7c5d1add6 100644 --- a/src/vs/editor/browser/gpu/raster/raster.ts +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { MetadataConsts } from 'vs/editor/common/encodedTokenAttributes'; +import type { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; export interface IGlyphRasterizer { /** diff --git a/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts b/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts index ec83930bf03..c75ea1740fe 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts @@ -2,9 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorConfiguration } from 'vs/editor/common/config/editorConfiguration'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; + +import type { ColorScheme } from '../../../../platform/theme/common/theme.js'; +import type { IEditorConfiguration } from '../../../common/config/editorConfiguration.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; export class ViewLineOptions { public readonly themeType: ColorScheme; From 48c0ee46e3d3d7054aeb88fc3ab21aac36aa98d9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 6 Sep 2024 07:19:15 -0700 Subject: [PATCH 152/286] Add webgpu types --- package-lock.json | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index d1df23b3f60..8ae291f7169 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,7 @@ "@vscode/test-web": "^0.0.56", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", + "@webgpu/types": "^0.1.44", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", @@ -3413,6 +3414,12 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.44", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", + "integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==", + "dev": true + }, "node_modules/@webpack-cli/configtest": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", diff --git a/package.json b/package.json index 870d2518210..4c66c0d310b 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "@vscode/test-web": "^0.0.56", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", - "@webgpu/types": "^0.1.40", + "@webgpu/types": "^0.1.44", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", From 056fa7ada096f5492eef003cbe5554d0c9c9babb Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 6 Sep 2024 09:00:38 -0700 Subject: [PATCH 153/286] Add Chat symbol anchor proposal For #227753 Adds the first part of #227753, which lets `ChatResponseAnchorPart` show a symbol. This enables nicer rendering of the anchor --- src/vs/base/browser/ui/list/listWidget.ts | 4 +- .../common/extensionsApiProposals.ts | 3 + .../api/common/extHostTypeConverters.ts | 13 +++- src/vs/workbench/api/common/extHostTypes.ts | 8 +-- .../chatMarkdownDecorationsRenderer.ts | 65 ++++++++++--------- .../browser/media/chatInlineAnchorWidget.css | 46 +++++++++++++ .../media/chatInlineFileLinkWidget.css | 12 ---- .../contrib/chat/common/annotations.ts | 27 +++++++- .../contrib/chat/common/chatModel.ts | 7 +- .../contrib/chat/common/chatService.ts | 3 +- .../vscode.proposed.chatSymbolAnchor.d.ts | 19 ++++++ 11 files changed, 153 insertions(+), 54 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css delete mode 100644 src/vs/workbench/contrib/chat/browser/media/chatInlineFileLinkWidget.css create mode 100644 src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 67b8e18a3a6..6b6f6bcdd6a 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -892,7 +892,7 @@ export class DefaultStyleController implements IStyleController { } if (styles.listActiveSelectionIconForeground) { - content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; }`); + content.push(`@layer monaco-list { .monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; } }`); } if (styles.listFocusAndSelectionBackground) { @@ -915,7 +915,7 @@ export class DefaultStyleController implements IStyleController { } if (styles.listInactiveSelectionIconForeground) { - content.push(`.monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; }`); + content.push(`@layer monaco-list { .monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; } }`); } if (styles.listInactiveFocusBackground) { diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 362b4298cdd..ca51f6dbf68 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -40,6 +40,9 @@ const _allApiProposals = { chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', }, + chatSymbolAnchor: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts', + }, chatTab: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', }, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b71c0f98796..c0652cccd45 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2486,18 +2486,27 @@ export namespace ChatResponseAnchorPart { export function from(part: vscode.ChatResponseAnchorPart): Dto { // Work around type-narrowing confusion between vscode.Uri and URI const isUri = (thing: unknown): thing is vscode.Uri => URI.isUri(thing); + const isSymbolInformation = (x: any): x is vscode.SymbolInformation => x instanceof types.SymbolInformation; return { kind: 'inlineReference', name: part.title, - inlineReference: isUri(part.value) ? part.value : Location.from(part.value) + inlineReference: isUri(part.value) + ? part.value + : isSymbolInformation(part.value) + ? WorkspaceSymbol.from(part.value) + : Location.from(part.value) }; } export function to(part: Dto): vscode.ChatResponseAnchorPart { const value = revive(part); return new types.ChatResponseAnchorPart( - URI.isUri(value.inlineReference) ? value.inlineReference : Location.to(value.inlineReference), + URI.isUri(value.inlineReference) + ? value.inlineReference + : 'location' in value.inlineReference + ? WorkspaceSymbol.to(value.inlineReference) + : Location.to(value.inlineReference), part.name ); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 842dbbf5679..c2e1d7a38e5 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1346,14 +1346,14 @@ export class SymbolInformation { location!: Location; kind: SymbolKind; tags?: SymbolTag[]; - containerName: string | undefined; + containerName: string; constructor(name: string, kind: SymbolKind, containerName: string | undefined, location: Location); constructor(name: string, kind: SymbolKind, range: Range, uri?: URI, containerName?: string); constructor(name: string, kind: SymbolKind, rangeOrContainer: string | undefined | Range, locationOrUri?: Location | URI, containerName?: string) { this.name = name; this.kind = kind; - this.containerName = containerName; + this.containerName = containerName ?? ''; if (typeof rangeOrContainer === 'string') { this.containerName = rangeOrContainer; @@ -4422,9 +4422,9 @@ export class ChatResponseFileTreePart { } export class ChatResponseAnchorPart { - value: vscode.Uri | vscode.Location; + value: vscode.Uri | vscode.Location | vscode.SymbolInformation; title?: string; - constructor(value: vscode.Uri | vscode.Location, title?: string) { + constructor(value: vscode.Uri | vscode.Location | vscode.SymbolInformation, title?: string) { this.value = value; this.title = title; } diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index d3e17a2ade9..95a0959888a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { applyDragImage } from '../../../../base/browser/dnd.js'; import * as dom from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; @@ -13,7 +12,8 @@ import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; import { URI } from '../../../../base/common/uri.js'; -import { Location } from '../../../../editor/common/languages.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { SymbolKinds } from '../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -23,11 +23,9 @@ import { IInstantiationService, ServicesAccessor } from '../../../../platform/in import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { listActiveSelectionBackground, listActiveSelectionForeground } from '../../../../platform/theme/common/colors/listColors.js'; import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { fillEditorsDragData } from '../../../browser/dnd.js'; -import { contentRefUrl } from '../common/annotations.js'; +import { ContentRefData, contentRefUrl } from '../common/annotations.js'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../common/chatColors.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestVariablePart, chatSubcommandLeader, IParsedChatRequest, IParsedChatRequestPart } from '../common/chatParserTypes.js'; @@ -36,7 +34,7 @@ import { IChatVariablesService } from '../common/chatVariables.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { IChatWidgetService } from './chat.js'; import { ChatAgentHover, getChatAgentHoverOptions } from './chatAgentHover.js'; -import './media/chatInlineFileLinkWidget.css'; +import './media/chatInlineAnchorWidget.css'; /** For rendering slash commands, variables */ const decorationRefUrl = `http://_vscodedecoration_`; @@ -241,20 +239,20 @@ export class ChatMarkdownDecorationsRenderer extends Disposable { private renderFileWidget(href: string, a: HTMLAnchorElement, store: DisposableStore): void { // TODO this can be a nicer FileLabel widget with an icon. Do a simple link for now. const fullUri = URI.parse(href); - let location: Location | { uri: URI; range: undefined }; + let data: ContentRefData; try { - location = revive(JSON.parse(fullUri.fragment)); + data = revive(JSON.parse(fullUri.fragment)); } catch (err) { this.logService.error('Invalid chat widget render data JSON', toErrorMessage(err)); return; } - if (!location.uri || !URI.isUri(location.uri)) { + if (data.kind !== 'symbol' && !URI.isUri(data.uri)) { this.logService.error(`Invalid chat widget render data: ${fullUri.fragment}`); return; } - store.add(this.instantiationService.createInstance(InlineFileLinkWidget, a, location)); + store.add(this.instantiationService.createInstance(InlineAnchorWidget, a, data)); } private renderResourceWidget(name: string, args: IDecorationWidgetArgs | undefined, store: DisposableStore): HTMLElement { @@ -284,47 +282,56 @@ export class ChatMarkdownDecorationsRenderer extends Disposable { } -class InlineFileLinkWidget extends Disposable { +class InlineAnchorWidget extends Disposable { constructor( element: HTMLAnchorElement, - location: Location | { uri: URI; range: undefined }, + data: ContentRefData, @IHoverService hoverService: IHoverService, @IInstantiationService instantiationService: IInstantiationService, @ILabelService labelService: ILabelService, @ILanguageService languageService: ILanguageService, @IModelService modelService: IModelService, - @IThemeService themeService: IThemeService, ) { super(); - element.classList.add('chat-inline-file-link-widget'); + element.classList.add('chat-inline-anchor-widget', 'show-file-icons'); + element.replaceChildren(); + + const resourceLabel = this._register(new IconLabel(element, { supportHighlights: false, supportIcons: true })); + + let location: { readonly uri: URI; readonly range?: IRange }; + if (data.kind === 'symbol') { + location = data.symbol.location; + + const icon = SymbolKinds.toIcon(data.symbol.kind); + resourceLabel.setLabel(`$(${icon.id}) ${data.symbol.name}`, undefined, {}); + } else { + location = data; + + const label = labelService.getUriBasenameLabel(location.uri); + const title = location.range && data.kind !== 'symbol' ? + `${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` : + label; + + resourceLabel.setLabel(title, undefined, { + extraClasses: getIconClasses(modelService, languageService, location.uri) + }); + } const fragment = location.range ? `${location.range.startLineNumber}-${location.range.endLineNumber}` : ''; element.setAttribute('data-href', location.uri.with({ fragment }).toString()); - const label = labelService.getUriLabel(location.uri, { relative: true }); - const title = location.range ? - `${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` : - label; - - element.replaceChildren(); - - const resourceLabel = this._register(new IconLabel(element, { supportHighlights: false, supportIcons: true })); - resourceLabel.setLabel(label, undefined, { - extraClasses: getIconClasses(modelService, languageService, location.uri) - }); - // Hover - this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title)); + const relativeLabel = labelService.getUriLabel(location.uri, { relative: true }); + this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, relativeLabel)); // Drag and drop element.draggable = true; this._register(dom.addDisposableListener(element, 'dragstart', e => { instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [location.uri], e)); - const theme = themeService.getColorTheme(); - applyDragImage(e, label, 'monaco-drag-image', theme.getColor(listActiveSelectionBackground)?.toString(), theme.getColor(listActiveSelectionForeground)?.toString()); + e.dataTransfer?.setDragImage(element, 0, 0); })); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css new file mode 100644 index 00000000000..cd4255fcefe --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-inline-anchor-widget, +.chat-inline-anchor-widget a { + color: inherit !important; +} + +.chat-inline-anchor-widget { + outline: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); + outline-offset: -1px; + border-radius: 4px; +} + +.chat-inline-anchor-widget .monaco-icon-label { + display: inline-flex; + margin: 0 1px; + vertical-align: text-bottom; + align-items: center; + line-height: normal; +} + +.chat-inline-anchor-widget .monaco-highlighted-label { + padding: 2px; + padding-right: 4px; +} + +.chat-inline-anchor-widget .codicon { + vertical-align: text-bottom; + color: reset-layer !important; +} + +.chat-inline-anchor-widget:hover .monaco-icon-label { + background-color: var(--vscode-list-hoverBackground); +} + +.chat-inline-anchor-widget .monaco-icon-label::before { + height: auto; + padding-right: 3px; +} + +.chat-inline-anchor-widget .monaco-icon-label .monaco-highlighted-label { + white-space: normal; +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatInlineFileLinkWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatInlineFileLinkWidget.css deleted file mode 100644 index d4746e3afe3..00000000000 --- a/src/vs/workbench/contrib/chat/browser/media/chatInlineFileLinkWidget.css +++ /dev/null @@ -1,12 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.chat-inline-file-link-widget .monaco-icon-label { - display: inline-flex; -} - -.chat-inline-file-link-widget .monaco-icon-label .monaco-highlighted-label { - white-space: normal; -} diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index 8aede0dc32b..5d0fd7db153 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -6,19 +6,42 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; +import { IWorkspaceSymbol } from '../../search/common/search.js'; import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from './chatModel.js'; import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from './chatService.js'; export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI +export type ContentRefData = + | { readonly kind: 'symbol'; readonly symbol: IWorkspaceSymbol } + | { + readonly kind?: undefined; + readonly uri: URI; + readonly range?: IRange; + }; + export function annotateSpecialMarkdownContent(response: ReadonlyArray): IChatProgressRenderableResponseContent[] { const result: IChatProgressRenderableResponseContent[] = []; for (const item of response) { const previousItem = result[result.length - 1]; if (item.kind === 'inlineReference') { - const location = 'uri' in item.inlineReference ? item.inlineReference : { uri: item.inlineReference }; + const location: ContentRefData = 'uri' in item.inlineReference + ? item.inlineReference + : 'name' in item.inlineReference + ? { kind: 'symbol', symbol: item.inlineReference } + : { uri: item.inlineReference }; + const printUri = URI.parse(contentRefUrl).with({ fragment: JSON.stringify(location) }); - const markdownText = `[${item.name || basename(location.uri)}](${printUri.toString()})`; + let label: string | undefined = item.name; + if (!label) { + if (location.kind === 'symbol') { + label = location.symbol.name; + } else { + label = basename(location.uri); + } + } + + const markdownText = `[${label}](${printUri.toString()})`; if (previousItem?.kind === 'markdownContent') { const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); result[result.length - 1] = { content: merged, kind: 'markdownContent' }; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 3952609480e..49535f0cc9c 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -289,11 +289,14 @@ export class Response extends Disposable implements IResponse { } private _updateRepr(quiet?: boolean) { + const inlineRefToRepr = (part: IChatContentInlineReference) => + 'uri' in part.inlineReference ? basename(part.inlineReference.uri) : 'name' in part.inlineReference ? part.inlineReference.name : basename(part.inlineReference); + this._responseRepr = this._responseParts.map(part => { if (part.kind === 'treeData') { return ''; } else if (part.kind === 'inlineReference') { - return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); + return inlineRefToRepr(part); } else if (part.kind === 'command') { return part.command.title; } else if (part.kind === 'textEditGroup') { @@ -313,7 +316,7 @@ export class Response extends Disposable implements IResponse { this._markdownContent = this._responseParts.map(part => { if (part.kind === 'inlineReference') { - return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); + return inlineRefToRepr(part); } else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { return part.content.value; } else { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index abb04d4a0bf..99edf364e6f 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -14,6 +14,7 @@ import { ISelection } from '../../../../editor/common/core/selection.js'; import { Command, Location, TextEdit } from '../../../../editor/common/languages.js'; import { FileType } from '../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkspaceSymbol } from '../../search/common/search.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from './chatAgents.js'; import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; @@ -96,7 +97,7 @@ export interface IChatCodeCitation { } export interface IChatContentInlineReference { - inlineReference: URI | Location; + inlineReference: URI | Location | IWorkspaceSymbol; name?: string; kind: 'inlineReference'; } diff --git a/src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts b/src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts new file mode 100644 index 00000000000..487a41e2956 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // Extended to add `SymbolInformation`. Would also be added to constructor + export interface ChatResponseAnchorPart { + /** + * The target of this anchor. + * + * If this is a {@linkcode Uri} or {@linkcode Location}, this is rendered as a normal link. + * + * If this is a {@linkcode SymbolInformation}, this is rendered as a symbol link. + */ + value: Uri | Location | SymbolInformation; + } +} From 25f80c94c62dab67c38222546f745e4711318aa1 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 6 Sep 2024 21:19:28 +0200 Subject: [PATCH 154/286] More detailed scopes in typescript.scm (#227828) Part of #210475 --- .../languages/highlights/typescript.scm | 84 ++++++++++++------- 1 file changed, 54 insertions(+), 30 deletions(-) diff --git a/src/vs/editor/common/languages/highlights/typescript.scm b/src/vs/editor/common/languages/highlights/typescript.scm index e50bc9dec97..4d3be97c142 100644 --- a/src/vs/editor/common/languages/highlights/typescript.scm +++ b/src/vs/editor/common/languages/highlights/typescript.scm @@ -15,6 +15,8 @@ ; Keywords +("typeof") @keyword.operator.expression.typeof + [ "delete" "in" @@ -22,7 +24,6 @@ "instanceof" "keyof" "of" - "typeof" ] @keyword.operator.expression [ @@ -91,7 +92,7 @@ [ "void" -] @support.type +] @support.type.primitive [ "new" @@ -108,56 +109,79 @@ "?" ] @punctuation.delimiter +[ + "!" + "~" + "===" + "!==" + "&&" + "||" + "??" +] @keyword.operator.logical + [ "-" - "--" - "-=" "+" - "++" - "+=" "*" - "*=" - "**" - "**=" "/" - "/=" "%" - "%=" + "^" +] @keyword.operator.arithmetic + +(binary_expression ([ "<" "<=" - "<<" - "<<=" - "=" - "==" - "===" - "!" - "!=" - "!==" - "=>" ">" ">=" +]) @keyword.operator.relational) + +[ + "=" +] @keyword.operator.assignment + +(augmented_assignment_expression ([ + "-=" + "+=" + "*=" + "/=" + "%=" + "^=" + "&=" + "|=" + "&&=" + "||=" + "??=" +]) @keyword.operator.assignment.compound) + +[ + "++" +] @keyword.operator.increment + +[ + "--" +] @keyword.operator.decrement + +[ + "**" + "**=" + "<<" + "<<=" + "==" + "!=" + "=>" ">>" ">>=" ">>>" ">>>=" "~" - "^" "&" "|" - "^=" - "&=" - "|=" - "&&" - "||" - "??" - "&&=" - "||=" - "??=" ] @keyword.operator ; Special identifiers (type_identifier) @entity.name.type +(predefined_type (["string" "boolean" "number"])) @support.type.primitive (predefined_type) @support.type (("const") From 077bd2f1b436949ca5d35590b41f8455c7f3b431 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Sep 2024 12:20:13 -0700 Subject: [PATCH 155/286] fixups - Don't set a config option for update interval. We don't let the one in core be configurable, don't see a huge need for this. - Don't start the server when the update is available - Fix some race conditions - Fix some clippy lints on Rust 1.81 --- cli/Cargo.toml | 1 + cli/src/auth.rs | 2 +- cli/src/commands/args.rs | 4 -- cli/src/commands/serve_web.rs | 100 ++++++++++----------------------- cli/src/download_cache.rs | 11 ---- cli/src/tunnels/code_server.rs | 2 +- 6 files changed, 33 insertions(+), 87 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b820ffcc50f..2907ff3d7e7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -81,4 +81,5 @@ codegen-units = 1 [features] default = [] +vsda = [] vscode-encrypt = [] diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 2d9162c5483..51942c96c75 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -723,7 +723,7 @@ impl Auth { match &init_code_json.message { Some(m) => self.log.result(m), - None => self.log.result(&format!( + None => self.log.result(format!( "To grant access to the server, please log into {} and use code {}", init_code_json.verification_uri, init_code_json.user_code )), diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 895dad08f95..101f1eac29f 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -222,10 +222,6 @@ pub struct ServeWebArgs { /// Set the root path for extensions. #[clap(long)] pub extensions_dir: Option, - /// Set update check interval in seconds, defaults to 3600 seconds. Set to 0 to disable update checks - #[clap(long)] - pub update_check_interval: Option, - } #[derive(Args, Debug, Clone)] diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 1e227402808..4acb9a18c73 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -15,7 +15,7 @@ use std::time::{Duration, Instant}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::{pin,time}; +use tokio::{pin, time}; use crate::async_pipe::{ get_socket_name, get_socket_rw_stream, listen_socket_rw_stream, AsyncPipe, @@ -50,7 +50,7 @@ const SERVER_IDLE_TIMEOUT_SECS: u64 = 60 * 60; /// (should be large enough to basically never happen) const SERVER_ACTIVE_TIMEOUT_SECS: u64 = SERVER_IDLE_TIMEOUT_SECS * 24 * 30 * 12; /// How long to cache the "latest" version we get from the update service. -const RELEASE_CACHE_SECS: u64 = 60 * 60; +const RELEASE_CHECK_INTERVAL: u64 = 60 * 60; /// Number of bytes for the secret keys. See workbench.ts for their usage. const SECRET_KEY_BYTES: usize = 32; @@ -87,13 +87,9 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result = ConnectionManager::new(&ctx, platform, args.clone()); - - let update_check_interval = args.update_check_interval.unwrap_or(3600); - - if update_check_interval > 0 { - // Start the update checker - cm.clone().start_update_checker(Duration::from_secs(update_check_interval)); - } + let update_check_interval = 3600; + cm.clone() + .start_update_checker(Duration::from_secs(update_check_interval)); let key = get_server_key_half(&ctx.paths); let make_svc = move || { @@ -546,34 +542,26 @@ impl ConnectionManager { pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { let base_path = normalize_base_path(args.server_base_path.as_deref().unwrap_or_default()); - let cache = DownloadCache::load(ctx.paths.web_server_storage()); - let latest_version: tokio::sync::Mutex>; + let cache = DownloadCache::new(ctx.paths.web_server_storage()); let target_kind = TargetKind::Web; - //Set the instant to now minus the RELEASE_CACHE_SECS - //This allows the service to skip use of the cache for the first run - let instant = Instant::now() - Duration::from_secs(RELEASE_CACHE_SECS); + let quality = VSCODE_CLI_QUALITY.map_or(Quality::Stable, |q| match Quality::try_from(q) { + Ok(q) => q, + Err(_) => Quality::Stable, + }); - let quality = VSCODE_CLI_QUALITY - .map_or(Quality::Stable, |q| { - match Quality::try_from(q) { - Ok(q) => q, - Err(_) => Quality::Stable - } - }); - - if let Some(latest_commit) = cache.get().first() { - let release = Release { - name: String::from("0.0.0"), // Version information not stored on cache - commit: latest_commit.clone(), - platform, - target: target_kind, - quality - }; - latest_version = tokio::sync::Mutex::new(Some((instant, release))); - } else { - latest_version = tokio::sync::Mutex::default(); - } + let latest_version = tokio::sync::Mutex::new(cache.get().first().map(|latest_commit| { + ( + Instant::now() - Duration::from_secs(RELEASE_CHECK_INTERVAL), + Release { + name: String::from("0.0.0"), // Version information not stored on cache + commit: latest_commit.clone(), + platform, + target: target_kind, + quality, + }, + ) + })); Arc::new(Self { platform, @@ -592,29 +580,27 @@ impl ConnectionManager { // spawns a task that checks for updates every n seconds duration pub fn start_update_checker(self: Arc, duration: Duration) { - debug!(self.log, "starting update checker"); tokio::spawn(async move { let mut interval = time::interval(duration); loop { interval.tick().await; - debug!(self.log, "checking for updates"); - match self.get_latest_release().await { - Ok(_) => {}, - Err(e) => { - error!(self.log, "error getting latest version: {}", e); - } - }; + + if let Err(e) = self.get_latest_release().await { + warning!(self.log, "error getting latest version: {}", e); + } } }); } - // Returns the latest release, available on the cache + // Returns the latest release from the cache, if one exists. pub async fn get_release_from_cache(&self) -> Result { let latest = self.latest_version.lock().await; if let Some((_, release)) = &*latest { return Ok(release.clone()); } - Err(CodeError::ServerNotYetDownloaded) + + drop(latest); + self.get_latest_release().await } /// Gets a connection to a server version @@ -656,32 +642,6 @@ impl ConnectionManager { return Ok(previous.clone()); } - // If the new release and previous release are different, download the new version - if let Ok(new_release) = &release { - let (_, previous_release) = latest.clone().unwrap_or((Instant::now(), Release { - name: String::from("0.0.0"), - commit: String::from("0.0.0"), - platform:self.platform, - target: target_kind, - quality - })); - if new_release.commit != previous_release.commit { - match self.get_version_data_inner(new_release.clone()) { - Ok(mut r) => { - match r.wait().await { - Ok(_) => {}, - Err(e) => { - info!(self.log, "{}", e); - } - }; - }, - Err(e) => { - info!(self.log, "{}", e); - } - } - } - } - let release = release?; debug!(self.log, "refreshed latest release: {}", release); *latest = Some((now, release.clone())); diff --git a/cli/src/download_cache.rs b/cli/src/download_cache.rs index 5a343315d86..cd02b02d75a 100644 --- a/cli/src/download_cache.rs +++ b/cli/src/download_cache.rs @@ -36,17 +36,6 @@ impl DownloadCache { } } - /// Gets an DownloadCache with previously persisted value if it exists - /// on the persistant storage, else returns a new DownloadCache. - pub fn load(path: PathBuf) -> DownloadCache { - let state = PersistedState::>::new(path.join(PERSISTED_STATE_FILE_NAME)); - state.load(); - DownloadCache { - state, - path, - } - } - /// Gets the value stored on the state pub fn get(&self) -> Vec { self.state.load() diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 0579f8ef0dc..465f6e24230 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -674,7 +674,7 @@ where let write_line = |line: &str| -> std::io::Result<()> { if let Some(mut f) = log_file.as_ref() { f.write_all(line.as_bytes())?; - f.write_all(&[b'\n'])?; + f.write_all(b"\n")?; } if write_directly { println!("{}", line); From 9d388bbf8bf9ac0c3c978ec84431eee302cb7712 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 6 Sep 2024 12:35:09 -0700 Subject: [PATCH 156/286] Align verified badge with first line of text (#227829) For #227682 --- src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index 29e38f48cad..75a0cfa7f39 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -44,6 +44,8 @@ .chat-agent-hover.verifiedPublisher .extension-verified-publisher { display: flex; + align-items: start; + margin-top: 1px; } .chat-agent-hover .chat-agent-hover-warning .codicon { From 48dde95787ae1faa06fa5a4545b1450712ba80ef Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 6 Sep 2024 13:03:41 -0700 Subject: [PATCH 157/286] update code symbols in outline (#227638) * lower limit for how many cells we can pre-cache symbols for, update cache when out of date * update tests * fix tests * pre-cache symbols for the goto symbols view * test precaching * reset cached flag --- .../contrib/outline/notebookOutline.ts | 26 ++++---- .../notebook/browser/notebook.contribution.ts | 2 + .../viewModel/notebookOutlineDataSource.ts | 14 ++--- .../viewModel/notebookOutlineEntryFactory.ts | 48 +++++++++++---- .../browser/contrib/notebookOutline.test.ts | 61 +++++++++++++------ .../notebookOutlineViewProviders.test.ts | 57 +++++++++++------ .../browser/contrib/notebookSymbols.test.ts | 34 ++++++++--- .../test/browser/testNotebookEditor.ts | 4 ++ 8 files changed, 167 insertions(+), 79 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 726ea7f950d..90839d12a98 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -521,7 +521,6 @@ export class NotebookCellOutline implements IOutline { private _breadcrumbsDataSource!: IBreadcrumbsDataSource; // view settings - private gotoShowCodeCellSymbols: boolean; private outlineShowCodeCellSymbols: boolean; // getters @@ -562,7 +561,6 @@ export class NotebookCellOutline implements IOutline { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, ) { - this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); this.initializeOutline(); @@ -633,8 +631,7 @@ export class NotebookCellOutline implements IOutline { // recompute symbols when the configuration changes (recompute state - and therefore recompute active - is also called within compute symbols) this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(NotebookSetting.gotoSymbolsAllSymbols) || e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols)) { - this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + if (e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols)) { this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); this.computeSymbols(); } @@ -700,13 +697,14 @@ export class NotebookCellOutline implements IOutline { } private async computeSymbols(cancelToken: CancellationToken = CancellationToken.None) { - if (this._target === OutlineTarget.QuickPick && this.gotoShowCodeCellSymbols) { - await this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); - } else if (this._target === OutlineTarget.OutlinePane && this.outlineShowCodeCellSymbols) { + if (this._target === OutlineTarget.OutlinePane && this.outlineShowCodeCellSymbols) { // No need to wait for this, we want the outline to show up quickly. - void this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); + void this.doComputeSymbols(cancelToken); } } + public async doComputeSymbols(cancelToken: CancellationToken): Promise { + await this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); + } private async delayedComputeSymbols() { this.delayerRecomputeState.cancel(); this.delayerRecomputeActive.cancel(); @@ -814,7 +812,7 @@ export class NotebookOutlineCreator implements IOutlineCreator reg.dispose(); @@ -824,8 +822,14 @@ export class NotebookOutlineCreator implements IOutlineCreator | undefined> { - return this._instantiationService.createInstance(NotebookCellOutline, editor, target); + async createOutline(editor: INotebookEditorPane, target: OutlineTarget, cancelToken: CancellationToken): Promise | undefined> { + const outline = this._instantiationService.createInstance(NotebookCellOutline, editor, target); + if (target === OutlineTarget.QuickPick) { + // The quickpick creates the outline on demand + // so we need to ensure the symbols are pre-cached before the entries are syncronously requested + await outline.doComputeSymbols(cancelToken); + } + return outline; } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 8e4349b8b50..4b1e25a025b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -127,6 +127,7 @@ import { DefaultFormatter } from '../../format/browser/formatActionsMultiple.js' import { NotebookMultiTextDiffEditor } from './diff/notebookMultiDiffEditor.js'; import { NotebookMultiDiffEditorInput } from './diff/notebookMultiDiffEditorInput.js'; import { getFormattedMetadataJSON } from '../common/model/notebookCellTextModel.js'; +import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './viewModel/notebookOutlineEntryFactory.js'; /*--------------------------------------------------------------------------------------------- */ @@ -790,6 +791,7 @@ registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingSe registerSingleton(INotebookKeymapService, NotebookKeymapService, InstantiationType.Delayed); registerSingleton(INotebookLoggingService, NotebookLoggingService, InstantiationType.Delayed); registerSingleton(INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory, InstantiationType.Delayed); +registerSingleton(INotebookOutlineEntryFactory, NotebookOutlineEntryFactory, InstantiationType.Delayed); const schemas: IJSONSchemaMap = {}; function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema }): x is IConfigurationPropertySchema { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts index c02dadb3196..57c4a840aa7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts @@ -11,12 +11,10 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IMarkerService } from '../../../../../platform/markers/common/markers.js'; import { IActiveNotebookEditor, INotebookEditor } from '../notebookBrowser.js'; import { CellKind } from '../../common/notebookCommon.js'; -import { INotebookExecutionStateService } from '../../common/notebookExecutionStateService.js'; import { OutlineChangeEvent, OutlineConfigKeys } from '../../../../services/outline/browser/outline.js'; import { OutlineEntry } from './OutlineEntry.js'; -import { IOutlineModelService } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { NotebookOutlineEntryFactory } from './notebookOutlineEntryFactory.js'; +import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './notebookOutlineEntryFactory.js'; export interface INotebookCellOutlineDataSource { readonly activeElement: OutlineEntry | undefined; @@ -34,16 +32,12 @@ export class NotebookCellOutlineDataSource implements INotebookCellOutlineDataSo private _entries: OutlineEntry[] = []; private _activeEntry?: OutlineEntry; - private readonly _outlineEntryFactory: NotebookOutlineEntryFactory; - constructor( private readonly _editor: INotebookEditor, - @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, - @IOutlineModelService private readonly _outlineModelService: IOutlineModelService, @IMarkerService private readonly _markerService: IMarkerService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @INotebookOutlineEntryFactory private readonly _outlineEntryFactory: NotebookOutlineEntryFactory ) { - this._outlineEntryFactory = new NotebookOutlineEntryFactory(this._notebookExecutionStateService); this.recomputeState(); } @@ -68,9 +62,9 @@ export class NotebookCellOutlineDataSource implements INotebookCellOutlineDataSo if (notebookCells) { const promises: Promise[] = []; // limit the number of cells so that we don't resolve an excessive amount of text models - for (const cell of notebookCells.slice(0, 100)) { + for (const cell of notebookCells.slice(0, 50)) { // gather all symbols asynchronously - promises.push(this._outlineEntryFactory.cacheSymbols(cell, this._outlineModelService, cancelToken)); + promises.push(this._outlineEntryFactory.cacheSymbols(cell, cancelToken)); } await Promise.allSettled(promises); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts index 28dc9ac2227..6c708a2fa67 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts @@ -14,6 +14,8 @@ import { CellKind } from '../../common/notebookCommon.js'; import { INotebookExecutionStateService } from '../../common/notebookExecutionStateService.js'; import { IRange } from '../../../../../editor/common/core/range.js'; import { SymbolKind } from '../../../../../editor/common/languages.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; export const enum NotebookOutlineConstants { NonHeaderOutlineLevel = 7, @@ -41,12 +43,25 @@ function getMarkdownHeadersInCellFallbackToHtmlTags(fullContent: string) { return headers; } -export class NotebookOutlineEntryFactory { +export const INotebookOutlineEntryFactory = createDecorator('INotebookOutlineEntryFactory'); + +export interface INotebookOutlineEntryFactory { + readonly _serviceBrand: undefined; + + getOutlineEntries(cell: ICellViewModel, index: number): OutlineEntry[]; + cacheSymbols(cell: ICellViewModel, cancelToken: CancellationToken): Promise; +} + +export class NotebookOutlineEntryFactory implements INotebookOutlineEntryFactory { + + declare readonly _serviceBrand: undefined; private cellOutlineEntryCache: Record = {}; private readonly cachedMarkdownOutlineEntries = new WeakMap(); constructor( - private readonly executionStateService: INotebookExecutionStateService + @INotebookExecutionStateService private readonly executionStateService: INotebookExecutionStateService, + @IOutlineModelService private readonly outlineModelService: IOutlineModelService, + @ITextModelService private readonly textModelService: ITextModelService ) { } public getOutlineEntries(cell: ICellViewModel, index: number): OutlineEntry[] { @@ -80,16 +95,16 @@ export class NotebookOutlineEntryFactory { const exeState = !isMarkdown && this.executionStateService.getCellExecution(cell.uri); let preview = content.trim(); - if (!isMarkdown && cell.model.textModel) { - const cachedEntries = this.cellOutlineEntryCache[cell.model.textModel.id]; + if (!isMarkdown) { + const cached = this.cellOutlineEntryCache[cell.id]; // Gathering symbols from the model is an async operation, but this provider is syncronous. // So symbols need to be precached before this function is called to get the full list. - if (cachedEntries) { + if (cached) { // push code cell entry that is a parent of cached symbols, always necessary. filtering for quickpick done in that provider. entries.push(new OutlineEntry(index++, NotebookOutlineConstants.NonHeaderOutlineLevel, cell, preview, !!exeState, exeState ? exeState.isPaused : false)); - cachedEntries.forEach((cached) => { - entries.push(new OutlineEntry(index++, cached.level, cell, cached.name, false, false, cached.range, cached.kind)); + cached.forEach((entry) => { + entries.push(new OutlineEntry(index++, entry.level, cell, entry.name, false, false, entry.range, entry.kind)); }); } } @@ -106,11 +121,20 @@ export class NotebookOutlineEntryFactory { return entries; } - public async cacheSymbols(cell: ICellViewModel, outlineModelService: IOutlineModelService, cancelToken: CancellationToken) { - const textModel = await cell.resolveTextModel(); - const outlineModel = await outlineModelService.getOrCreate(textModel, cancelToken); - const entries = createOutlineEntries(outlineModel.getTopLevelSymbols(), 8); - this.cellOutlineEntryCache[textModel.id] = entries; + public async cacheSymbols(cell: ICellViewModel, cancelToken: CancellationToken) { + if (cell.cellKind === CellKind.Markup) { + return; + } + + const ref = await this.textModelService.createModelReference(cell.uri); + try { + const textModel = ref.object.textEditorModel; + const outlineModel = await this.outlineModelService.getOrCreate(textModel, cancelToken); + const entries = createOutlineEntries(outlineModel.getTopLevelSymbols(), 8); + this.cellOutlineEntryCache[cell.id] = entries; + } finally { + ref.dispose(); + } } } diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts index 428a991cb6f..81042265ee8 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts @@ -16,22 +16,26 @@ import { CellKind, IOutputDto, NotebookCellMetadata } from '../../../common/note import { IActiveNotebookEditor, INotebookEditorPane } from '../../../browser/notebookBrowser.js'; import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { NotebookCellOutline } from '../../../browser/contrib/outline/notebookOutline.js'; +import { NotebookCellOutline, NotebookOutlineCreator } from '../../../browser/contrib/outline/notebookOutline.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; import { LanguageFeaturesService } from '../../../../../../editor/common/services/languageFeaturesService.js'; import { IEditorPaneSelectionChangeEvent } from '../../../../../common/editor.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from '../../../browser/viewModel/notebookOutlineEntryFactory.js'; suite('Notebook Outline', function () { let disposables: DisposableStore; let instantiationService: TestInstantiationService; + let symbolsCached: boolean; teardown(() => disposables.dispose()); ensureNoDisposablesAreLeakedInTestSuite(); setup(() => { + symbolsCached = false; disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); instantiationService.set(IEditorService, new class extends mock() { }); @@ -46,27 +50,39 @@ suite('Notebook Outline', function () { }); - function withNotebookOutline(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (outline: NotebookCellOutline, editor: IActiveNotebookEditor) => R): Promise { - return withTestNotebook(cells, (editor) => { + async function withNotebookOutline( + cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], + target: OutlineTarget, + callback: (outline: NotebookCellOutline, editor: IActiveNotebookEditor) => R, + ): Promise { + + return withTestNotebook(cells, async (editor) => { if (!editor.hasModel()) { assert.ok(false, 'MUST have active text editor'); } - const outline = instantiationService.createInstance(NotebookCellOutline, new class extends mock() { + const notebookEditorPane = new class extends mock() { override getControl() { return editor; } override onDidChangeModel: Event = Event.None; override onDidChangeSelection: Event = Event.None; - }, OutlineTarget.OutlinePane); + }; - disposables.add(outline); - return callback(outline, editor); + + const testOutlineEntryFactory = instantiationService.createInstance(NotebookOutlineEntryFactory) as any; + testOutlineEntryFactory.cacheSymbols = async () => { symbolsCached = true; }; + instantiationService.stub(INotebookOutlineEntryFactory, testOutlineEntryFactory); + + const outline = await instantiationService.createInstance(NotebookOutlineCreator).createOutline(notebookEditorPane, target, CancellationToken.None); + + disposables.add(outline!); + return callback(outline as NotebookCellOutline, editor); }); } test('basic', async function () { - await withNotebookOutline([], outline => { + await withNotebookOutline([], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements(), []); }); @@ -75,7 +91,7 @@ suite('Notebook Outline', function () { test('special characters in heading', async function () { await withNotebookOutline([ ['# Hellö & Hällo', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'Hellö & Hällo'); @@ -83,7 +99,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['# bold', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'bold'); @@ -93,7 +109,7 @@ suite('Notebook Outline', function () { test('Notebook falsely detects "empty cells"', async function () { await withNotebookOutline([ [' 的时代 ', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '的时代'); @@ -101,7 +117,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ [' ', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'empty cell'); @@ -109,7 +125,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['+++++[]{}--)(0 ', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '+++++[]{}--)(0'); @@ -117,7 +133,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['+++++[]{}--)(0 Hello **&^ ', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '+++++[]{}--)(0 Hello **&^'); @@ -125,7 +141,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['!@#$\n Überschrïft', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '!@#$'); @@ -135,7 +151,7 @@ suite('Notebook Outline', function () { test('Heading text defines entry label', async function () { return await withNotebookOutline([ ['foo\n # h1', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'h1'); @@ -145,7 +161,7 @@ suite('Notebook Outline', function () { test('Notebook outline ignores markdown headings #115200', async function () { await withNotebookOutline([ ['## h2 \n# h1', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 2); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'h2'); @@ -155,11 +171,20 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['## h2', 'md', CellKind.Markup], ['# h1', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 2); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'h2'); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[1].label, 'h1'); }); }); + + test('Symbols for goto quickpick are pre-cached', async function () { + await withNotebookOutline([ + ['a = 1\nb = 2', 'python', CellKind.Code] + ], OutlineTarget.QuickPick, outline => { + assert.ok(outline instanceof NotebookCellOutline); + assert.strictEqual(symbolsCached, true); + }); + }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts index 513458476e6..a7f35bacea3 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts @@ -20,6 +20,8 @@ import { NotebookOutlineEntryFactory } from '../../../browser/viewModel/notebook import { OutlineEntry } from '../../../browser/viewModel/OutlineEntry.js'; import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; import { MockDocumentSymbol } from '../testNotebookEditor.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; +import { URI } from '../../../../../../base/common/uri.js'; suite('Notebook Outline View Providers', function () { @@ -55,12 +57,27 @@ suite('Notebook Outline View Providers', function () { return 0; } }; + const textModelService = new class extends mock() { + override createModelReference(uri: URI) { + return Promise.resolve({ + object: { + textEditorModel: { + id: uri.toString(), + getVersionId() { return 1; } + } + }, + dispose() { } + } as IReference); + } + }; // #endregion // #region Helpers function createCodeCellViewModel(version: number = 1, source = '# code', textmodelId = 'textId') { return { + uri: { toString() { return textmodelId; } }, + id: textmodelId, textBuffer: { getLineCount() { return 0; } }, @@ -206,9 +223,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -249,9 +266,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -295,9 +312,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -338,9 +355,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -387,9 +404,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -446,9 +463,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -499,9 +516,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -552,9 +569,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -608,9 +625,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -659,9 +676,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts index c7deb045301..1eaf85e79f5 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts @@ -13,6 +13,9 @@ import { ICellViewModel } from '../../../browser/notebookBrowser.js'; import { NotebookOutlineEntryFactory } from '../../../browser/viewModel/notebookOutlineEntryFactory.js'; import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; import { MockDocumentSymbol } from '../testNotebookEditor.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IReference } from '../../../../../../base/common/lifecycle.js'; suite('Notebook Symbols', function () { ensureNoDisposablesAreLeakedInTestSuite(); @@ -42,9 +45,24 @@ suite('Notebook Symbols', function () { return 0; } }; + const textModelService = new class extends mock() { + override createModelReference(uri: URI) { + return Promise.resolve({ + object: { + textEditorModel: { + id: uri.toString(), + getVersionId() { return 1; } + } + }, + dispose() { } + } as IReference); + } + }; function createCellViewModel(version: number = 1, textmodelId = 'textId') { return { + id: textmodelId, + uri: { toString() { return textmodelId; } }, textBuffer: { getLineCount() { return 0; } }, @@ -65,7 +83,7 @@ suite('Notebook Symbols', function () { test('Cell without symbols cache', function () { setSymbolsForTextModel([{ name: 'var', range: {} }]); - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); assert.equal(entries.length, 1, 'no entries created'); @@ -74,10 +92,10 @@ suite('Notebook Symbols', function () { test('Cell with simple symbols', async function () { setSymbolsForTextModel([{ name: 'var1', range: {} }, { name: 'var2', range: {} }]); - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); const cell = createCellViewModel(); - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); const entries = entryFactory.getOutlineEntries(cell, 0); assert.equal(entries.length, 3, 'wrong number of outline entries'); @@ -96,10 +114,10 @@ suite('Notebook Symbols', function () { { name: 'root1', range: {}, children: [{ name: 'nested1', range: {} }, { name: 'nested2', range: {} }] }, { name: 'root2', range: {}, children: [{ name: 'nested1', range: {} }] } ]); - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); const cell = createCellViewModel(); - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); assert.equal(entries.length, 6, 'wrong number of outline entries'); @@ -119,12 +137,12 @@ suite('Notebook Symbols', function () { test('Multiple Cells with symbols', async function () { setSymbolsForTextModel([{ name: 'var1', range: {} }], '$1'); setSymbolsForTextModel([{ name: 'var2', range: {} }], '$2'); - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); const cell1 = createCellViewModel(1, '$1'); const cell2 = createCellViewModel(1, '$2'); - await entryFactory.cacheSymbols(cell1, outlineModelService, CancellationToken.None); - await entryFactory.cacheSymbols(cell2, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell1, CancellationToken.None); + await entryFactory.cacheSymbols(cell2, CancellationToken.None); const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), 0); const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), 0); diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index d23c98e25a9..de21fbf708a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -67,6 +67,8 @@ import { mainWindow } from '../../../../../base/browser/window.js'; import { TestCodeEditorService } from '../../../../../editor/test/browser/editorTestServices.js'; import { INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory } from '../../browser/viewModel/notebookOutlineDataSourceFactory.js'; import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js'; +import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from '../../browser/viewModel/notebookOutlineEntryFactory.js'; +import { IOutlineService } from '../../../../services/outline/browser/outline.js'; export class TestCell extends NotebookCellTextModel { constructor( @@ -199,7 +201,9 @@ export function setupInstantiationService(disposables: Pick() { override registerOutlineCreator() { return { dispose() { } }; } }); instantiationService.stub(INotebookCellOutlineDataSourceFactory, instantiationService.createInstance(NotebookCellOutlineDataSourceFactory)); + instantiationService.stub(INotebookOutlineEntryFactory, instantiationService.createInstance(NotebookOutlineEntryFactory)); instantiationService.stub(ILanguageDetectionService, new class MockLanguageDetectionService implements ILanguageDetectionService { _serviceBrand: undefined; From c6f85783f9a5bc70d9420a1fc9ce8895d5532f05 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 6 Sep 2024 22:45:56 +0200 Subject: [PATCH 158/286] Allow @vscode/tree-sitter-wasm type import in more places (#227831) Part of #227752 --- .eslintrc.json | 4 +++- .../editor/standalone/browser/standaloneTreeSitterService.ts | 1 - .../treeSitter/browser/treeSitterTokenizationFeature.ts | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index acea42273e7..4f0f7dc565e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -846,7 +846,8 @@ "vs/platform/*/~", "vs/editor/~", "vs/editor/contrib/*/~", - "vs/editor/standalone/~" + "vs/editor/standalone/~", + "@vscode/tree-sitter-wasm" // type import ] }, { @@ -932,6 +933,7 @@ "tas-client-umd", // node module allowed even in /common/ "vscode-textmate", // node module allowed even in /common/ "@vscode/vscode-languagedetection", // node module allowed even in /common/ + "@vscode/tree-sitter-wasm", // type import { "when": "hasBrowser", "pattern": "@xterm/xterm" diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts index aedfbdceb0d..90257943088 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// eslint-disable-next-line local/code-import-patterns import type { Parser } from '@vscode/tree-sitter-wasm'; import { Event } from '../../../base/common/event.js'; import { ITextModel } from '../../common/model.js'; diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index a20c8146c15..44b591daf4a 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// eslint-disable-next-line local/code-import-patterns import type { Parser } from '@vscode/tree-sitter-wasm'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; From f8c0820a97318b7c37eee5f433f4c181daab685c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Sep 2024 14:31:52 -0700 Subject: [PATCH 159/286] cli: fix vsda failure in main (#227835) Fixes #227833 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e23f76c32d..72dade9d86a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.94.0", - "distro": "1f99a7e25e381bc566d839c0259770ef5735d8ed", + "distro": "a8d95f75457d2afe29c673226304b87f11d87bc0", "author": { "name": "Microsoft Corporation" }, From 2c8764ef26b847beacfc3474707c13d7b5315b2b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Fri, 6 Sep 2024 15:03:57 -0700 Subject: [PATCH 160/286] add missing role to command center (#227824) fix #227569 --- src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 779187ad2c3..81db9ad010a 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -141,7 +141,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { override render(container: HTMLElement): void { super.render(container); container.classList.toggle('command-center-quick-pick'); - + container.role = 'button'; const action = this.action; // icon (search) From 03f6dab0ddbaf8cbb8409a32f3333eab46760e42 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Sep 2024 15:09:47 -0700 Subject: [PATCH 161/286] chore: bump distro (#227840) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72dade9d86a..149a219512b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.94.0", - "distro": "a8d95f75457d2afe29c673226304b87f11d87bc0", + "distro": "fcaeb73de7ac6ff11a3e732c63987e01b88341e7", "author": { "name": "Microsoft Corporation" }, From 35676d1921521d81c021ee9549583fd2a4c35451 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Sep 2024 16:03:56 -0700 Subject: [PATCH 162/286] extensions: allow opening extension host CPU profile in the built-in profile viewer (#227834) Fixes #227168 Also moves some old Action registrations to Action2 and uses proper menu registration for context menu display. --- src/vs/platform/actions/common/actions.ts | 1 + .../abstractRuntimeExtensionsEditor.ts | 33 +-- .../browser/browserRuntimeExtensionsEditor.ts | 8 - .../debugExtensionHostAction.ts | 85 ++++--- .../extensionProfileService.ts | 3 + .../extensions.contribution.ts | 122 +++------- .../runtimeExtensionsEditor.ts | 227 ++++++++++++------ 7 files changed, 242 insertions(+), 237 deletions(-) diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 62d68f28f16..a89d42ec7a9 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -82,6 +82,7 @@ export class MenuId { static readonly ExplorerContext = new MenuId('ExplorerContext'); static readonly ExplorerContextShare = new MenuId('ExplorerContextShare'); static readonly ExtensionContext = new MenuId('ExtensionContext'); + static readonly ExtensionEditorContextMenu = new MenuId('ExtensionEditorContextMenu'); static readonly GlobalActivity = new MenuId('GlobalActivity'); static readonly CommandCenter = new MenuId('CommandCenter'); static readonly CommandCenterCenter = new MenuId('CommandCenterCenter'); diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 7e7ce15578c..f6873bf5524 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -13,13 +13,12 @@ import { Action, IAction, Separator } from '../../../../base/common/actions.js'; import { isNonEmptyArray } from '../../../../base/common/arrays.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { fromNow } from '../../../../base/common/date.js'; -import { memoize } from '../../../../base/common/decorators.js'; import { IDisposable, dispose } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; -import './media/runtimeExtensionsEditor.css'; import * as nls from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Action2, MenuId } from '../../../../platform/actions/common/actions.js'; +import { createAndFillInContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -35,9 +34,6 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; -import { errorIcon, warningIcon } from './extensionsIcons.js'; -import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; -import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; @@ -45,6 +41,10 @@ import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegi import { DefaultIconPath, EnablementState } from '../../../services/extensionManagement/common/extensionManagement.js'; import { LocalWebWorkerRunningLocation } from '../../../services/extensions/common/extensionRunningLocation.js'; import { IExtensionHostProfile, IExtensionService, IExtensionsStatus } from '../../../services/extensions/common/extensions.js'; +import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; +import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; +import { errorIcon, warningIcon } from './extensionsIcons.js'; +import './media/runtimeExtensionsEditor.css'; interface IExtensionProfileInformation { /** @@ -81,7 +81,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly _extensionService: IExtensionService, @INotificationService private readonly _notificationService: INotificationService, @@ -93,6 +93,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { @IClipboardService private readonly _clipboardService: IClipboardService, @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IHoverService private readonly _hoverService: IHoverService, + @IMenuService private readonly _menuService: IMenuService, ) { super(AbstractRuntimeExtensionsEditor.ID, group, telemetryService, themeService, storageService); @@ -496,14 +497,9 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } actions.push(new Separator()); - const profileAction = this._createProfileAction(); - if (profileAction) { - actions.push(profileAction); - } - const saveExtensionHostProfileAction = this.saveExtensionHostProfileAction; - if (saveExtensionHostProfileAction) { - actions.push(saveExtensionHostProfileAction); - } + + const menuActions = this._menuService.getMenuActions(MenuId.ExtensionEditorContextMenu, this.contextKeyService); + createAndFillInContextMenuActions(menuActions, { primary: [], secondary: actions }); this._contextMenuService.showContextMenu({ getAnchor: () => e.anchor, @@ -512,11 +508,6 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { }); } - @memoize - private get saveExtensionHostProfileAction(): IAction | null { - return this._createSaveExtensionHostProfileAction(); - } - public layout(dimension: Dimension): void { this._list?.layout(dimension.height); } @@ -525,8 +516,6 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { protected abstract _getUnresponsiveProfile(extensionId: ExtensionIdentifier): IExtensionHostProfile | undefined; protected abstract _createSlowExtensionAction(element: IRuntimeExtension): Action | null; protected abstract _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null; - protected abstract _createSaveExtensionHostProfileAction(): Action | null; - protected abstract _createProfileAction(): Action | null; } export class ShowRuntimeExtensionsAction extends Action2 { diff --git a/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts index 57e32f123d4..e7fd431b5a7 100644 --- a/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts @@ -29,12 +29,4 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { } return null; } - - protected _createSaveExtensionHostProfileAction(): Action | null { - return null; - } - - protected _createProfileAction(): Action | null { - return null; - } } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts index 9da3189d7da..71291a40f8e 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts @@ -3,62 +3,73 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from '../../../../base/common/actions.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { randomPort } from '../../../../base/common/ports.js'; import * as nls from '../../../../nls.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { Action2, MenuId } from '../../../../platform/actions/common/actions.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ActiveEditorContext } from '../../../common/contextkeys.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IConfig, IDebugService } from '../../debug/common/debug.js'; import { ExtensionHostKind } from '../../../services/extensions/common/extensionHostKind.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IHostService } from '../../../services/host/browser/host.js'; +import { IConfig, IDebugService } from '../../debug/common/debug.js'; +import { RuntimeExtensionsEditor } from './runtimeExtensionsEditor.js'; -export class DebugExtensionHostAction extends Action { - static readonly ID = 'workbench.extensions.action.debugExtensionHost'; - static readonly LABEL = nls.localize('debugExtensionHost', "Start Debugging Extension Host In New Window"); - static readonly CSS_CLASS = 'debug-extension-host'; - - constructor( - @INativeHostService private readonly _nativeHostService: INativeHostService, - @IDialogService private readonly _dialogService: IDialogService, - @IExtensionService private readonly _extensionService: IExtensionService, - @IProductService private readonly productService: IProductService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IHostService private readonly _hostService: IHostService, - ) { - super(DebugExtensionHostAction.ID, DebugExtensionHostAction.LABEL, DebugExtensionHostAction.CSS_CLASS); +export class DebugExtensionHostAction extends Action2 { + constructor() { + super({ + id: 'workbench.extensions.action.debugExtensionHost', + title: { value: nls.localize('debugExtensionHost', "Start Debugging Extension Host In New Window"), original: 'Start Debugging Extension Host In New Window' }, + category: Categories.Developer, + f1: true, + icon: Codicon.debugStart, + menu: { + id: MenuId.EditorTitle, + when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), + group: 'navigation', + } + }); } - override async run(_args: unknown): Promise { - const inspectPorts = await this._extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, false); - if (inspectPorts.length === 0) { - const res = await this._dialogService.confirm({ - message: nls.localize('restart1', "Debug Extensions"), - detail: nls.localize('restart2', "In order to debug extensions a restart is required. Do you want to restart '{0}' now?", this.productService.nameLong), - primaryButton: nls.localize({ key: 'restart3', comment: ['&& denotes a mnemonic'] }, "&&Restart") - }); - if (res.confirmed) { - await this._nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] }); + run(accessor: ServicesAccessor): void { + const nativeHostService = accessor.get(INativeHostService); + const dialogService = accessor.get(IDialogService); + const extensionService = accessor.get(IExtensionService); + const productService = accessor.get(IProductService); + const instantiationService = accessor.get(IInstantiationService); + const hostService = accessor.get(IHostService); + + extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, false).then(async inspectPorts => { + if (inspectPorts.length === 0) { + const res = await dialogService.confirm({ + message: nls.localize('restart1', "Debug Extensions"), + detail: nls.localize('restart2', "In order to debug extensions a restart is required. Do you want to restart '{0}' now?", productService.nameLong), + primaryButton: nls.localize({ key: 'restart3', comment: ['&& denotes a mnemonic'] }, "&&Restart") + }); + if (res.confirmed) { + await nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] }); + } + return; } - return; - } + if (inspectPorts.length > 1) { + // TODO + console.warn(`There are multiple extension hosts available for debugging. Picking the first one...`); + } - if (inspectPorts.length > 1) { - // TODO - console.warn(`There are multiple extension hosts available for debugging. Picking the first one...`); - } + const s = instantiationService.createInstance(Storage); + s.storeDebugOnNewWindow(inspectPorts[0].port); - const s = this._instantiationService.createInstance(Storage); - s.storeDebugOnNewWindow(inspectPorts[0].port); - - this._hostService.openWindow(); + hostService.openWindow(); + }); } } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts index b7f8774a575..99057a3464c 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts @@ -23,6 +23,7 @@ import { ExtensionHostKind } from '../../../services/extensions/common/extension import { IExtensionHostProfile, IExtensionService, ProfileSession } from '../../../services/extensions/common/extensions.js'; import { ExtensionHostProfiler } from '../../../services/extensions/electron-sandbox/extensionHostProfiler.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; +import { URI } from '../../../../base/common/uri.js'; export class ExtensionHostProfileService extends Disposable implements IExtensionHostProfileService { @@ -42,6 +43,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio private profilingStatusBarIndicator: IStatusbarEntryAccessor | undefined; private readonly profilingStatusBarIndicatorLabelUpdater = this._register(new MutableDisposable()); + public lastProfileSavedTo: URI | undefined; public get state() { return this._state; } public get lastProfile() { return this._profile; } @@ -166,6 +168,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio private _setLastProfile(profile: IExtensionHostProfile) { this._profile = profile; + this.lastProfileSavedTo = undefined; this._onDidChangeLastProfile.fire(undefined); } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index 44bebc97bdd..021c893c2b6 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -3,32 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../nls.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { MenuRegistry, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; -import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { ServicesAccessor, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; -import { RuntimeExtensionsEditor, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction, IExtensionHostProfileService } from './runtimeExtensionsEditor.js'; -import { DebugExtensionHostAction, DebugExtensionsContribution } from './debugExtensionHostAction.js'; -import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions } from '../../../common/editor.js'; -import { ActiveEditorContext } from '../../../common/contextkeys.js'; -import { EditorInput } from '../../../common/editor/editorInput.js'; -import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { CleanUpExtensionsFolderAction, OpenExtensionsFolderAction } from './extensionsActions.js'; -import { IExtensionRecommendationNotificationService } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; -import { ISharedProcessService } from '../../../../platform/ipc/electron-sandbox/services.js'; -import { ExtensionRecommendationNotificationServiceChannel } from '../../../../platform/extensionRecommendations/common/extensionRecommendationsIpc.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionHostProfileService } from './extensionProfileService.js'; -import { ExtensionsAutoProfiler } from './extensionsAutoProfiler.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IExtensionRecommendationNotificationService } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; +import { ExtensionRecommendationNotificationServiceChannel } from '../../../../platform/extensionRecommendations/common/extensionRecommendationsIpc.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ISharedProcessService } from '../../../../platform/ipc/electron-sandbox/services.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../common/contributions.js'; +import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../common/editor.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; +import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; +import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; +import { DebugExtensionHostAction, DebugExtensionsContribution } from './debugExtensionHostAction.js'; +import { ExtensionHostProfileService } from './extensionProfileService.js'; +import { CleanUpExtensionsFolderAction, OpenExtensionsFolderAction } from './extensionsActions.js'; +import { ExtensionsAutoProfiler } from './extensionsAutoProfiler.js'; +import { RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js'; +import { IExtensionHostProfileService, OpenExtensionHostProfileACtion, RuntimeExtensionsEditor, SaveExtensionHostProfileAction, StartExtensionHostProfileAction, StopExtensionHostProfileAction } from './runtimeExtensionsEditor.js'; // Singletons registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, InstantiationType.Delayed); @@ -76,76 +72,12 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, Lifecyc workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInitializerContribution, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(DebugExtensionsContribution, LifecyclePhase.Restored); + // Register Commands -CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor, ...args) => { - const instantiationService = accessor.get(IInstantiationService); - return instantiationService.createInstance(DebugExtensionHostAction).run(args); -}); +registerAction2(DebugExtensionHostAction); +registerAction2(StartExtensionHostProfileAction); +registerAction2(StopExtensionHostProfileAction); +registerAction2(SaveExtensionHostProfileAction); +registerAction2(OpenExtensionHostProfileACtion); -CommandsRegistry.registerCommand(StartExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL).run(); -}); - -CommandsRegistry.registerCommand(StopExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL).run(); -}); - -CommandsRegistry.registerCommand(SaveExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL).run(); -}); - -// Running extensions - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: DebugExtensionHostAction.ID, - title: DebugExtensionHostAction.LABEL, - icon: Codicon.debugStart - }, - group: 'navigation', - when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID) -}); - -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: DebugExtensionHostAction.ID, - title: localize('debugExtensionHost', "Debug Extensions In New Window"), - category: localize('developer', "Developer"), - icon: Codicon.debugStart - }, -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: StartExtensionHostProfileAction.ID, - title: StartExtensionHostProfileAction.LABEL, - icon: Codicon.circleFilled - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running')) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: StopExtensionHostProfileAction.ID, - title: StopExtensionHostProfileAction.LABEL, - icon: Codicon.debugStop - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running')) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: SaveExtensionHostProfileAction.ID, - title: SaveExtensionHostProfileAction.LABEL, - icon: Codicon.saveAll, - precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)) -}); diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index 2f54dbdb86a..ac1b7ad2bd8 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -3,35 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from '../../../../nls.js'; import { Action } from '../../../../base/common/actions.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IInstantiationService, createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IExtensionsWorkbenchService } from '../common/extensions.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IExtensionService, IExtensionHostProfile } from '../../../services/extensions/common/extensions.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { Event } from '../../../../base/common/event.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IContextKeyService, RawContextKey, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IStorageService } from '../../../../platform/storage/common/storage.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { SlowExtensionAction } from './extensionsSlowActions.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { ReportExtensionIssueAction } from '../common/reportExtensionIssueAction.js'; -import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from '../browser/abstractRuntimeExtensionsEditor.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IV8Profile, Utils } from '../../../../platform/profiling/common/profiling.js'; -import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; -import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Event } from '../../../../base/common/event.js'; import { Schemas } from '../../../../base/common/network.js'; import { joinPath } from '../../../../base/common/resources.js'; -import { IExtensionFeaturesManagementService } from '../../../services/extensionManagement/common/extensionFeatures.js'; -import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { URI } from '../../../../base/common/uri.js'; +import * as nls from '../../../../nls.js'; +import { Action2, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService, ServicesAccessor, createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IV8Profile, Utils } from '../../../../platform/profiling/common/profiling.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { ActiveEditorContext } from '../../../common/contextkeys.js'; +import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IExtensionFeaturesManagementService } from '../../../services/extensionManagement/common/extensionFeatures.js'; +import { IExtensionHostProfile, IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from '../browser/abstractRuntimeExtensionsEditor.js'; +import { IExtensionsWorkbenchService } from '../common/extensions.js'; +import { ReportExtensionIssueAction } from '../common/reportExtensionIssueAction.js'; +import { SlowExtensionAction } from './extensionsSlowActions.js'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -52,6 +57,7 @@ export interface IExtensionHostProfileService { readonly state: ProfileSessionState; readonly lastProfile: IExtensionHostProfile | null; + lastProfileSavedTo: URI | undefined; startProfiling(): void; stopProfiling(): void; @@ -82,9 +88,10 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { @IClipboardService clipboardService: IClipboardService, @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, - @IHoverService hoverService: IHoverService + @IHoverService hoverService: IHoverService, + @IMenuService menuService: IMenuService, ) { - super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService, hoverService); + super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService, hoverService, menuService); this._profileInfo = this._extensionHostProfileService.lastProfile; this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService); this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService); @@ -121,83 +128,150 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { } return null; } - - protected _createSaveExtensionHostProfileAction(): Action | null { - return this._instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL); - } - - protected _createProfileAction(): Action | null { - const state = this._extensionHostProfileService.state; - const profileAction = ( - state === ProfileSessionState.Running - ? this._instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL) - : this._instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL) - ); - return profileAction; - } } -export class StartExtensionHostProfileAction extends Action { +export class StartExtensionHostProfileAction extends Action2 { static readonly ID = 'workbench.extensions.action.extensionHostProfile'; static readonly LABEL = nls.localize('extensionHostProfileStart', "Start Extension Host Profile"); - constructor( - id: string = StartExtensionHostProfileAction.ID, label: string = StartExtensionHostProfileAction.LABEL, - @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, - ) { - super(id, label); + constructor() { + super({ + id: StartExtensionHostProfileAction.ID, + title: { value: StartExtensionHostProfileAction.LABEL, original: 'Start Extension Host Profile' }, + precondition: CONTEXT_PROFILE_SESSION_STATE.isEqualTo('none'), + icon: Codicon.circleFilled, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running')), + group: 'navigation', + }, { + id: MenuId.ExtensionEditorContextMenu, + when: CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running'), + group: 'profiling', + }] + }); } - override run(): Promise { - this._extensionHostProfileService.startProfiling(); + run(accessor: ServicesAccessor): Promise { + const extensionHostProfileService = accessor.get(IExtensionHostProfileService); + extensionHostProfileService.startProfiling(); return Promise.resolve(); } } -export class StopExtensionHostProfileAction extends Action { +export class StopExtensionHostProfileAction extends Action2 { static readonly ID = 'workbench.extensions.action.stopExtensionHostProfile'; static readonly LABEL = nls.localize('stopExtensionHostProfileStart', "Stop Extension Host Profile"); - constructor( - id: string = StartExtensionHostProfileAction.ID, label: string = StartExtensionHostProfileAction.LABEL, - @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, - ) { - super(id, label); + constructor() { + super({ + id: StopExtensionHostProfileAction.ID, + title: { value: StopExtensionHostProfileAction.LABEL, original: 'Stop Extension Host Profile' }, + icon: Codicon.debugStop, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running')), + group: 'navigation', + }, { + id: MenuId.ExtensionEditorContextMenu, + when: CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running'), + group: 'profiling', + }] + }); } - override run(): Promise { - this._extensionHostProfileService.stopProfiling(); + run(accessor: ServicesAccessor): Promise { + const extensionHostProfileService = accessor.get(IExtensionHostProfileService); + extensionHostProfileService.stopProfiling(); return Promise.resolve(); } } -export class SaveExtensionHostProfileAction extends Action { +export class OpenExtensionHostProfileACtion extends Action2 { + static readonly LABEL = nls.localize('openExtensionHostProfile', "Open Extension Host Profile"); + static readonly ID = 'workbench.extensions.action.openExtensionHostProfile'; + + constructor() { + super({ + id: OpenExtensionHostProfileACtion.ID, + title: { value: OpenExtensionHostProfileACtion.LABEL, original: 'Open Extension Host Profile' }, + precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, + icon: Codicon.graph, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)), + group: 'navigation', + }, { + id: MenuId.ExtensionEditorContextMenu, + when: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, + group: 'profiling', + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const extensionHostProfileService = accessor.get(IExtensionHostProfileService); + const commandService = accessor.get(ICommandService); + const editorService = accessor.get(IEditorService); + if (!extensionHostProfileService.lastProfileSavedTo) { + await commandService.executeCommand(SaveExtensionHostProfileAction.ID); + } + if (!extensionHostProfileService.lastProfileSavedTo) { + return; + } + + await editorService.openEditor({ + resource: extensionHostProfileService.lastProfileSavedTo, + options: { + revealIfOpened: true, + override: 'jsProfileVisualizer.cpuprofile.table', + }, + }, SIDE_GROUP); + } + +} + +export class SaveExtensionHostProfileAction extends Action2 { static readonly LABEL = nls.localize('saveExtensionHostProfile', "Save Extension Host Profile"); static readonly ID = 'workbench.extensions.action.saveExtensionHostProfile'; - constructor( - id: string = SaveExtensionHostProfileAction.ID, label: string = SaveExtensionHostProfileAction.LABEL, - @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, - @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, - @IFileService private readonly _fileService: IFileService, - @IFileDialogService private readonly _fileDialogService: IFileDialogService, - ) { - super(id, label, undefined, false); - this._extensionHostProfileService.onDidChangeLastProfile(() => { - this.enabled = (this._extensionHostProfileService.lastProfile !== null); + constructor() { + super({ + id: SaveExtensionHostProfileAction.ID, + title: { value: SaveExtensionHostProfileAction.LABEL, original: 'Save Extension Host Profile' }, + precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, + icon: Codicon.saveAll, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)), + group: 'navigation', + }, { + id: MenuId.ExtensionEditorContextMenu, + when: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, + group: 'profiling', + }] }); } - override run(): Promise { - return Promise.resolve(this._asyncRun()); + run(accessor: ServicesAccessor): Promise { + const environmentService = accessor.get(IWorkbenchEnvironmentService); + const extensionHostProfileService = accessor.get(IExtensionHostProfileService); + const fileService = accessor.get(IFileService); + const fileDialogService = accessor.get(IFileDialogService); + return this._asyncRun(environmentService, extensionHostProfileService, fileService, fileDialogService); } - private async _asyncRun(): Promise { - const picked = await this._fileDialogService.showSaveDialog({ + private async _asyncRun( + environmentService: IWorkbenchEnvironmentService, + extensionHostProfileService: IExtensionHostProfileService, + fileService: IFileService, + fileDialogService: IFileDialogService + ): Promise { + const picked = await fileDialogService.showSaveDialog({ title: nls.localize('saveprofile.dialogTitle', "Save Extension Host Profile"), availableFileSystems: [Schemas.file], - defaultUri: joinPath(await this._fileDialogService.defaultFilePath(), `CPU-${new Date().toISOString().replace(/[\-:]/g, '')}.cpuprofile`), + defaultUri: joinPath(await fileDialogService.defaultFilePath(), `CPU-${new Date().toISOString().replace(/[\-:]/g, '')}.cpuprofile`), filters: [{ name: 'CPU Profiles', extensions: ['cpuprofile', 'txt'] @@ -208,12 +282,12 @@ export class SaveExtensionHostProfileAction extends Action { return; } - const profileInfo = this._extensionHostProfileService.lastProfile; + const profileInfo = extensionHostProfileService.lastProfile; let dataToWrite: object = profileInfo ? profileInfo.data : {}; let savePath = picked.fsPath; - if (this._environmentService.isBuilt) { + if (environmentService.isBuilt) { // when running from a not-development-build we remove // absolute filenames because we don't want to reveal anything // about users. We also append the `.txt` suffix to make it @@ -223,6 +297,9 @@ export class SaveExtensionHostProfileAction extends Action { savePath = savePath + '.txt'; } - return this._fileService.writeFile(URI.file(savePath), VSBuffer.fromString(JSON.stringify(profileInfo ? profileInfo.data : {}, null, '\t'))); + const saveURI = URI.file(savePath); + extensionHostProfileService.lastProfileSavedTo = saveURI; + return fileService.writeFile(saveURI, VSBuffer.fromString(JSON.stringify(profileInfo ? profileInfo.data : {}, null, '\t'))); } } + From f8785f80214be4fc18876476ee3863ea406e7ffb Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Sep 2024 16:04:32 -0700 Subject: [PATCH 163/286] debug: fix some unsoundness in type convertsions in the ext host (#227846) Refs #211878 --- .../api/common/extHostDebugService.ts | 90 +++++++++++-------- .../workbench/api/node/extHostDebugService.ts | 37 ++++---- .../workbench/contrib/debug/common/debug.ts | 1 - 3 files changed, 69 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index d072857559c..fe33d966e66 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -3,34 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as vscode from 'vscode'; +import { coalesce } from '../../../base/common/arrays.js'; import { asPromise } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; import { Disposable as DisposableCls, toDisposable } from '../../../base/common/lifecycle.js'; +import { ThemeIcon as ThemeIconUtils } from '../../../base/common/themables.js'; +import { URI, UriComponents } from '../../../base/common/uri.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { ISignService } from '../../../platform/sign/common/sign.js'; import { IWorkspaceFolder } from '../../../platform/workspace/common/workspace.js'; -import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThreadFocusDto, IStackFrameFocusDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape } from './extHost.protocol.js'; -import { IExtHostEditorTabs } from './extHostEditorTabs.js'; -import { IExtHostExtensionService } from './extHostExtensionService.js'; -import { IExtHostRpcService } from './extHostRpcService.js'; -import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, DebugThread, DebugStackFrame, ThemeIcon } from './extHostTypes.js'; -import { IExtHostWorkspace } from './extHostWorkspace.js'; import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js'; -import { MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebugVisualization, IDebugVisualizationContext, IDebuggerContribution, DebugVisualizationType, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; +import { DebugVisualizationType, IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterImpl, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { convertToDAPaths, convertToVSCPaths, isDebuggerMainContribution } from '../../contrib/debug/common/debugUtils.js'; import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; -import type * as vscode from 'vscode'; -import { IExtHostConfiguration } from './extHostConfiguration.js'; -import { IExtHostVariableResolverProvider } from './extHostVariableResolverService.js'; -import { ThemeIcon as ThemeIconUtils } from '../../../base/common/themables.js'; +import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, IStackFrameFocusDto, IThreadFocusDto, MainContext, MainThreadDebugServiceShape } from './extHost.protocol.js'; import { IExtHostCommands } from './extHostCommands.js'; -import * as Convert from './extHostTypeConverters.js'; -import { coalesce } from '../../../base/common/arrays.js'; +import { IExtHostConfiguration } from './extHostConfiguration.js'; +import { IExtHostEditorTabs } from './extHostEditorTabs.js'; +import { IExtHostExtensionService } from './extHostExtensionService.js'; +import { IExtHostRpcService } from './extHostRpcService.js'; import { IExtHostTesting } from './extHostTesting.js'; +import * as Convert from './extHostTypeConverters.js'; +import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, DebugStackFrame, DebugThread, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, ThemeIcon } from './extHostTypes.js'; +import { IExtHostVariableResolverProvider } from './extHostVariableResolverService.js'; +import { IExtHostWorkspace } from './extHostWorkspace.js'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -578,8 +578,8 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I return variableResolver.resolveAnyAsync(ws, config); } - protected createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { - if (adapter.type === 'implementation') { + protected createDebugAdapter(adapter: vscode.DebugAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { + if (adapter instanceof DebugAdapterInlineImplementation) { return new DirectDebugAdapter(adapter.implementation); } return undefined; @@ -600,9 +600,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I throw new Error(`Couldn't find a debug adapter descriptor for debug type '${session.type}' (extension might have failed to activate)`); } - const adapterDescriptor = this.convertToDto(daDescriptor); - - const da = this.createDebugAdapter(adapterDescriptor, session); + const da = this.createDebugAdapter(daDescriptor, session); if (!da) { throw new Error(`Couldn't create a debug adapter for type '${session.type}'.`); } @@ -891,35 +889,49 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I // private & dto helpers private convertToDto(x: vscode.DebugAdapterDescriptor): Dto { - if (x instanceof DebugAdapterExecutable) { - return { - type: 'executable', - command: x.command, - args: x.args, - options: x.options - } satisfies IDebugAdapterExecutable; + return this.convertExecutableToDto(x); } else if (x instanceof DebugAdapterServer) { - return { - type: 'server', - port: x.port, - host: x.host - } satisfies IDebugAdapterServer; + return this.convertServerToDto(x); } else if (x instanceof DebugAdapterNamedPipeServer) { - return { - type: 'pipeServer', - path: x.path - } satisfies IDebugAdapterNamedPipeServer; + return this.convertPipeServerToDto(x); } else if (x instanceof DebugAdapterInlineImplementation) { - return { - type: 'implementation', - implementation: x.implementation - } as Dto; + return this.convertImplementationToDto(x); } else { throw new Error('convertToDto unexpected type'); } } + protected convertExecutableToDto(x: DebugAdapterExecutable): IDebugAdapterExecutable { + return { + type: 'executable', + command: x.command, + args: x.args, + options: x.options + }; + } + + protected convertServerToDto(x: DebugAdapterServer): IDebugAdapterServer { + return { + type: 'server', + port: x.port, + host: x.host + }; + } + + protected convertPipeServerToDto(x: DebugAdapterNamedPipeServer): IDebugAdapterNamedPipeServer { + return { + type: 'pipeServer', + path: x.path + }; + } + + protected convertImplementationToDto(x: DebugAdapterInlineImplementation): IDebugAdapterImpl { + return { + type: 'implementation', + }; + } + private getAdapterDescriptorFactoryByType(type: string): vscode.DebugAdapterDescriptorFactory | undefined { const results = this._adapterFactories.filter(p => p.type === type); if (results.length > 0) { diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index ed9f4f68826..33e1b4d34ac 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as vscode from 'vscode'; import { createCancelablePromise, firstParallel, timeout } from '../../../base/common/async.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; import * as platform from '../../../base/common/platform.js'; @@ -11,23 +12,21 @@ import { IExternalTerminalService } from '../../../platform/externalTerminal/com import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from '../../../platform/externalTerminal/node/externalTerminalService.js'; import { ISignService } from '../../../platform/sign/common/sign.js'; import { SignService } from '../../../platform/sign/node/signService.js'; +import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js'; +import { ExecutableDebugAdapter, NamedPipeDebugAdapter, SocketDebugAdapter } from '../../contrib/debug/node/debugAdapter.js'; +import { hasChildProcesses, prepareCommand } from '../../contrib/debug/node/terminals.js'; +import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; +import { IExtHostCommands } from '../common/extHostCommands.js'; +import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration.js'; import { ExtHostDebugServiceBase, ExtHostDebugSession } from '../common/extHostDebugService.js'; import { IExtHostEditorTabs } from '../common/extHostEditorTabs.js'; import { IExtHostExtensionService } from '../common/extHostExtensionService.js'; import { IExtHostRpcService } from '../common/extHostRpcService.js'; import { IExtHostTerminalService } from '../common/extHostTerminalService.js'; -import { DebugAdapterExecutable, ThemeIcon } from '../common/extHostTypes.js'; +import { IExtHostTesting } from '../common/extHostTesting.js'; +import { DebugAdapterExecutable, DebugAdapterNamedPipeServer, DebugAdapterServer, ThemeIcon } from '../common/extHostTypes.js'; import { IExtHostVariableResolverProvider } from '../common/extHostVariableResolverService.js'; import { IExtHostWorkspace } from '../common/extHostWorkspace.js'; -import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js'; -import { IAdapterDescriptor } from '../../contrib/debug/common/debug.js'; -import { ExecutableDebugAdapter, NamedPipeDebugAdapter, SocketDebugAdapter } from '../../contrib/debug/node/debugAdapter.js'; -import { hasChildProcesses, prepareCommand } from '../../contrib/debug/node/terminals.js'; -import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; -import type * as vscode from 'vscode'; -import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration.js'; -import { IExtHostCommands } from '../common/extHostCommands.js'; -import { IExtHostTesting } from '../common/extHostTesting.js'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -50,16 +49,16 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing); } - protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { - switch (adapter.type) { - case 'server': - return new SocketDebugAdapter(adapter); - case 'pipeServer': - return new NamedPipeDebugAdapter(adapter); - case 'executable': - return new ExecutableDebugAdapter(adapter, session.type); + protected override createDebugAdapter(adapter: vscode.DebugAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { + if (adapter instanceof DebugAdapterExecutable) { + return new ExecutableDebugAdapter(this.convertExecutableToDto(adapter), session.type); + } else if (adapter instanceof DebugAdapterServer) { + return new SocketDebugAdapter(this.convertServerToDto(adapter)); + } else if (adapter instanceof DebugAdapterNamedPipeServer) { + return new NamedPipeDebugAdapter(this.convertPipeServerToDto(adapter)); + } else { + return super.createDebugAdapter(adapter, session); } - return super.createDebugAdapter(adapter, session); } protected override daExecutableFromPackage(session: ExtHostDebugSession, extensionRegistry: ExtensionDescriptionRegistry): DebugAdapterExecutable | undefined { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 798dcc4e797..cc6138857e7 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -911,7 +911,6 @@ export interface IDebugAdapterInlineImpl extends IDisposable { export interface IDebugAdapterImpl { readonly type: 'implementation'; - readonly implementation: IDebugAdapterInlineImpl; } export type IAdapterDescriptor = IDebugAdapterExecutable | IDebugAdapterServer | IDebugAdapterNamedPipeServer | IDebugAdapterImpl; From e8e4e8742d77bf8227d8433f1fe639d97460eab8 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 6 Sep 2024 16:17:21 -0700 Subject: [PATCH 164/286] Combine proposals Also hooks up some experimental context menu actions --- src/vs/platform/actions/common/actions.ts | 2 + .../common/extensionsApiProposals.ts | 3 - src/vs/workbench/api/common/extHostTypes.ts | 6 +- .../chat/browser/chatInlineAnchorWidget.ts | 203 ++++++++++++++++++ .../chatMarkdownDecorationsRenderer.ts | 63 +----- .../files/browser/fileActions.contribution.ts | 24 +++ ...ode.proposed.chatParticipantAdditions.d.ts | 12 ++ .../vscode.proposed.chatSymbolAnchor.d.ts | 19 -- 8 files changed, 246 insertions(+), 86 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts delete mode 100644 src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 62d68f28f16..3fcecbff639 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -222,6 +222,8 @@ export class MenuId { static readonly ChatExecute = new MenuId('ChatExecute'); static readonly ChatExecuteSecondary = new MenuId('ChatExecuteSecondary'); static readonly ChatInputSide = new MenuId('ChatInputSide'); + static readonly ChatInlineResourceAnchorContext = new MenuId('ChatInlineResourceAnchorContext'); + static readonly ChatInlineSymbolAnchorContext = new MenuId('ChatInlineSymbolAnchorContext'); static readonly AccessibleView = new MenuId('AccessibleView'); static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar'); static readonly DiffEditorHunkToolbar = new MenuId('DiffEditorHunkToolbar'); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index ca51f6dbf68..362b4298cdd 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -40,9 +40,6 @@ const _allApiProposals = { chatProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts', }, - chatSymbolAnchor: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts', - }, chatTab: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', }, diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c2e1d7a38e5..1811501aef7 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4422,10 +4422,12 @@ export class ChatResponseFileTreePart { } export class ChatResponseAnchorPart { - value: vscode.Uri | vscode.Location | vscode.SymbolInformation; + value: vscode.Uri | vscode.Location; + value2: vscode.Uri | vscode.Location | vscode.SymbolInformation; title?: string; constructor(value: vscode.Uri | vscode.Location | vscode.SymbolInformation, title?: string) { - this.value = value; + this.value = value as any; + this.value2 = value; this.title = title; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts new file mode 100644 index 00000000000..4dfab620aff --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; +import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; +import { IAction } from '../../../../base/common/actions.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; +import { Location, SymbolKinds } from '../../../../editor/common/languages.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { DefinitionAction } from '../../../../editor/contrib/gotoSymbol/browser/goToCommands.js'; +import * as nls from '../../../../nls.js'; +import { createAndFillInContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { fillEditorsDragData } from '../../../browser/dnd.js'; +import { ContentRefData } from '../common/annotations.js'; + +export class InlineAnchorWidget extends Disposable { + + constructor( + element: HTMLAnchorElement, + data: ContentRefData, + @IHoverService hoverService: IHoverService, + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService, + @ILanguageService languageService: ILanguageService, + @IModelService modelService: IModelService, + @IContextMenuService contextMenuService: IContextMenuService, + @IContextKeyService originalContextKeyService: IContextKeyService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IMenuService menuService: IMenuService, + ) { + super(); + + const contextKeyService = this._register(originalContextKeyService.createScoped(element)); + + element.classList.add('chat-inline-anchor-widget', 'show-file-icons'); + element.replaceChildren(); + + const resourceLabel = this._register(new IconLabel(element, { supportHighlights: false, supportIcons: true })); + + let location: { readonly uri: URI; readonly range?: IRange }; + let contextMenuId: MenuId; + if (data.kind === 'symbol') { + location = data.symbol.location; + contextMenuId = MenuId.ChatInlineSymbolAnchorContext; + + const icon = SymbolKinds.toIcon(data.symbol.kind); + resourceLabel.setLabel(`$(${icon.id}) ${data.symbol.name}`, undefined, {}); + + const model = modelService.getModel(location.uri); + if (model) { + const hasDefinitionProvider = EditorContextKeys.hasDefinitionProvider.bindTo(contextKeyService); + const hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(contextKeyService); + const updateContents = () => { + if (model.isDisposed()) { + return; + } + + hasDefinitionProvider.set(languageFeaturesService.definitionProvider.has(model)); + hasReferenceProvider.set(languageFeaturesService.definitionProvider.has(model)); + }; + updateContents(); + this._register(languageFeaturesService.definitionProvider.onDidChange(updateContents)); + this._register(languageFeaturesService.referenceProvider.onDidChange(updateContents)); + } + } else { + location = data; + contextMenuId = MenuId.ChatInlineResourceAnchorContext; + + const label = labelService.getUriBasenameLabel(location.uri); + const title = location.range && data.kind !== 'symbol' ? + `${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` : + label; + + resourceLabel.setLabel(title, undefined, { + extraClasses: getIconClasses(modelService, languageService, location.uri) + }); + } + + const fragment = location.range ? `${location.range.startLineNumber}-${location.range.endLineNumber}` : ''; + element.setAttribute('data-href', location.uri.with({ fragment }).toString()); + + // Context menu + this._register(dom.addDisposableListener(element, dom.EventType.CONTEXT_MENU, domEvent => { + const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent); + dom.EventHelper.stop(domEvent, true); + + contextMenuService.showContextMenu({ + contextKeyService, + getAnchor: () => event, + getActions: () => { + const menu = menuService.getMenuActions(contextMenuId, contextKeyService, { arg: location }); + const primary: IAction[] = []; + createAndFillInContextMenuActions(menu, primary); + return primary; + }, + }); + })); + + // Hover + const relativeLabel = labelService.getUriLabel(location.uri, { relative: true }); + this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, relativeLabel)); + + // Drag and drop + element.draggable = true; + this._register(dom.addDisposableListener(element, 'dragstart', e => { + instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [location.uri], e)); + + e.dataTransfer?.setDragImage(element, 0, 0); + })); + } +} + +registerAction2(class GoToDefinitionAction extends Action2 { + + static readonly id = 'chat.inlineSymbolAnchor.goToDefinition'; + + constructor() { + super({ + id: GoToDefinitionAction.id, + title: { + ...nls.localize2('actions.goToDecl.label', "Go to Definition"), + mnemonicTitle: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition"), + }, + precondition: EditorContextKeys.hasDefinitionProvider, + menu: [{ + id: MenuId.ChatInlineSymbolAnchorContext, + group: 'navigation', + order: 1.1, + },] + }); + } + + override async run(accessor: ServicesAccessor, location: Location): Promise { + const editorService = accessor.get(ICodeEditorService); + + await editorService.openCodeEditor({ + resource: location.uri, options: { + selection: { + startColumn: location.range.startColumn, + startLineNumber: location.range.startLineNumber, + } + } + }, null); + + const action = new DefinitionAction({ openToSide: false, openInPeek: false, muteMessage: true }, { title: { value: '', original: '' }, id: '', precondition: undefined }); + return action.run(accessor); + } +}); + +registerAction2(class GoToReferencesAction extends Action2 { + + static readonly id = 'chat.inlineSymbolAnchor.goToReferences'; + + constructor() { + super({ + id: GoToReferencesAction.id, + title: { + ...nls.localize2('goToReferences.label', "Go to References"), + mnemonicTitle: nls.localize({ key: 'miGotoReference', comment: ['&& denotes a mnemonic'] }, "Go to &&References"), + }, + precondition: EditorContextKeys.hasReferenceProvider, + menu: [{ + id: MenuId.ChatInlineSymbolAnchorContext, + group: 'navigation', + order: 1.1, + },] + }); + } + + override async run(accessor: ServicesAccessor, location: Location): Promise { + const editorService = accessor.get(ICodeEditorService); + const commandService = accessor.get(ICommandService); + + await editorService.openCodeEditor({ + resource: location.uri, options: { + selection: { + startColumn: location.range.startColumn, + startLineNumber: location.range.startLineNumber, + } + } + }, null); + + await commandService.executeCommand('editor.action.goToReferences'); + } +}); diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index 95a0959888a..79555a8099f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -6,17 +6,11 @@ import * as dom from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; import { URI } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { SymbolKinds } from '../../../../editor/common/languages.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; @@ -24,7 +18,6 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { ILabelService } from '../../../../platform/label/common/label.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; -import { fillEditorsDragData } from '../../../browser/dnd.js'; import { ContentRefData, contentRefUrl } from '../common/annotations.js'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../common/chatColors.js'; @@ -34,6 +27,7 @@ import { IChatVariablesService } from '../common/chatVariables.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { IChatWidgetService } from './chat.js'; import { ChatAgentHover, getChatAgentHoverOptions } from './chatAgentHover.js'; +import { InlineAnchorWidget } from './chatInlineAnchorWidget.js'; import './media/chatInlineAnchorWidget.css'; /** For rendering slash commands, variables */ @@ -280,58 +274,3 @@ export class ChatMarkdownDecorationsRenderer extends Disposable { } } } - - -class InlineAnchorWidget extends Disposable { - - constructor( - element: HTMLAnchorElement, - data: ContentRefData, - @IHoverService hoverService: IHoverService, - @IInstantiationService instantiationService: IInstantiationService, - @ILabelService labelService: ILabelService, - @ILanguageService languageService: ILanguageService, - @IModelService modelService: IModelService, - ) { - super(); - - element.classList.add('chat-inline-anchor-widget', 'show-file-icons'); - element.replaceChildren(); - - const resourceLabel = this._register(new IconLabel(element, { supportHighlights: false, supportIcons: true })); - - let location: { readonly uri: URI; readonly range?: IRange }; - if (data.kind === 'symbol') { - location = data.symbol.location; - - const icon = SymbolKinds.toIcon(data.symbol.kind); - resourceLabel.setLabel(`$(${icon.id}) ${data.symbol.name}`, undefined, {}); - } else { - location = data; - - const label = labelService.getUriBasenameLabel(location.uri); - const title = location.range && data.kind !== 'symbol' ? - `${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` : - label; - - resourceLabel.setLabel(title, undefined, { - extraClasses: getIconClasses(modelService, languageService, location.uri) - }); - } - - const fragment = location.range ? `${location.range.startLineNumber}-${location.range.endLineNumber}` : ''; - element.setAttribute('data-href', location.uri.with({ fragment }).toString()); - - // Hover - const relativeLabel = labelService.getUriLabel(location.uri, { relative: true }); - this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, relativeLabel)); - - // Drag and drop - element.draggable = true; - this._register(dom.addDisposableListener(element, 'dragstart', e => { - instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [location.uri], e)); - - e.dataTransfer?.setDragImage(element, 0, 0); - })); - } -} diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index c1b61149ad8..82c188c79a0 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -757,3 +757,27 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { }, order: 1 }); + + +// Chat resource anchor context menu + +MenuRegistry.appendMenuItem(MenuId.ChatInlineResourceAnchorContext, { + group: 'navigation', + order: 10, + command: openToSideCommand, + when: ResourceContextKey.HasResource +}); + +MenuRegistry.appendMenuItem(MenuId.ChatInlineResourceAnchorContext, { + group: '1_cutcopypaste', + order: 10, + command: copyPathCommand, + when: ResourceContextKey.IsFileSystemResource +}); + +MenuRegistry.appendMenuItem(MenuId.ChatInlineResourceAnchorContext, { + group: '1_cutcopypaste', + order: 20, + command: copyRelativePathCommand, + when: ResourceContextKey.IsFileSystemResource +}); diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index a3ff6db82d1..9352503656d 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -129,6 +129,18 @@ declare module 'vscode' { constructor(uri: Uri, range: Range); } + // Extended to add `SymbolInformation`. Would also be added to `constructor`. + export interface ChatResponseAnchorPart { + /** + * The target of this anchor. + * + * If this is a {@linkcode Uri} or {@linkcode Location}, this is rendered as a normal link. + * + * If this is a {@linkcode SymbolInformation}, this is rendered as a symbol link. + */ + value2: Uri | Location | SymbolInformation; + } + export interface ChatResponseStream { /** diff --git a/src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts b/src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts deleted file mode 100644 index 487a41e2956..00000000000 --- a/src/vscode-dts/vscode.proposed.chatSymbolAnchor.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // Extended to add `SymbolInformation`. Would also be added to constructor - export interface ChatResponseAnchorPart { - /** - * The target of this anchor. - * - * If this is a {@linkcode Uri} or {@linkcode Location}, this is rendered as a normal link. - * - * If this is a {@linkcode SymbolInformation}, this is rendered as a symbol link. - */ - value: Uri | Location | SymbolInformation; - } -} From 976e68e6207c1c94540f2d4000a308b1e38e6719 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 6 Sep 2024 17:19:05 -0700 Subject: [PATCH 165/286] eng: identify ternary expressions in selfhost runner (#227848) Fixes #227222 --- .../src/sourceUtils.ts | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts index 56b26cafda8..3ed21cf5810 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { TestCase, TestConstruct, TestSuite, VSCodeTest } from './testTree'; const suiteNames = new Set(['suite', 'flakySuite']); +const testNames = new Set(['test']); export const enum Action { Skip, @@ -19,22 +20,19 @@ export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: V return Action.Recurse; } - let lhs = node.expression; - if (isSkipCall(lhs)) { + const asSuite = identifyCall(node.expression, suiteNames); + const asTest = identifyCall(node.expression, testNames); + const either = asSuite || asTest; + if (either === IdentifiedCall.Skipped) { return Action.Skip; } - - if (isPropertyCall(lhs) && lhs.name.text === 'only') { - lhs = lhs.expression; + if (either === IdentifiedCall.Nothing) { + return Action.Recurse; } const name = node.arguments[0]; const func = node.arguments[1]; - if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) { - return Action.Recurse; - } - - if (!func) { + if (!name || !ts.isStringLiteralLike(name) || !func) { return Action.Recurse; } @@ -46,23 +44,45 @@ export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: V ); const cparent = parent instanceof TestConstruct ? parent : undefined; - if (lhs.escapedText === 'test') { + + // we know this is either a suite or a test because we checked for skipped/nothing above + + if (asTest) { return new TestCase(name.text, range, cparent); } - if (suiteNames.has(lhs.escapedText.toString())) { + if (asSuite) { return new TestSuite(name.text, range, cparent); } - return Action.Recurse; + throw new Error('unreachable'); +}; + +const enum IdentifiedCall { + Nothing, + Skipped, + IsThing, +} + +const identifyCall = (lhs: ts.Node, needles: ReadonlySet): IdentifiedCall => { + if (ts.isIdentifier(lhs)) { + return needles.has(lhs.escapedText || lhs.text) ? IdentifiedCall.IsThing : IdentifiedCall.Nothing; + } + + if (isPropertyCall(lhs) && lhs.name.text === 'skip') { + return needles.has(lhs.expression.text) ? IdentifiedCall.Skipped : IdentifiedCall.Nothing; + } + + if (ts.isParenthesizedExpression(lhs) && ts.isConditionalExpression(lhs.expression)) { + return Math.max(identifyCall(lhs.expression.whenTrue, needles), identifyCall(lhs.expression.whenFalse, needles)); + } + + return IdentifiedCall.Nothing; }; const isPropertyCall = ( - lhs: ts.LeftHandSideExpression + lhs: ts.Node ): lhs is ts.PropertyAccessExpression & { expression: ts.Identifier; name: ts.Identifier } => ts.isPropertyAccessExpression(lhs) && ts.isIdentifier(lhs.expression) && ts.isIdentifier(lhs.name); - -const isSkipCall = (lhs: ts.LeftHandSideExpression) => - isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip'; From 2a4d7851efdcfcd0a2217625a1e961abd0b253e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 11:43:31 +0900 Subject: [PATCH 166/286] Bump pathval from 1.1.0 to 1.1.1 in /test/monaco (#227783) Bumps [pathval](https://github.com/chaijs/pathval) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/chaijs/pathval/releases) - [Changelog](https://github.com/chaijs/pathval/blob/master/CHANGELOG.md) - [Commits](https://github.com/chaijs/pathval/compare/v1.1.0...v1.1.1) --- updated-dependencies: - dependency-name: pathval dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/monaco/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index 7846640eea8..224680dd597 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -77,9 +77,9 @@ } }, "node_modules/pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA= sha512-qZ181q3ICkag/+lv1X6frDUF84pqCm30qild3LGbD84n0AC75CYwnWsQRDlpz7zDkU5NVcmhHh4LjXK0goLYZA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, "engines": { "node": "*" From bc0764dcb1069dd292bb15429595392de1b71889 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 11:48:31 +0900 Subject: [PATCH 167/286] Bump ansi-regex (#227785) Bumps and [ansi-regex](https://github.com/chalk/ansi-regex). These dependencies needed to be updated together. Updates `ansi-regex` from 3.0.0 to 5.0.1 - [Release notes](https://github.com/chalk/ansi-regex/releases) - [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v5.0.1) Updates `ansi-regex` from 4.1.0 to 5.0.1 - [Release notes](https://github.com/chalk/ansi-regex/releases) - [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v5.0.1) --- updated-dependencies: - dependency-name: ansi-regex dependency-type: indirect - dependency-name: ansi-regex dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1363d5928a..88d1a66925e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8836,7 +8836,7 @@ "node_modules/gulp-cli/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8= sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9066,9 +9066,9 @@ } }, "node_modules/gulp-eslint/node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true, "engines": { "node": ">=4" @@ -9838,7 +9838,7 @@ "node_modules/gulp-plumber/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8= sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10389,7 +10389,7 @@ "node_modules/has-ansi/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8= sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10685,7 +10685,7 @@ "node_modules/husky/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8= sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10912,9 +10912,9 @@ } }, "node_modules/inquirer/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true, "engines": { "node": ">=6" @@ -11045,9 +11045,9 @@ } }, "node_modules/inquirer/node_modules/string-width/node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true, "engines": { "node": ">=4" @@ -17971,9 +17971,9 @@ } }, "node_modules/table/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true, "engines": { "node": ">=6" @@ -19282,9 +19282,9 @@ } }, "node_modules/vscode-nls-dev/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true, "engines": { "node": ">=6" From baa49e9f9ceb8f14ff8bfd1f757db7e01833130f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 02:49:29 +0000 Subject: [PATCH 168/286] Bump xml2js and @azure/core-http in /build Bumps [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) and [@azure/core-http](https://github.com/Azure/azure-sdk-for-js). These dependencies needed to be updated together. Updates `xml2js` from 0.4.23 to 0.5.0 - [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/commits/0.5.0) Updates `@azure/core-http` from 3.0.0 to 3.0.4 - [Release notes](https://github.com/Azure/azure-sdk-for-js/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/Changelog-for-next-generation.md) - [Commits](https://github.com/Azure/azure-sdk-for-js/compare/@azure/core-http_3.0.0...@azure/core-http_3.0.4) --- updated-dependencies: - dependency-name: xml2js dependency-type: indirect - dependency-name: "@azure/core-http" dependency-type: indirect ... Signed-off-by: dependabot[bot] --- build/package-lock.json | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/build/package-lock.json b/build/package-lock.json index c13ecdcabc5..9c423af3291 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -133,9 +133,10 @@ } }, "node_modules/@azure/core-http": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.0.tgz", - "integrity": "sha512-BxI2SlGFPPz6J1XyZNIVUf0QZLBKFX+ViFjKOkzqD18J1zOINIQ8JSBKKr+i+v8+MB6LacL6Nn/sP/TE13+s2Q==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz", + "integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==", + "deprecated": "deprecating as we migrated to core v2", "dev": true, "dependencies": { "@azure/abort-controller": "^1.0.0", @@ -151,7 +152,7 @@ "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" }, "engines": { "node": ">=14.0.0" @@ -1372,28 +1373,6 @@ "node": ">=10" } }, - "node_modules/@vscode/vsce/node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@vscode/vsce/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -4594,9 +4573,9 @@ "devOptional": true }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { "sax": ">=0.6.0", From 533d8ec6a5a221cb4e64236c773e57601f080ed0 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Sat, 7 Sep 2024 06:03:25 +0200 Subject: [PATCH 169/286] Rework eventing for PCAs and fix a few bugs along the way (#227854) A big change, but a good one... This addresses some core issues around how we manage multiple PublicClientApplications (which are an object that should be created for each set of clientId,authority). Previously, we were doing some pretty nasty things to detect when a new PCA was created/deleted and as a result it would cause infinite loops and the likes... Now we've focused on managing that in SecretStorage by looking for a `publicClientApplications` key. This is all encapsulated in the new `PublicClientApplicationsSecretStorage`. Since we no longer relied on that hack, we still needed some way to have a PCA inform that: * accounts have changed * the last account was removed (signaling that this PCA could be disposed of in `PublicClientApplicationsSecretStorage`) Both of these events have been added to `CachedPublicClientApplication` (now in its own file) and are being used. (replacing the old `_accountChangeHandler` which was hacky... true events are cleaner). Last thing in the eventing space is that I try to minimize calls to `_storePublicClientApplications` so to not spam events across SecretStorage. You can see this in my usage of `_doCreatePublicClientApplication` over `getOrCreate`. Couple random other things: * `changed` accounts are properly bubbled up in `_onDidChangeSessionsEmitter` which is needed when a token is refreshed * `getSessions` when no scopes are passed in no longer causes new tokens to be minted * we use to only remove the first account we found but in some cases there may be the same account across different PCAs, so there's a `return` that's removed in `authProvider.ts` that fixes this bug * Logging is clearer and more verbose (in a good way) --- .../src/common/publicClientCache.ts | 3 + .../src/node/authProvider.ts | 106 +++--- .../src/node/cachedPublicClientApplication.ts | 148 +++++++++ .../src/node/publicClientCache.ts | 314 ++++++++---------- 4 files changed, 355 insertions(+), 216 deletions(-) create mode 100644 extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts diff --git a/extensions/microsoft-authentication/src/common/publicClientCache.ts b/extensions/microsoft-authentication/src/common/publicClientCache.ts index cb9339f926d..925a4d1a88c 100644 --- a/extensions/microsoft-authentication/src/common/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/common/publicClientCache.ts @@ -7,6 +7,8 @@ import type { Disposable, Event } from 'vscode'; export interface ICachedPublicClientApplication extends Disposable { initialize(): Promise; + onDidAccountsChange: Event<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; + onDidRemoveLastAccount: Event; acquireTokenSilent(request: SilentFlowRequest): Promise; acquireTokenInteractive(request: InteractiveRequest): Promise; removeAccount(account: AccountInfo): Promise; @@ -16,6 +18,7 @@ export interface ICachedPublicClientApplication extends Disposable { } export interface ICachedPublicClientApplicationManager { + onDidAccountsChange: Event<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; getOrCreate(clientId: string, authority: string): Promise; getAll(): ICachedPublicClientApplication[]; } diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index 3925f6f58cf..02bb66863be 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -48,15 +48,12 @@ export class MsalAuthProvider implements AuthenticationProvider { private readonly _env: Environment = Environment.AzureCloud ) { this._disposables = context.subscriptions; - this._publicClientManager = new CachedPublicClientApplicationManager( - context.globalState, - context.secrets, - this._logger, - (e) => this._handleAccountChange(e) + this._publicClientManager = new CachedPublicClientApplicationManager(context.globalState, context.secrets, this._logger); + this._disposables.push( + this._onDidChangeSessionsEmitter, + this._publicClientManager, + this._publicClientManager.onDidAccountsChange(e => this._handleAccountChange(e)) ); - this._disposables.push(this._publicClientManager); - this._disposables.push(this._onDidChangeSessionsEmitter); - } async initialize(): Promise { @@ -79,40 +76,43 @@ export class MsalAuthProvider implements AuthenticationProvider { * See {@link onDidChangeSessions} for more information on how this is used. * @param param0 Event that contains the added and removed accounts */ - private _handleAccountChange({ added, deleted }: { added: AccountInfo[]; deleted: AccountInfo[] }) { - const process = (a: AccountInfo) => ({ - // This shouldn't be needed - accessToken: '1234', - id: a.homeAccountId, - scopes: [], - account: { - id: a.homeAccountId, - label: a.username - }, - idToken: a.idToken, + private _handleAccountChange({ added, changed, deleted }: { added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }) { + this._onDidChangeSessionsEmitter.fire({ + added: added.map(this.sessionFromAccountInfo), + changed: changed.map(this.sessionFromAccountInfo), + removed: deleted.map(this.sessionFromAccountInfo) }); - this._onDidChangeSessionsEmitter.fire({ added: added.map(process), changed: [], removed: deleted.map(process) }); } //#region AuthenticationProvider methods async getSessions(scopes: string[] | undefined, options?: AuthenticationGetSessionOptions): Promise { + const askingForAll = scopes === undefined; const scopeData = new ScopeData(scopes); - this._logger.info('[getSessions]', scopes ? scopeData.scopeStr : 'all', 'starting'); - if (!scopes) { - // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. + // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. + this._logger.info('[getSessions]', askingForAll ? '[all]' : `[${scopeData.scopeStr}]`, 'starting'); - const allSessions: AuthenticationSession[] = []; + // This branch only gets called by Core for sign out purposes and initial population of the account menu. Since we are + // living in a world where a "session" from Core's perspective is an account, we return 1 session per account. + // See the large comment on `onDidChangeSessions` for more information. + if (askingForAll) { + const allSessionsForAccounts = new Map(); for (const cachedPca of this._publicClientManager.getAll()) { - const sessions = await this.getAllSessionsForPca(cachedPca, scopeData.originalScopes, scopeData.scopesToSend, options?.account); - allSessions.push(...sessions); + for (const account of cachedPca.accounts) { + if (allSessionsForAccounts.has(account.homeAccountId)) { + continue; + } + allSessionsForAccounts.set(account.homeAccountId, this.sessionFromAccountInfo(account)); + } } + const allSessions = Array.from(allSessionsForAccounts.values()); + this._logger.info('[getSessions] [all]', `returned ${allSessions.length} session(s)`); return allSessions; } const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); const sessions = await this.getAllSessionsForPca(cachedPca, scopeData.originalScopes, scopeData.scopesToSend, options?.account); - this._logger.info(`[getSessions] returned ${sessions.length} sessions`); + this._logger.info(`[getSessions] [${scopeData.scopeStr}] returned ${sessions.length} session(s)`); return sessions; } @@ -121,7 +121,7 @@ export class MsalAuthProvider implements AuthenticationProvider { const scopeData = new ScopeData(scopes); // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. - this._logger.info('[createSession]', scopeData.scopeStr, 'starting'); + this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting'); const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); let result: AuthenticationResult; try { @@ -169,32 +169,43 @@ export class MsalAuthProvider implements AuthenticationProvider { } } - const session = this.toAuthenticationSession(result, scopeData.originalScopes); + const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes); this._telemetryReporter.sendLoginEvent(session.scopes); - this._logger.info('[createSession]', scopeData.scopeStr, 'returned session'); + this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'returned session'); + // This is the only scenario in which we need to fire the _onDidChangeSessionsEmitter out of band... + // the badge flow (when the client passes no options in to getSession) will only remove a badge if a session + // was created that _matches the scopes_ that that badge requests. See `onDidChangeSessions` for more info. + // TODO: This should really be fixed in Core. this._onDidChangeSessionsEmitter.fire({ added: [session], changed: [], removed: [] }); return session; } async removeSession(sessionId: string): Promise { this._logger.info('[removeSession]', sessionId, 'starting'); + const promises = new Array>(); for (const cachedPca of this._publicClientManager.getAll()) { const accounts = cachedPca.accounts; for (const account of accounts) { if (account.homeAccountId === sessionId) { this._telemetryReporter.sendLogoutEvent(); - try { - await cachedPca.removeAccount(account); - } catch (e) { - this._telemetryReporter.sendLogoutFailedEvent(); - throw e; - } - this._logger.info('[removeSession]', sessionId, 'removed session'); - return; + promises.push(cachedPca.removeAccount(account)); + this._logger.info(`[removeSession] [${sessionId}] [${cachedPca.clientId}] [${cachedPca.authority}] removing session...`); } } } - this._logger.info('[removeSession]', sessionId, 'session not found'); + if (!promises.length) { + this._logger.info('[removeSession]', sessionId, 'session not found'); + return; + } + const results = await Promise.allSettled(promises); + for (const result of results) { + if (result.status === 'rejected') { + this._telemetryReporter.sendLogoutFailedEvent(); + this._logger.error('[removeSession]', sessionId, 'error removing session', result.reason); + } + } + + this._logger.info('[removeSession]', sessionId, `attempted to remove ${promises.length} sessions`); } //#endregion @@ -217,7 +228,7 @@ export class MsalAuthProvider implements AuthenticationProvider { for (const account of accounts) { try { const result = await cachedPca.acquireTokenSilent({ account, scopes: scopesToSend, redirectUri }); - sessions.push(this.toAuthenticationSession(result, originalScopes)); + sessions.push(this.sessionFromAuthenticationResult(result, originalScopes)); } catch (e) { // If we can't get a token silently, the account is probably in a bad state so we should skip it // MSAL will log this already, so we don't need to log it again @@ -227,7 +238,7 @@ export class MsalAuthProvider implements AuthenticationProvider { return sessions; } - private toAuthenticationSession(result: AuthenticationResult, scopes: readonly string[]): AuthenticationSession & { idToken: string } { + private sessionFromAuthenticationResult(result: AuthenticationResult, scopes: readonly string[]): AuthenticationSession & { idToken: string } { return { accessToken: result.accessToken, idToken: result.idToken, @@ -239,4 +250,17 @@ export class MsalAuthProvider implements AuthenticationProvider { scopes }; } + + private sessionFromAccountInfo(account: AccountInfo): AuthenticationSession { + return { + accessToken: '1234', + id: account.homeAccountId, + scopes: [], + account: { + id: account.homeAccountId, + label: account.username + }, + idToken: account.idToken, + }; + } } diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts new file mode 100644 index 00000000000..62882d68ca0 --- /dev/null +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PublicClientApplication, AccountInfo, Configuration, SilentFlowRequest, AuthenticationResult, InteractiveRequest } from '@azure/msal-node'; +import { Disposable, Memento, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter } from 'vscode'; +import { raceCancellationAndTimeoutError } from '../common/async'; +import { SecretStorageCachePlugin } from '../common/cachePlugin'; +import { MsalLoggerOptions } from '../common/loggerOptions'; +import { ICachedPublicClientApplication } from '../common/publicClientCache'; + +export class CachedPublicClientApplication implements ICachedPublicClientApplication { + private _pca: PublicClientApplication; + + private _accounts: AccountInfo[] = []; + private readonly _disposable: Disposable; + + private readonly _loggerOptions = new MsalLoggerOptions(this._logger); + private readonly _secretStorageCachePlugin = new SecretStorageCachePlugin( + this._secretStorage, + // Include the prefix as a differentiator to other secrets + `pca:${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` + ); + private readonly _config: Configuration = { + auth: { clientId: this._clientId, authority: this._authority }, + system: { + loggerOptions: { + correlationId: `${this._clientId}] [${this._authority}`, + loggerCallback: (level, message, containsPii) => this._loggerOptions.loggerCallback(level, message, containsPii), + } + }, + cache: { + cachePlugin: this._secretStorageCachePlugin + } + }; + + /** + * We keep track of the last time an account was removed so we can recreate the PCA if we detect that an account was removed. + * This is due to MSAL-node not providing a way to detect when an account is removed from the cache. An internal issue has been + * filed to track this. If MSAL-node ever provides a way to detect this or handle this better in the Persistant Cache Plugin, + * we can remove this logic. + */ + private _lastCreated: Date; + + //#region Events + + private readonly _onDidAccountsChangeEmitter = new EventEmitter<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; + readonly onDidAccountsChange = this._onDidAccountsChangeEmitter.event; + + private readonly _onDidRemoveLastAccountEmitter = new EventEmitter(); + readonly onDidRemoveLastAccount = this._onDidRemoveLastAccountEmitter.event; + + //#endregion + + constructor( + private readonly _clientId: string, + private readonly _authority: string, + private readonly _globalMemento: Memento, + private readonly _secretStorage: SecretStorage, + private readonly _logger: LogOutputChannel + ) { + this._pca = new PublicClientApplication(this._config); + this._lastCreated = new Date(); + this._disposable = Disposable.from( + this._registerOnSecretStorageChanged(), + this._onDidAccountsChangeEmitter, + this._onDidRemoveLastAccountEmitter + ); + } + + get accounts(): AccountInfo[] { return this._accounts; } + get clientId(): string { return this._clientId; } + get authority(): string { return this._authority; } + + initialize(): Promise { + return this._update(); + } + + dispose(): void { + this._disposable.dispose(); + } + + async acquireTokenSilent(request: SilentFlowRequest): Promise { + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}]`); + const result = await this._pca.acquireTokenSilent(request); + if (result.account && !result.fromCache) { + this._onDidAccountsChangeEmitter.fire({ added: [], changed: [result.account], deleted: [] }); + } + return result; + } + + async acquireTokenInteractive(request: InteractiveRequest): Promise { + this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${this._authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); + return await window.withProgress( + { + location: ProgressLocation.Notification, + cancellable: true, + title: l10n.t('Signing in to Microsoft...') + }, + (_process, token) => raceCancellationAndTimeoutError( + this._pca.acquireTokenInteractive(request), + token, + 1000 * 60 * 5 + ) + ); + } + + removeAccount(account: AccountInfo): Promise { + this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, new Date()); + return this._pca.getTokenCache().removeAccount(account); + } + + private _registerOnSecretStorageChanged() { + return this._secretStorageCachePlugin.onDidChange(() => this._update()); + } + + private async _update() { + const before = this._accounts; + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update before: ${before.length}`); + // Dates are stored as strings in the memento + const lastRemovalDate = this._globalMemento.get(`lastRemoval:${this._clientId}:${this._authority}`); + if (lastRemovalDate && this._lastCreated && Date.parse(lastRemovalDate) > this._lastCreated.getTime()) { + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication removal detected... recreating PCA...`); + this._pca = new PublicClientApplication(this._config); + this._lastCreated = new Date(); + } + + const after = await this._pca.getAllAccounts(); + this._accounts = after; + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update after: ${after.length}`); + + const beforeSet = new Set(before.map(b => b.homeAccountId)); + const afterSet = new Set(after.map(a => a.homeAccountId)); + + const added = after.filter(a => !beforeSet.has(a.homeAccountId)); + const deleted = before.filter(b => !afterSet.has(b.homeAccountId)); + if (added.length > 0 || deleted.length > 0) { + this._onDidAccountsChangeEmitter.fire({ added, changed: [], deleted }); + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication accounts changed. added: ${added.length}, deleted: ${deleted.length}`); + if (!after.length) { + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication final account deleted. Firing event.`); + this._onDidRemoveLastAccountEmitter.fire(); + } + } + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update complete`); + } +} diff --git a/extensions/microsoft-authentication/src/node/publicClientCache.ts b/extensions/microsoft-authentication/src/node/publicClientCache.ts index 34bf2c3c73b..6ecc34501f7 100644 --- a/extensions/microsoft-authentication/src/node/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/node/publicClientCache.ts @@ -3,77 +3,84 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AccountInfo, AuthenticationResult, Configuration, InteractiveRequest, PublicClientApplication, SilentFlowRequest } from '@azure/msal-node'; -import { SecretStorageCachePlugin } from '../common/cachePlugin'; -import { SecretStorage, LogOutputChannel, Disposable, SecretStorageChangeEvent, EventEmitter, Memento, window, ProgressLocation, l10n } from 'vscode'; -import { MsalLoggerOptions } from '../common/loggerOptions'; +import { AccountInfo } from '@azure/msal-node'; +import { SecretStorage, LogOutputChannel, Disposable, EventEmitter, Memento, Event } from 'vscode'; import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache'; -import { raceCancellationAndTimeoutError } from '../common/async'; +import { CachedPublicClientApplication } from './cachedPublicClientApplication'; export interface IPublicClientApplicationInfo { clientId: string; authority: string; } -const _keyPrefix = 'pca:'; - export class CachedPublicClientApplicationManager implements ICachedPublicClientApplicationManager { - // The key is the clientId and authority stringified + // The key is the clientId and authority JSON stringified private readonly _pcas = new Map(); + private readonly _pcaDisposables = new Map(); - private _initialized = false; private _disposable: Disposable; + private _pcasSecretStorage: PublicClientApplicationsSecretStorage; + + private readonly _onDidAccountsChangeEmitter = new EventEmitter<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>(); + readonly onDidAccountsChange = this._onDidAccountsChangeEmitter.event; constructor( private readonly _globalMemento: Memento, private readonly _secretStorage: SecretStorage, - private readonly _logger: LogOutputChannel, - private readonly _accountChangeHandler: (e: { added: AccountInfo[]; deleted: AccountInfo[] }) => void + private readonly _logger: LogOutputChannel ) { - this._disposable = _secretStorage.onDidChange(e => this._handleSecretStorageChange(e)); + this._pcasSecretStorage = new PublicClientApplicationsSecretStorage(_secretStorage); + this._disposable = Disposable.from( + this._pcasSecretStorage, + this._registerSecretStorageHandler(), + this._onDidAccountsChangeEmitter + ); + } + + private _registerSecretStorageHandler() { + return this._pcasSecretStorage.onDidChange(() => this._handleSecretStorageChange()); } async initialize() { this._logger.debug('[initialize] Initializing PublicClientApplicationManager'); - const keys = await this._secretStorage.get('publicClientApplications'); + let keys: string[] | undefined; + try { + keys = await this._pcasSecretStorage.get(); + } catch (e) { + // data is corrupted + this._logger.error('[initialize] Error initializing PublicClientApplicationManager:', e); + await this._pcasSecretStorage.delete(); + } if (!keys) { - this._initialized = true; return; } const promises = new Array>(); - try { - for (const key of JSON.parse(keys) as string[]) { - try { - const { clientId, authority } = JSON.parse(key) as IPublicClientApplicationInfo; - // Load the PCA in memory - promises.push(this.getOrCreate(clientId, authority)); - } catch (e) { - // ignore - } + for (const key of keys) { + try { + const { clientId, authority } = JSON.parse(key) as IPublicClientApplicationInfo; + // Load the PCA in memory + promises.push(this._doCreatePublicClientApplication(clientId, authority, key)); + } catch (e) { + this._logger.error('[initialize] Error intitializing PCA:', key); } - } catch (e) { - // data is corrupted - this._logger.error('[initialize] Error initializing PublicClientApplicationManager:', e); - await this._secretStorage.delete('publicClientApplications'); } - // TODO: should we do anything for when this fails? - await Promise.allSettled(promises); + const results = await Promise.allSettled(promises); + for (const result of results) { + if (result.status === 'rejected') { + this._logger.error('[initialize] Error getting PCA:', result.reason); + } + } this._logger.debug('[initialize] PublicClientApplicationManager initialized'); - this._initialized = true; } dispose() { this._disposable.dispose(); - Disposable.from(...this._pcas.values()).dispose(); + Disposable.from(...this._pcaDisposables.values()).dispose(); } async getOrCreate(clientId: string, authority: string): Promise { - if (!this._initialized) { - throw new Error('PublicClientApplicationManager not initialized'); - } - // Use the clientId and authority as the key const pcasKey = JSON.stringify({ clientId, authority }); let pca = this._pcas.get(pcasKey); @@ -83,170 +90,127 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PublicClientApplicationManager cache miss, creating new PCA...`); - pca = new CachedPublicClientApplication(clientId, authority, this._globalMemento, this._secretStorage, this._accountChangeHandler, this._logger); - this._pcas.set(pcasKey, pca); - await pca.initialize(); + pca = await this._doCreatePublicClientApplication(clientId, authority, pcasKey); await this._storePublicClientApplications(); - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PublicClientApplicationManager PCA created`); + this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PCA created.`); + return pca; + } + + private async _doCreatePublicClientApplication(clientId: string, authority: string, pcasKey: string) { + const pca = new CachedPublicClientApplication(clientId, authority, this._globalMemento, this._secretStorage, this._logger); + this._pcas.set(pcasKey, pca); + const disposable = Disposable.from( + pca, + pca.onDidAccountsChange(e => this._onDidAccountsChangeEmitter.fire(e)), + pca.onDidRemoveLastAccount(() => { + // The PCA has no more accounts, so we can dispose it so we're not keeping it + // around forever. + disposable.dispose(); + this._pcas.delete(pcasKey); + this._logger.debug(`[_doCreatePublicClientApplication] [${clientId}] [${authority}] PCA disposed. Firing off storing of PCAs...`); + void this._storePublicClientApplications(); + }) + ); + this._pcaDisposables.set(pcasKey, disposable); + // Intialize the PCA after the `onDidAccountsChange` is set so we get initial state. + await pca.initialize(); return pca; } getAll(): ICachedPublicClientApplication[] { - if (!this._initialized) { - throw new Error('PublicClientApplicationManager not initialized'); - } return Array.from(this._pcas.values()); } - private async _handleSecretStorageChange(e: SecretStorageChangeEvent) { - if (!e.key.startsWith(_keyPrefix)) { - return; - } - - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager secret storage change: ${e.key}`); - const result = await this._secretStorage.get(e.key); - const pcasKey = e.key.split(_keyPrefix)[1]; - - // If the cache was deleted, or the PCA has zero accounts left, remove the PCA - if (!result || this._pcas.get(pcasKey)?.accounts.length === 0) { - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager removing PCA: ${pcasKey}`); - this._pcas.delete(pcasKey); + private async _handleSecretStorageChange() { + this._logger.debug(`[_handleSecretStorageChange] Handling PCAs secret storage change...`); + let result: string[] | undefined; + try { + result = await this._pcasSecretStorage.get(); + } catch (_e) { + // The data in secret storage has been corrupted somehow so + // we store what we have in this window await this._storePublicClientApplications(); - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager PCA removed: ${pcasKey}`); + return; + } + if (!result) { + this._logger.debug(`[_handleSecretStorageChange] PCAs deleted in secret storage. Disposing all...`); + Disposable.from(...this._pcaDisposables.values()).dispose(); + this._pcas.clear(); + this._pcaDisposables.clear(); + this._logger.debug(`[_handleSecretStorageChange] Finished PCAs secret storage change.`); return; } - // Load the PCA in memory if it's not already loaded - const { clientId, authority } = JSON.parse(pcasKey) as IPublicClientApplicationInfo; - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager loading PCA: ${pcasKey}`); - await this.getOrCreate(clientId, authority); - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager PCA loaded: ${pcasKey}`); + const pcaKeysFromStorage = new Set(result); + // Handle the deleted ones + for (const pcaKey of this._pcas.keys()) { + if (!pcaKeysFromStorage.delete(pcaKey)) { + // This PCA has been removed in another window + this._pcaDisposables.get(pcaKey)?.dispose(); + this._pcaDisposables.delete(pcaKey); + this._pcas.delete(pcaKey); + this._logger.debug(`[_handleSecretStorageChange] Disposed PCA that was deleted in another window: ${pcaKey}`); + } + } + + // Handle the new ones + for (const newPca of pcaKeysFromStorage) { + try { + const { clientId, authority } = JSON.parse(newPca); + this._logger.debug(`[_handleSecretStorageChange] [${clientId}] [${authority}] Creating new PCA that was created in another window...`); + await this._doCreatePublicClientApplication(clientId, authority, newPca); + this._logger.debug(`[_handleSecretStorageChange] [${clientId}] [${authority}] PCA created.`); + } catch (_e) { + // This really shouldn't happen, but should we do something about this? + this._logger.error(`Failed to parse new PublicClientApplication: ${newPca}`); + continue; + } + } + + this._logger.debug('[_handleSecretStorageChange] Finished handling PCAs secret storage change.'); } - private async _storePublicClientApplications() { - await this._secretStorage.store( - 'publicClientApplications', - JSON.stringify(Array.from(this._pcas.keys())) - ); + private _storePublicClientApplications() { + return this._pcasSecretStorage.store(Array.from(this._pcas.keys())); } } -class CachedPublicClientApplication implements ICachedPublicClientApplication { - private _pca: PublicClientApplication; +class PublicClientApplicationsSecretStorage { + private static key = 'publicClientApplications'; - private _accounts: AccountInfo[] = []; - private readonly _disposable: Disposable; + private _disposable: Disposable; - private readonly _loggerOptions = new MsalLoggerOptions(this._logger); - private readonly _secretStorageCachePlugin = new SecretStorageCachePlugin( - this._secretStorage, - // Include the prefix in the key so we can easily identify it later - `${_keyPrefix}${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` - ); - private readonly _config: Configuration = { - auth: { clientId: this._clientId, authority: this._authority }, - system: { - loggerOptions: { - correlationId: `${this._clientId}] [${this._authority}`, - loggerCallback: (level, message, containsPii) => this._loggerOptions.loggerCallback(level, message, containsPii), - } - }, - cache: { - cachePlugin: this._secretStorageCachePlugin + private readonly _onDidChangeEmitter = new EventEmitter; + readonly onDidChange: Event = this._onDidChangeEmitter.event; + + constructor(private readonly _secretStorage: SecretStorage) { + this._disposable = Disposable.from( + this._onDidChangeEmitter, + this._secretStorage.onDidChange(e => { + if (e.key === PublicClientApplicationsSecretStorage.key) { + this._onDidChangeEmitter.fire(); + } + }) + ); + } + + async get(): Promise { + const value = await this._secretStorage.get(PublicClientApplicationsSecretStorage.key); + if (!value) { + return undefined; } - }; - - /** - * We keep track of the last time an account was removed so we can recreate the PCA if we detect that an account was removed. - * This is due to MSAL-node not providing a way to detect when an account is removed from the cache. An internal issue has been - * filed to track this. If MSAL-node ever provides a way to detect this or handle this better in the Persistant Cache Plugin, - * we can remove this logic. - */ - private _lastCreated: Date; - - constructor( - private readonly _clientId: string, - private readonly _authority: string, - private readonly _globalMemento: Memento, - private readonly _secretStorage: SecretStorage, - private readonly _accountChangeHandler: (e: { added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }) => void, - private readonly _logger: LogOutputChannel - ) { - this._pca = new PublicClientApplication(this._config); - this._lastCreated = new Date(); - this._disposable = this._registerOnSecretStorageChanged(); + return JSON.parse(value); } - get accounts(): AccountInfo[] { return this._accounts; } - get clientId(): string { return this._clientId; } - get authority(): string { return this._authority; } - - initialize(): Promise { - return this._update(); + store(value: string[]): Thenable { + return this._secretStorage.store(PublicClientApplicationsSecretStorage.key, JSON.stringify(value)); } - dispose(): void { + delete(): Thenable { + return this._secretStorage.delete(PublicClientApplicationsSecretStorage.key); + } + + dispose() { this._disposable.dispose(); } - - async acquireTokenSilent(request: SilentFlowRequest): Promise { - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}]`); - const result = await this._pca.acquireTokenSilent(request); - if (result.account && !result.fromCache) { - this._accountChangeHandler({ added: [], changed: [result.account], deleted: [] }); - } - return result; - } - - async acquireTokenInteractive(request: InteractiveRequest): Promise { - this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${this._authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); - return await window.withProgress( - { - location: ProgressLocation.Notification, - cancellable: true, - title: l10n.t('Signing in to Microsoft...') - }, - (_process, token) => raceCancellationAndTimeoutError( - this._pca.acquireTokenInteractive(request), - token, - 1000 * 60 * 5 - ), // 5 minutes - ); - } - - removeAccount(account: AccountInfo): Promise { - this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, new Date()); - return this._pca.getTokenCache().removeAccount(account); - } - - private _registerOnSecretStorageChanged() { - return this._secretStorageCachePlugin.onDidChange(() => this._update()); - } - - private async _update() { - const before = this._accounts; - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update before: ${before.length}`); - // Dates are stored as strings in the memento - const lastRemovalDate = this._globalMemento.get(`lastRemoval:${this._clientId}:${this._authority}`); - if (lastRemovalDate && this._lastCreated && Date.parse(lastRemovalDate) > this._lastCreated.getTime()) { - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication removal detected... recreating PCA...`); - this._pca = new PublicClientApplication(this._config); - this._lastCreated = new Date(); - } - - const after = await this._pca.getAllAccounts(); - this._accounts = after; - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update after: ${after.length}`); - - const beforeSet = new Set(before.map(b => b.homeAccountId)); - const afterSet = new Set(after.map(a => a.homeAccountId)); - - const added = after.filter(a => !beforeSet.has(a.homeAccountId)); - const deleted = before.filter(b => !afterSet.has(b.homeAccountId)); - if (added.length > 0 || deleted.length > 0) { - this._accountChangeHandler({ added, changed: [], deleted }); - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication accounts changed. added: ${added.length}, deleted: ${deleted.length}`); - } - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update complete`); - } } From f1f1413d68e2f7a4caac8d7a28a6b49b999fa8e7 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Sat, 7 Sep 2024 01:01:58 -0700 Subject: [PATCH 170/286] Fix cell status bar target check (#227861) --- .../browser/view/cellParts/cellStatusPart.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index ed204686f09..f58ff0aa23f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -105,7 +105,17 @@ export class CellEditorStatusBar extends CellContentPart { event: e }); } else { - if ((e.target as HTMLElement).classList.contains('cell-status-item-has-command')) { + const target = e.target; + let itemHasCommand = false; + if (target && DOM.isHTMLElement(target)) { + const targetElement = target; + if (targetElement.classList.contains('cell-status-item-has-command')) { + itemHasCommand = true; + } else if (targetElement.parentElement && targetElement.parentElement.classList.contains('cell-status-item-has-command')) { + itemHasCommand = true; + } + } + if (itemHasCommand) { this._onDidClick.fire({ type: ClickTargetType.ContributedCommandItem, event: e From 9b569af9a49d9b3343419b1592b02fd6d4a84feb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 8 Sep 2024 23:53:33 +0200 Subject: [PATCH 171/286] fix #226660 (#227920) --- .../extensions/browser/extensionsActions.ts | 21 ++++---- .../browser/extensionsWorkbenchService.ts | 2 +- .../extensionsActions.test.ts | 3 +- .../electron-sandbox/extensionsViews.test.ts | 3 +- .../extensionsWorkbenchService.test.ts | 3 +- .../browser/extensionEnablementService.ts | 44 +++++++++++++--- .../common/extensionManagement.ts | 1 + .../extensionEnablementService.test.ts | 52 ++++++++++++++++++- 8 files changed, 105 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 0e6b8794df2..a263b5ba6f1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2571,10 +2571,11 @@ export class ExtensionStatusAction extends ExtensionAction { } } - // Extension is disabled by untrusted workspace - if (this.extension.enablementState === EnablementState.DisabledByTrustRequirement || - // All disabled dependencies of the extension are disabled by untrusted workspace - (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.workbenchExtensionEnablementService.getDependenciesEnablementStates(this.extension.local).every(([, enablementState]) => this.workbenchExtensionEnablementService.isEnabledEnablementState(enablementState) || enablementState === EnablementState.DisabledByTrustRequirement))) { + if (!this.workspaceTrustService.isWorkspaceTrusted() && + // Extension is disabled by untrusted workspace + (this.extension.enablementState === EnablementState.DisabledByTrustRequirement || + // All disabled dependencies of the extension are disabled by untrusted workspace + (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.workbenchExtensionEnablementService.getDependenciesEnablementStates(this.extension.local).every(([, enablementState]) => this.workbenchExtensionEnablementService.isEnabledEnablementState(enablementState) || enablementState === EnablementState.DisabledByTrustRequirement)))) { this.enabled = true; const untrustedDetails = getWorkspaceSupportTypeMessage(this.extension.local.manifest.capabilities?.untrustedWorkspaces); this.updateStatus({ icon: trustIcon, message: new MarkdownString(untrustedDetails ? escapeMarkdownSyntaxTokens(untrustedDetails) : localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted.")) }, true); @@ -2682,6 +2683,12 @@ export class ExtensionStatusAction extends ExtensionAction { return; } + if (!this.extension.local.isValid) { + const errors = this.extension.local.validations.filter(([severity]) => severity === Severity.Error).map(([, message]) => message); + this.updateStatus({ icon: errorIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); + return; + } + const isEnabled = this.workbenchExtensionEnablementService.isEnabled(this.extension.local); const isRunning = this.extensionService.extensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier)); @@ -2711,12 +2718,6 @@ export class ExtensionStatusAction extends ExtensionAction { return; } } - - if (isEnabled && !isRunning && !this.extension.local.isValid) { - const errors = this.extension.local.validations.filter(([severity]) => severity === Severity.Error).map(([, message]) => message); - this.updateStatus({ icon: errorIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); - } - } private updateStatus(status: ExtensionStatus | undefined, updateClass: boolean): void { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 183f43daa23..84ce0a60cff 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1435,7 +1435,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.isUnderDevelopment) { continue; } - if (extensionsToCheck.some(e => areSameExtensions({ id: extension.identifier.value, uuid: extension.uuid }, e.identifier))) { + if (extensionsToCheck.some(e => areSameExtensions({ id: extension.identifier.value, uuid: extension.uuid }, e.local?.identifier ?? e.identifier))) { continue; } // Extension is running but doesn't exist locally. Remove it from running extensions. diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index f5ca24e8f59..5341efafabf 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -2538,7 +2538,8 @@ function aLocalExtension(name: string = 'someext', manifest: any = {}, propertie type: ExtensionType.User, location: URI.file(`pub.${name}`), identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, - ...properties + ...properties, + isValid: properties.isValid ?? true, }; properties.isBuiltin = properties.type === ExtensionType.System; return Object.create({ manifest, ...properties }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 46881e77cd8..6507d4cb008 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -548,7 +548,8 @@ suite('ExtensionsViews Tests', () => { location: URI.file(`pub.${name}`), identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, metadata: { id: getGalleryExtensionId(manifest.publisher, manifest.name), publisherId: manifest.publisher, publisherDisplayName: 'somename' }, - ...properties + ...properties, + isValid: properties.isValid ?? true, }; properties.isBuiltin = properties.type === ExtensionType.System; return Object.create({ manifest, ...properties }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 15e65b0d1dd..375e0f597a2 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -1650,7 +1650,8 @@ suite('ExtensionsWorkbenchServiceTest', () => { type: ExtensionType.User, location: URI.file(`pub.${name}`), identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, - ...properties + ...properties, + isValid: properties.isValid ?? true, }; return Object.create({ manifest, ...properties }); } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 99aefca9884..8bd57b0d460 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -42,6 +42,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench protected readonly extensionsManager: ExtensionsManager; private readonly storageManager: StorageManager; + private extensionsDisabledByExtensionDependency: IExtension[] = []; constructor( @IStorageService storageService: IStorageService, @@ -71,6 +72,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench this.extensionsManager = this._register(instantiationService.createInstance(ExtensionsManager)); this.extensionsManager.whenInitialized().then(() => { if (!isDisposed) { + this._onDidChangeExtensions([], [], false); this._register(this.extensionsManager.onDidChangeExtensions(({ added, removed, isProfileSwitch }) => this._onDidChangeExtensions(added, removed, isProfileSwitch))); uninstallDisposable.dispose(); } @@ -161,6 +163,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench throw new Error(localize('cannot change enablement virtual workspace', "Cannot change enablement of {0} extension because it does not support virtual workspaces", extension.manifest.displayName || extension.identifier.id)); case EnablementState.DisabledByExtensionKind: throw new Error(localize('cannot change enablement extension kind', "Cannot change enablement of {0} extension because of its extension kind", extension.manifest.displayName || extension.identifier.id)); + case EnablementState.DisabledByInvalidExtension: + throw new Error(localize('cannot change invalid extension enablement', "Cannot change enablement of {0} extension because of it is invalid", extension.manifest.displayName || extension.identifier.id)); case EnablementState.DisabledByExtensionDependency: if (donotCheckDependencies) { break; @@ -334,8 +338,13 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } enablementState = this._getUserEnablementState(extension.identifier); + const isEnabled = this.isEnabledEnablementState(enablementState); - if (this.extensionBisectService.isDisabledByBisect(extension)) { + if (isEnabled && !extension.isValid) { + enablementState = EnablementState.DisabledByInvalidExtension; + } + + else if (this.extensionBisectService.isDisabledByBisect(extension)) { enablementState = EnablementState.DisabledByEnvironment; } @@ -347,7 +356,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench enablementState = EnablementState.DisabledByVirtualWorkspace; } - else if (this.isEnabledEnablementState(enablementState) && this._isDisabledByWorkspaceTrust(extension, workspaceType)) { + else if (isEnabled && this._isDisabledByWorkspaceTrust(extension, workspaceType)) { enablementState = EnablementState.DisabledByTrustRequirement; } @@ -355,11 +364,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench enablementState = EnablementState.DisabledByExtensionKind; } - else if (this.isEnabledEnablementState(enablementState) && this._isDisabledByExtensionDependency(extension, extensions, workspaceType, computedEnablementStates)) { + else if (isEnabled && this._isDisabledByExtensionDependency(extension, extensions, workspaceType, computedEnablementStates)) { enablementState = EnablementState.DisabledByExtensionDependency; } - else if (!this.isEnabledEnablementState(enablementState) && this._isEnabledInEnv(extension)) { + else if (!isEnabled && this._isEnabledInEnv(extension)) { enablementState = EnablementState.EnabledByEnvironment; } @@ -465,7 +474,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench : []; if (!dependencyExtensions.length) { - return false; + return !!extensions.length && !!extension.manifest.extensionDependencies?.length; } const hasEnablementState = computedEnablementStates.has(extension); @@ -626,9 +635,21 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private _onDidChangeExtensions(added: ReadonlyArray, removed: ReadonlyArray, isProfileSwitch: boolean): void { - const disabledExtensions = added.filter(e => !this.isEnabledEnablementState(this.getEnablementState(e))); - if (disabledExtensions.length) { - this._onEnablementChanged.fire(disabledExtensions); + const changedExtensions: IExtension[] = added.filter(e => !this.isEnabledEnablementState(this.getEnablementState(e))); + const existingExtensionsDisabledByExtensionDependency = this.extensionsDisabledByExtensionDependency; + this.extensionsDisabledByExtensionDependency = this.extensionsManager.extensions.filter(extension => this.getEnablementState(extension) === EnablementState.DisabledByExtensionDependency); + for (const extension of existingExtensionsDisabledByExtensionDependency) { + if (this.extensionsDisabledByExtensionDependency.every(e => !areSameExtensions(e.identifier, extension.identifier))) { + changedExtensions.push(extension); + } + } + for (const extension of this.extensionsDisabledByExtensionDependency) { + if (existingExtensionsDisabledByExtensionDependency.every(e => !areSameExtensions(e.identifier, extension.identifier))) { + changedExtensions.push(extension); + } + } + if (changedExtensions.length) { + this._onEnablementChanged.fire(changedExtensions); } if (!isProfileSwitch) { removed.forEach(({ identifier }) => this._reset(identifier)); @@ -714,6 +735,13 @@ class ExtensionsManager extends Disposable { private updateExtensions(added: IExtension[], identifiers: IExtensionIdentifier[], server: IExtensionManagementServer | undefined, isProfileSwitch: boolean): void { if (added.length) { + for (const extension of added) { + const extensionServer = this.extensionManagementServerService.getExtensionManagementServer(extension); + const index = this._extensions.findIndex(e => areSameExtensions(e.identifier, extension.identifier) && this.extensionManagementServerService.getExtensionManagementServer(e) === extensionServer); + if (index !== -1) { + this._extensions.splice(index, 1); + } + } this._extensions.push(...added); } const removed: IExtension[] = []; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 07d4b25fbfc..32c82faa57d 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -94,6 +94,7 @@ export const enum EnablementState { DisabledByEnvironment, EnabledByEnvironment, DisabledByVirtualWorkspace, + DisabledByInvalidExtension, DisabledByExtensionDependency, DisabledGlobally, DisabledWorkspace, diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index fd3d4210cb7..351dc1729f0 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import * as sinon from 'sinon'; -import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, DidUpdateExtensionMetadata } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, DidUpdateExtensionMetadata, InstallOperation } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation, IProfileAwareExtensionManagementService, DidChangeProfileEvent } from '../../common/extensionManagement.js'; import { ExtensionEnablementService } from '../../browser/extensionEnablementService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; @@ -979,6 +979,53 @@ suite('ExtensionEnablementService Test', () => { assert.deepStrictEqual(testObject.getEnablementStates(installed), [EnablementState.DisabledGlobally, EnablementState.DisabledByExtensionDependency]); }); + test('test extension is disabled by dependency when it has a missing dependency', async () => { + const target = aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] }); + installed.push(target); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); + await (testObject).waitUntilInitialized(); + + assert.strictEqual(testObject.getEnablementState(target), EnablementState.DisabledByExtensionDependency); + }); + + test('test extension is disabled by invalidity', async () => { + const target = aLocalExtension2('pub.b', {}, { isValid: false }); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.DisabledByInvalidExtension); + }); + + test('test extension is disabled by dependency when it has a dependency that is invalid', async () => { + const target = aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] }); + installed.push(...[target, aLocalExtension2('pub.a', {}, { isValid: false })]); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); + await (testObject).waitUntilInitialized(); + + assert.strictEqual(testObject.getEnablementState(target), EnablementState.DisabledByExtensionDependency); + }); + + test('test extension is enabled when its dependency becomes valid', async () => { + const extension = aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] }); + installed.push(...[extension, aLocalExtension2('pub.a', {}, { isValid: false })]); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); + await (testObject).waitUntilInitialized(); + + assert.strictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByExtensionDependency); + + const target = sinon.spy(); + disposableStore.add(testObject.onEnablementChanged(target)); + + const validExtension = aLocalExtension2('pub.a'); + didInstallEvent.fire([{ + identifier: validExtension.identifier, + operation: InstallOperation.Install, + source: validExtension.location, + profileLocation: validExtension.location, + local: validExtension, + }]); + + assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); + assert.deepStrictEqual((target.args[0][0][0]).identifier, { id: 'pub.b' }); + }); + test('test override workspace to trusted when getting extensions enablements', async () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: false, description: 'hello' } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return false; } }); @@ -1080,7 +1127,8 @@ function aLocalExtension2(id: string, manifest: Partial = {} location: URI.file(`pub.${name}`), galleryIdentifier: { id, uuid: undefined }, type: ExtensionType.User, - ...properties + ...properties, + isValid: properties.isValid ?? true, }; properties.isBuiltin = properties.type === ExtensionType.System; return Object.create({ manifest, ...properties }); From 56d050794db8668e5e0b64eda377ef5702512be6 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Sep 2024 10:45:46 +0200 Subject: [PATCH 172/286] fix #227765 (#227943) --- .../browser/extensions.contribution.ts | 4 +- .../browser/extensionsWorkbenchService.ts | 53 +++++++++---------- .../contrib/extensions/common/extensions.ts | 2 +- .../extensionsWorkbenchService.test.ts | 4 +- 4 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index a531a89c251..b5dc1e178fc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -645,7 +645,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }, { id: MenuId.CommandPalette, }], - run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateValue(true) + run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateForAllExtensions(true) }); const disableAutoUpdateWhenCondition = ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, false); @@ -662,7 +662,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }, { id: MenuId.CommandPalette, }], - run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateValue(false) + run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateForAllExtensions(false) }); this.registerExtensionAction({ diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 84ce0a60cff..08a136c6a60 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1039,22 +1039,10 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private initializeAutoUpdate(): void { - // Initialise Auto Update Value - let autoUpdateValue = this.getAutoUpdateValue(); - // Register listeners for auto updates this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - const wasAutoUpdateEnabled = autoUpdateValue !== false; - autoUpdateValue = this.getAutoUpdateValue(); - const isAutoUpdateEnabled = this.isAutoUpdateEnabled(); - if (wasAutoUpdateEnabled !== isAutoUpdateEnabled) { - this.setEnabledAutoUpdateExtensions([]); - this.setDisabledAutoUpdateExtensions([]); - this._onChange.fire(undefined); - this.updateExtensionsPinnedState(!isAutoUpdateEnabled); - } - if (isAutoUpdateEnabled) { + if (this.isAutoUpdateEnabled()) { this.eventuallyAutoUpdateExtensions(); } } @@ -1116,22 +1104,31 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return isBoolean(autoUpdate) || autoUpdate === 'onlyEnabledExtensions' ? autoUpdate : true; } - async updateAutoUpdateValue(value: AutoUpdateConfigurationValue): Promise { - const wasEnabled = this.isAutoUpdateEnabled(); - const isEnabled = value !== false; - if (wasEnabled !== isEnabled) { - const result = await this.dialogService.confirm({ - title: nls.localize('confirmEnableDisableAutoUpdate', "Auto Update Extensions"), - message: isEnabled - ? nls.localize('confirmEnableAutoUpdate', "Do you want to enable auto update for all extensions?") - : nls.localize('confirmDisableAutoUpdate', "Do you want to disable auto update for all extensions?"), - detail: nls.localize('confirmEnableDisableAutoUpdateDetail', "This will reset any auto update settings you have set for individual extensions."), - }); - if (!result.confirmed) { - return; - } + async updateAutoUpdateForAllExtensions(isAutoUpdateEnabled: boolean): Promise { + const wasAutoUpdateEnabled = this.isAutoUpdateEnabled(); + if (wasAutoUpdateEnabled === isAutoUpdateEnabled) { + return; } - await this.configurationService.updateValue(AutoUpdateConfigurationKey, value); + + const result = await this.dialogService.confirm({ + title: nls.localize('confirmEnableDisableAutoUpdate', "Auto Update Extensions"), + message: isAutoUpdateEnabled + ? nls.localize('confirmEnableAutoUpdate', "Do you want to enable auto update for all extensions?") + : nls.localize('confirmDisableAutoUpdate', "Do you want to disable auto update for all extensions?"), + detail: nls.localize('confirmEnableDisableAutoUpdateDetail', "This will reset any auto update settings you have set for individual extensions."), + }); + if (!result.confirmed) { + return; + } + + // Reset extensions enabled for auto update first to prevent them from being updated + this.setEnabledAutoUpdateExtensions([]); + + await this.configurationService.updateValue(AutoUpdateConfigurationKey, isAutoUpdateEnabled); + + this.setDisabledAutoUpdateExtensions([]); + await this.updateExtensionsPinnedState(!isAutoUpdateEnabled); + this._onChange.fire(undefined); } private readonly autoRestartListenerDisposable = this._register(new MutableDisposable()); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index e727e00d41e..0d0ce95fbe3 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -139,8 +139,8 @@ export interface IExtensionsWorkbenchService { isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean; updateAutoUpdateEnablementFor(extensionOrPublisher: IExtension | string, enable: boolean): Promise; shouldRequireConsentToUpdate(extension: IExtension): Promise; + updateAutoUpdateForAllExtensions(value: boolean): Promise; open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise; - updateAutoUpdateValue(value: AutoUpdateConfigurationValue): Promise; getAutoUpdateValue(): AutoUpdateConfigurationValue; checkForUpdates(): Promise; getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 375e0f597a2..6c0819d809f 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -1581,7 +1581,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), ['pub.a']); - await testObject.updateAutoUpdateValue(false); + await testObject.updateAutoUpdateForAllExtensions(false); assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); @@ -1607,7 +1607,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), ['pub.a']); assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); - await testObject.updateAutoUpdateValue(true); + await testObject.updateAutoUpdateForAllExtensions(true); assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); From 5c97ba8357d980188cafe9c77c9ab39e2bfd7939 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Sep 2024 10:54:29 +0200 Subject: [PATCH 173/286] log network requests in the process specific log (#227944) --- src/vs/code/electron-main/main.ts | 6 +-- .../sharedProcess/sharedProcessMain.ts | 6 +-- src/vs/code/node/cliProcessMain.ts | 4 +- src/vs/platform/request/common/request.ts | 12 +++--- .../platform/request/node/requestService.ts | 7 ++-- src/vs/server/node/serverServices.ts | 6 +-- src/vs/workbench/browser/web.main.ts | 2 +- .../request/common/request.contribution.ts | 37 ------------------- .../test/node/colorRegistry.releaseTest.ts | 4 +- .../request/browser/requestService.ts | 10 ++--- .../electron-sandbox/requestService.ts | 10 ++--- src/vs/workbench/workbench.common.main.ts | 3 -- 12 files changed, 23 insertions(+), 84 deletions(-) delete mode 100644 src/vs/workbench/contrib/request/common/request.contribution.ts diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 7b6961ddf88..46b2d0b7256 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -211,11 +211,7 @@ class CodeMain { services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService, undefined, false)); // Request - const networkLogger = loggerService.createLogger('network-main', { - name: localize('network-main', "Network (Main)"), - hidden: true - }); - services.set(IRequestService, new SyncDescriptor(RequestService, [networkLogger], true)); + services.set(IRequestService, new SyncDescriptor(RequestService, undefined, true)); // Themes services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index ac39f266781..2a99598a73d 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -270,11 +270,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { ]); // Request - const networkLogger = loggerService.createLogger('network-shared', { - name: localize('network-shared', "Network (Shared)"), - hidden: true, - }); - const requestService = new RequestService(networkLogger, configurationService, environmentService, logService); + const requestService = new RequestService(configurationService, environmentService, logService); services.set(IRequestService, requestService); // Checksum diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 535c724b438..d3b20ecea47 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -38,7 +38,7 @@ import { InstantiationService } from '../../platform/instantiation/common/instan import { ServiceCollection } from '../../platform/instantiation/common/serviceCollection.js'; import { ILanguagePackService } from '../../platform/languagePacks/common/languagePacks.js'; import { NativeLanguagePackService } from '../../platform/languagePacks/node/languagePacks.js'; -import { ConsoleLogger, getLogLevel, ILogger, ILoggerService, ILogService, LogLevel, NullLogger } from '../../platform/log/common/log.js'; +import { ConsoleLogger, getLogLevel, ILogger, ILoggerService, ILogService, LogLevel } from '../../platform/log/common/log.js'; import { FilePolicyService } from '../../platform/policy/common/filePolicyService.js'; import { IPolicyService, NullPolicyService } from '../../platform/policy/common/policy.js'; import { NativePolicyService } from '../../platform/policy/node/nativePolicyService.js'; @@ -195,7 +195,7 @@ class CliMain extends Disposable { services.set(IUriIdentityService, new UriIdentityService(fileService)); // Request - const requestService = new RequestService(new NullLogger(), configurationService, environmentService, logService); + const requestService = new RequestService(configurationService, environmentService, logService); services.set(IRequestService, requestService); // Download Service diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 25da055924c..46a78f000ba 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -11,7 +11,7 @@ import { IHeaders, IRequestContext, IRequestOptions } from '../../../base/parts/ import { localize } from '../../../nls.js'; import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; -import { ILogger } from '../../log/common/log.js'; +import { ILogService } from '../../log/common/log.js'; import { Registry } from '../../registry/common/platform.js'; export const IRequestService = createDecorator('requestService'); @@ -70,19 +70,19 @@ export abstract class AbstractRequestService extends Disposable implements IRequ private counter = 0; - constructor(protected readonly logger: ILogger) { + constructor(protected readonly logService: ILogService) { super(); } protected async logAndRequest(options: IRequestOptions, request: () => Promise): Promise { - const prefix = `#${++this.counter}: ${options.url}`; - this.logger.info(`${prefix} - begin`, options.type, new LoggableHeaders(options.headers ?? {})); + const prefix = `[network] #${++this.counter}: ${options.url}`; + this.logService.trace(`${prefix} - begin`, options.type, new LoggableHeaders(options.headers ?? {})); try { const result = await request(); - this.logger.info(`${prefix} - end`, options.type, result.res.statusCode, result.res.headers); + this.logService.trace(`${prefix} - end`, options.type, result.res.statusCode, result.res.headers); return result; } catch (error) { - this.logger.error(`${prefix} - error`, options.type, getErrorMessage(error)); + this.logService.error(`${prefix} - error`, options.type, getErrorMessage(error)); throw error; } } diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 981c60e48c5..010acda0546 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -16,7 +16,7 @@ import { IRequestContext, IRequestOptions } from '../../../base/parts/request/co import { IConfigurationService } from '../../configuration/common/configuration.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; -import { ILogService, ILogger } from '../../log/common/log.js'; +import { ILogService } from '../../log/common/log.js'; import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from '../common/request.js'; import { Agent, getProxyAgent } from './proxy.js'; import { createGunzip } from 'zlib'; @@ -52,12 +52,11 @@ export class RequestService extends AbstractRequestService implements IRequestSe private shellEnvErrorLogged?: boolean; constructor( - logger: ILogger, @IConfigurationService private readonly configurationService: IConfigurationService, @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @ILogService private readonly logService: ILogService, + @ILogService logService: ILogService, ) { - super(logger); + super(logService); this.configure(); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('http')) { diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index c66509b593e..70ebf61f857 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -148,11 +148,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionHostStatusService, extensionHostStatusService); // Request - const networkLogger = loggerService.createLogger('network-server', { - name: localize('network-server', "Network (Server)"), - hidden: true - }); - const requestService = new RequestService(networkLogger, configurationService, environmentService, logService); + const requestService = new RequestService(configurationService, environmentService, logService); services.set(IRequestService, requestService); let oneDsAppender: ITelemetryAppender = NullAppender; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 69140e5302e..069793bae0c 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -396,7 +396,7 @@ export class BrowserMain extends Disposable { this._register(workspaceTrustManagementService.onDidChangeTrust(() => configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()))); // Request Service - const requestService = new BrowserRequestService(remoteAgentService, configurationService, loggerService); + const requestService = new BrowserRequestService(remoteAgentService, configurationService, logService); serviceCollection.set(IRequestService, requestService); // Userdata Sync Store Management Service diff --git a/src/vs/workbench/contrib/request/common/request.contribution.ts b/src/vs/workbench/contrib/request/common/request.contribution.ts deleted file mode 100644 index cdc1d1d4865..00000000000 --- a/src/vs/workbench/contrib/request/common/request.contribution.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from '../../../../base/common/event.js'; -import { localize2 } from '../../../../nls.js'; -import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILoggerService } from '../../../../platform/log/common/log.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../services/output/common/output.js'; - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'workbench.actions.showNetworkLog', - title: localize2('showNetworkLog', "Show Network Log"), - category: Categories.Developer, - f1: true, - }); - } - async run(servicesAccessor: ServicesAccessor): Promise { - const loggerService = servicesAccessor.get(ILoggerService); - const outputService = servicesAccessor.get(IOutputService); - for (const logger of loggerService.getRegisteredLoggers()) { - if (logger.id.startsWith('network-')) { - loggerService.setVisibility(logger.id, true); - } - } - if (!outputService.getChannelDescriptor('network-window')) { - await Event.toPromise(Event.filter(Registry.as(Extensions.OutputChannels).onDidRegisterChannel, channel => channel === 'network-window')); - } - outputService.showChannel('network-window'); - } -}); diff --git a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts index 91100d7138f..50a4b4b6837 100644 --- a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts @@ -15,7 +15,7 @@ import { RequestService } from '../../../../../platform/request/node/requestServ import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; // eslint-disable-next-line local/code-import-patterns import '../../../../workbench.desktop.main.js'; -import { NullLogger, NullLogService } from '../../../../../platform/log/common/log.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { INativeEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { FileAccess } from '../../../../../base/common/network.js'; @@ -91,7 +91,7 @@ suite('Color Registry', function () { const docUrl = 'https://raw.githubusercontent.com/microsoft/vscode-docs/main/api/references/theme-color.md'; - const reqContext = await new RequestService(new NullLogger(), new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); + const reqContext = await new RequestService(new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); const content = (await asTextOrError(reqContext))!; const expression = /-\s*\`([\w\.]+)\`: (.*)/g; diff --git a/src/vs/workbench/services/request/browser/requestService.ts b/src/vs/workbench/services/request/browser/requestService.ts index b2138eea0fa..afa0b0f4358 100644 --- a/src/vs/workbench/services/request/browser/requestService.ts +++ b/src/vs/workbench/services/request/browser/requestService.ts @@ -12,8 +12,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from '../../../../platform/request/common/request.js'; import { request } from '../../../../base/parts/request/browser/request.js'; -import { ILoggerService } from '../../../../platform/log/common/log.js'; -import { localize } from '../../../../nls.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; export class BrowserRequestService extends AbstractRequestService implements IRequestService { @@ -22,12 +21,9 @@ export class BrowserRequestService extends AbstractRequestService implements IRe constructor( @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILoggerService loggerService: ILoggerService, + @ILogService logService: ILogService, ) { - super(loggerService.createLogger('network-window', { - name: localize('network-window', "Network (Window)"), - hidden: true - })); + super(logService); } async request(options: IRequestOptions, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/request/electron-sandbox/requestService.ts b/src/vs/workbench/services/request/electron-sandbox/requestService.ts index 8e8ba4e4c9d..f0b2a760c56 100644 --- a/src/vs/workbench/services/request/electron-sandbox/requestService.ts +++ b/src/vs/workbench/services/request/electron-sandbox/requestService.ts @@ -10,8 +10,7 @@ import { INativeHostService } from '../../../../platform/native/common/native.js import { IRequestContext, IRequestOptions } from '../../../../base/parts/request/common/request.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { request } from '../../../../base/parts/request/browser/request.js'; -import { ILoggerService } from '../../../../platform/log/common/log.js'; -import { localize } from '../../../../nls.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; export class NativeRequestService extends AbstractRequestService implements IRequestService { @@ -20,12 +19,9 @@ export class NativeRequestService extends AbstractRequestService implements IReq constructor( @INativeHostService private readonly nativeHostService: INativeHostService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILoggerService loggerService: ILoggerService, + @ILogService logService: ILogService, ) { - super(loggerService.createLogger('network-window', { - name: localize('network-window', "Network (Window)"), - hidden: true - })); + super(logService); } async request(options: IRequestOptions, token: CancellationToken): Promise { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index ffe569930b7..285fea2c50e 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -398,7 +398,4 @@ import './contrib/accountEntitlements/browser/accountsEntitlements.contribution. // Synchronized Scrolling import './contrib/scrollLocking/browser/scrollLocking.contribution.js'; -// Network -import './contrib/request/common/request.contribution.js'; - //#endregion From b6b5da01c4557b2ae083df05d5bab147a205f73c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:33:27 +0200 Subject: [PATCH 174/286] No tab bar handle closed editors (#227947) --- .../browser/parts/editor/noEditorTabsControl.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts index 061643f23d1..af16587fdb0 100644 --- a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts @@ -47,9 +47,17 @@ export class NoEditorTabsControl extends EditorTabsControl { beforeCloseEditor(editor: EditorInput): void { } - closeEditor(editor: EditorInput): void { } + closeEditor(editor: EditorInput): void { + this.handleClosedEditors(); + } - closeEditors(editors: EditorInput[]): void { } + closeEditors(editors: EditorInput[]): void { + this.handleClosedEditors(); + } + + private handleClosedEditors(): void { + this.activeEditor = this.tabsModel.activeEditor; + } moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number): void { } From 65e0b66786131851da09fead88920531cda2d18c Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 9 Sep 2024 11:36:39 +0200 Subject: [PATCH 175/286] dynamic #file completions, fed from history and file search (#227950) * dynamic #file completions, fed from history and file search https://github.com/microsoft/vscode/issues/227946 * signal that there are more suggestions --- .../browser/contrib/chatInputCompletions.ts | 150 +++++++++++++++--- 1 file changed, 132 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 96dbb4278d1..a751a34bf6f 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -8,25 +8,34 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IWordAtPosition, getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; -import { CompletionContext, CompletionItem, CompletionItemKind } from '../../../../../editor/common/languages.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } from '../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; import { SubmitAction } from '../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatInputPart } from '../chatInputPart.js'; -import { SelectAndInsertFileAction } from './chatDynamicVariables.js'; +import { ChatDynamicVariableModel, SelectAndInsertFileAction } from './chatDynamicVariables.js'; import { ChatAgentLocation, getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../common/chatAgents.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; -import { IChatVariablesService } from '../../common/chatVariables.js'; +import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; +import { IHistoryService } from '../../../../services/history/common/history.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { isPatternInWord } from '../../../../../base/common/filters.js'; +import { ISearchService } from '../../../../services/search/common/search.js'; +import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { URI } from '../../../../../base/common/uri.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -301,19 +310,35 @@ class AssignSelectedAgentAction extends Action2 { } registerAction2(AssignSelectedAgentAction); + +class ReferenceArgument { + constructor( + readonly widget: IChatWidget, + readonly variable: IDynamicVariable + ) { } +} + class BuiltinDynamicCompletions extends Disposable { + private static readonly addReferenceCommand = '_addReferenceCmd'; private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag + private readonly queryBuilder: QueryBuilder; + constructor( + @IHistoryService private readonly historyService: IHistoryService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @ISearchService private readonly searchService: ISearchService, + @ILabelService private readonly labelService: ILabelService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'chatDynamicCompletions', triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); if (!widget || !widget.supportsFileReferences) { return null; @@ -324,22 +349,111 @@ class BuiltinDynamicCompletions extends Disposable { return null; } + const result: CompletionList = { suggestions: [] }; + const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - return { - suggestions: [ - { - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - detail: localize('pickFileLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - } satisfies CompletionItem - ] - }; + result.suggestions.push({ + label: `${chatVariableLeader}file`, + insertText: `${chatVariableLeader}file:`, + detail: localize('pickFileLabel', "Pick a file"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, + sortText: 'z' + }); + + + await this.addFileEntries(widget, result, range, token); + + return result; } })); + + this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); + + this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); + } + + private async addFileEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { + + const makeFileCompletionItem = (resource: URI): CompletionItem => { + + const basename = this.labelService.getUriBasenameLabel(resource); + const insertText = `${chatVariableLeader}file:${basename} `; + + return { + label: { label: `${chatVariableLeader}file:${basename}`, description: this.labelService.getUriLabel(resource) }, + insertText, + range: info, + kind: CompletionItemKind.File, + sortText: 'zz', + command: { + id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { + id: 'vscode.file', + range: { startLineNumber: info.replace.startLineNumber, startColumn: info.replace.startColumn, endLineNumber: info.replace.endLineNumber, endColumn: info.replace.startColumn + insertText.length }, + data: resource + })] + } + }; + }; + + let pattern: string | undefined; + if (info.varWord?.word && info.varWord.word.startsWith(chatVariableLeader)) { + pattern = info.varWord.word.toLowerCase().slice(1); // remove leading # + } + + const seen = new ResourceSet(); + const len = result.suggestions.length; + + // HISTORY + // always take the last N items + for (const item of this.historyService.getHistory()) { + if (!item.resource || !this.workspaceContextService.getWorkspaceFolder(item.resource)) { + // ignore "forgein" editors + continue; + } + + if (pattern) { + // use pattern if available + const basename = this.labelService.getUriBasenameLabel(item.resource).toLowerCase(); + if (!isPatternInWord(pattern, 0, pattern.length, basename, 0, basename.length)) { + continue; + } + } + + seen.add(item.resource); + const newLen = result.suggestions.push(makeFileCompletionItem(item.resource)); + if (newLen - len >= 5) { + break; + } + } + + // SEARCH + // use file search when having a pattern + if (pattern) { + const query = this.queryBuilder.file(this.workspaceContextService.getWorkspace().folders, { + filePattern: pattern, + maxResults: 25 + }); + + const data = await this.searchService.fileSearch(query, token); + for (const match of data.results) { + if (seen.has(match.resource)) { + // already included via history + continue; + } + result.suggestions.push(makeFileCompletionItem(match.resource)); + } + + if (!data.limitHit) { + result.incomplete = true; + } + } + } + + private cmdAddReference(arg: ReferenceArgument) { + // invoked via the completion command + arg.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference(arg.variable); } } From 884cfb16a3ad080fe633f83eca5e4d442de96143 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:55:46 +0200 Subject: [PATCH 176/286] Reset traits after switching model (#227956) reset traits after switching model --- src/vs/base/browser/ui/tree/abstractTree.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 97f3c6015fb..023b00af3a4 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -3144,6 +3144,10 @@ export abstract class AbstractTree implements IDisposable this.renderers.forEach(r => r.setModel(newModel)); this.stickyScrollController?.setModel(newModel); + this.focus.set([]); + this.selection.set([]); + this.anchor.set([]); + this.view.splice(0, oldModel.getListRenderCount(oldModel.rootRef)); this.model.refilter(); From 3ab41c2f693f119a91cf59537132517902f5f120 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:02:31 +0200 Subject: [PATCH 177/286] SCM Graph - add branch picker (#227949) * WIP - saving my work * Extract HistoryItemRef picker * Extract Repository picker * Improve history item ref picker rendering * Refactor color map * Refresh the graph when the filter changes * Push minor fix --- extensions/git/src/historyProvider.ts | 104 ++++- src/vs/workbench/api/browser/mainThreadSCM.ts | 62 ++- .../workbench/api/common/extHost.protocol.ts | 15 +- src/vs/workbench/api/common/extHostSCM.ts | 15 +- .../contrib/scm/browser/media/scm.css | 8 +- .../contrib/scm/browser/scmHistory.ts | 28 +- .../contrib/scm/browser/scmHistoryViewPane.ts | 402 ++++++++++++++---- .../workbench/contrib/scm/common/history.ts | 17 +- .../scm/test/browser/scmHistory.test.ts | 12 +- .../vscode.proposed.scmHistoryProvider.d.ts | 32 +- 10 files changed, 534 insertions(+), 161 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 7d79e128844..6190c2a538b 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryChangeEvent, SourceControlHistoryItemRef, l10n } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, dispose } from './util'; import { toGitUri } from './uri'; @@ -17,6 +17,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter(); readonly onDidChangeCurrentHistoryItemGroup: Event = this._onDidChangeCurrentHistoryItemGroup.event; + private readonly _onDidChangeHistory = new EventEmitter(); + readonly onDidChangeHistory: Event = this._onDidChangeHistory.event; + private readonly _onDidChangeDecorations = new EventEmitter(); readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; @@ -28,12 +31,6 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } private historyItemDecorations = new Map(); - private historyItemLabels = new Map([ - ['HEAD -> refs/heads/', new ThemeIcon('target')], - ['tag: refs/tags/', new ThemeIcon('tag')], - ['refs/heads/', new ThemeIcon('git-branch')], - ['refs/remotes/', new ThemeIcon('cloud')], - ]); private disposables: Disposable[] = []; @@ -85,6 +82,51 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup: ${JSON.stringify(this.currentHistoryItemGroup)}`); } + async provideHistoryItemRefs(): Promise { + const refs = await this.repository.getRefs(); + + const branches: SourceControlHistoryItemRef[] = []; + const remoteBranches: SourceControlHistoryItemRef[] = []; + const tags: SourceControlHistoryItemRef[] = []; + + for (const ref of refs) { + switch (ref.type) { + case RefType.RemoteHead: + remoteBranches.push({ + id: `refs/remotes/${ref.remote}/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('cloud'), + category: l10n.t('remote branches') + }); + break; + case RefType.Tag: + tags.push({ + id: `refs/tags/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('tag'), + category: l10n.t('tags') + }); + break; + default: + branches.push({ + id: `refs/heads/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? ref.commit.substring(0, 8) : undefined, + revision: ref.commit, + icon: new ThemeIcon('git-branch'), + category: l10n.t('branches') + }); + break; + } + } + + return [...branches, ...remoteBranches, ...tags]; + } + async provideHistoryItems(options: SourceControlHistoryOptions): Promise { if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { return []; @@ -115,7 +157,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec await ensureEmojis(); return commits.map(commit => { - const labels = this.resolveHistoryItemLabels(commit); + const references = this.resolveHistoryItemRefs(commit); return { id: commit.hash, @@ -126,7 +168,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec displayId: commit.hash.substring(0, 8), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, - labels: labels.length !== 0 ? labels : undefined + references: references.length !== 0 ? references : undefined }; }); } catch (err) { @@ -208,19 +250,47 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return this.historyItemDecorations.get(uri.toString()); } - private resolveHistoryItemLabels(commit: Commit): SourceControlHistoryItemLabel[] { - const labels: SourceControlHistoryItemLabel[] = []; + private resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRef[] { + const references: SourceControlHistoryItemRef[] = []; - for (const label of commit.refNames) { - for (const [key, value] of this.historyItemLabels) { - if (label.startsWith(key)) { - labels.push({ title: label.substring(key.length), icon: value }); + for (const ref of commit.refNames) { + switch (true) { + case ref.startsWith('HEAD -> refs/heads/'): + references.push({ + id: ref.substring('HEAD -> '.length), + name: ref.substring('HEAD -> refs/heads/'.length), + revision: commit.hash, + icon: new ThemeIcon('target') + }); + break; + case ref.startsWith('tag: refs/tags/'): + references.push({ + id: ref.substring('tag: '.length), + name: ref.substring('tag: refs/tags/'.length), + revision: commit.hash, + icon: new ThemeIcon('tag') + }); + break; + case ref.startsWith('refs/heads/'): + references.push({ + id: ref, + name: ref.substring('refs/heads/'.length), + revision: commit.hash, + icon: new ThemeIcon('git-branch') + }); + break; + case ref.startsWith('refs/remotes/'): + references.push({ + id: ref, + name: ref.substring('refs/remotes/'.length), + revision: commit.hash, + icon: new ThemeIcon('cloud') + }); break; - } } } - return labels; + return references; } private async resolveHEADMergeBase(): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 221202313bd..232a052bb7e 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -6,7 +6,7 @@ import { Barrier } from '../../../base/common/async.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { Event, Emitter } from '../../../base/common/event.js'; -import { observableValue, observableValueOpts } from '../../../base/common/observable.js'; +import { derivedOpts, observableValue, observableValueOpts } from '../../../base/common/observable.js'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from '../../contrib/scm/common/scm.js'; import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol.js'; @@ -17,7 +17,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemRef, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; import { ResourceTree } from '../../../base/common/resourceTree.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js'; @@ -28,6 +28,8 @@ import { ITextModelContentProvider, ITextModelService } from '../../../editor/co import { Schemas } from '../../../base/common/network.js'; import { ITextModel } from '../../../editor/common/model.js'; import { structuralEquals } from '../../../base/common/equals.js'; +import { Codicon } from '../../../base/common/codicons.js'; +import { historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote } from '../../contrib/scm/browser/scmHistory.js'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -43,15 +45,15 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { - const labels = historyItemDto.labels?.map(l => ({ - title: l.title, icon: getIconFromIconDto(l.icon) + const references = historyItemDto.references?.map(r => ({ + ...r, icon: getIconFromIconDto(r.icon) })); const newLineIndex = historyItemDto.message.indexOf('\n'); const subject = newLineIndex === -1 ? historyItemDto.message : `${historyItemDto.message.substring(0, newLineIndex)}\u2026`; - return { ...historyItemDto, subject, labels }; + return { ...historyItemDto, subject, references }; } class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { @@ -171,12 +173,62 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { }, undefined); get currentHistoryItemGroup() { return this._currentHistoryItemGroup; } + readonly currentHistoryItemRef = derivedOpts({ + owner: this, + equalsFn: structuralEquals + }, reader => { + const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader); + + return currentHistoryItemGroup ? { + id: currentHistoryItemGroup.id ?? '', + name: currentHistoryItemGroup.name, + revision: currentHistoryItemGroup.revision, + color: historyItemGroupLocal, + icon: Codicon.target, + } : undefined; + }); + + readonly currentHistoryItemRemoteRef = derivedOpts({ + owner: this, + equalsFn: structuralEquals + }, reader => { + const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader); + + return currentHistoryItemGroup?.remote ? { + id: currentHistoryItemGroup.remote.id ?? '', + name: currentHistoryItemGroup.remote.name, + revision: currentHistoryItemGroup.remote.revision, + color: historyItemGroupRemote, + icon: Codicon.cloud, + } : undefined; + }); + + readonly currentHistoryItemBaseRef = derivedOpts({ + owner: this, + equalsFn: structuralEquals + }, reader => { + const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader); + + return currentHistoryItemGroup?.base ? { + id: currentHistoryItemGroup.base.id ?? '', + name: currentHistoryItemGroup.base.name, + revision: currentHistoryItemGroup.base.revision, + color: historyItemGroupBase, + icon: Codicon.cloud, + } : undefined; + }); + constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } async resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise { return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupIds, CancellationToken.None); } + async provideHistoryItemRefs(): Promise { + const historyItemRefs = await this.proxy.$provideHistoryItemRefs(this.handle, CancellationToken.None); + return historyItemRefs?.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); + } + async provideHistoryItems(options: ISCMHistoryOptions): Promise { const historyItems = await this.proxy.$provideHistoryItems(this.handle, options, CancellationToken.None); return historyItems?.map(historyItem => toISCMHistoryItem(historyItem)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0a34cf85f18..8b4cdc0a075 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1550,6 +1550,15 @@ export interface SCMHistoryItemGroupDto { readonly remote?: Omit, 'remote'>; } +export interface SCMHistoryItemRefDto { + readonly id: string; + readonly name: string; + readonly revision?: string; + readonly category?: string; + readonly description?: string; + readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; +} + export interface SCMHistoryItemDto { readonly id: string; readonly parentIds: string[]; @@ -1562,10 +1571,7 @@ export interface SCMHistoryItemDto { readonly insertions: number; readonly deletions: number; }; - readonly labels?: { - readonly title: string; - readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; - }[]; + readonly references?: SCMHistoryItemRefDto[]; } export interface SCMHistoryItemChangeDto { @@ -2358,6 +2364,7 @@ export interface ExtHostSCMShape { $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise; $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise; + $provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise; $provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 75453dbe3f2..a600b14bd04 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -11,7 +11,7 @@ import { debounce } from '../../../base/common/decorators.js'; import { DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; import { asPromise } from '../../../base/common/async.js'; import { ExtHostCommands } from './extHostCommands.js'; -import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol.js'; +import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto, SCMHistoryItemRefDto } from './extHost.protocol.js'; import { sortedDiff, equals } from '../../../base/common/arrays.js'; import { comparePaths } from '../../../base/common/comparers.js'; import type * as vscode from 'vscode'; @@ -72,11 +72,11 @@ function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vsc } function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { - const labels = historyItem.labels?.map(l => ({ - title: l.title, icon: getHistoryItemIconDto(l.icon) + const references = historyItem.references?.map(r => ({ + ...r, icon: getHistoryItemIconDto(r.icon) })); - return { ...historyItem, labels }; + return { ...historyItem, references }; } function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number { @@ -982,6 +982,13 @@ export class ExtHostSCM implements ExtHostSCMShape { return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupIds, token) ?? undefined; } + async $provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const historyItemRefs = await historyProvider?.provideHistoryItemRefs(token); + + return historyItemRefs?.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })) ?? undefined; + } + async $provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; const historyItems = await historyProvider?.provideHistoryItems(options, token); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index f41adfd5435..eb98f680a0b 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -495,13 +495,15 @@ display: flex; } -.monaco-toolbar .action-label.scm-graph-repository-picker { +.monaco-toolbar .action-label.scm-graph-repository-picker, +.monaco-toolbar .action-label.scm-graph-history-item-picker { align-items: center; font-weight: normal; line-height: 16px; } -.monaco-toolbar .action-label.scm-graph-repository-picker .codicon { +.monaco-toolbar .action-label.scm-graph-repository-picker .codicon, +.monaco-toolbar .action-label.scm-graph-history-item-picker .codicon { font-size: 14px; } @@ -558,7 +560,7 @@ .scm-history-view .history-item-load-more .history-item-placeholder.shimmer .monaco-icon-label-container { height: 18px; - background: var(--vscode-scm-historyItemDefaultLabelBackground); + background: var(--vscode-scmGraph-historyItemHoverDefaultLabelBackground); border-radius: 2px; opacity: 0.5; } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index c51e6fbcdd8..08e24fe1870 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -43,11 +43,11 @@ export const colorRegistry: ColorIdentifier[] = [ registerColor('scmGraph.foreground3', chartsYellow, localize('scmGraphForeground3', "Source control graph foreground color (3).")), ]; -function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map): ColorIdentifier | undefined { - for (const label of historyItem.labels ?? []) { - const colorIndex = colorMap.get(label.title); - if (colorIndex !== undefined) { - return colorIndex; +function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map): ColorIdentifier | undefined { + for (const ref of historyItem.references ?? []) { + const colorIdentifier = colorMap.get(ref.id); + if (colorIdentifier !== undefined) { + return colorIdentifier; } } @@ -215,7 +215,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV } else { // HEAD // TODO@lszomoru - implement a better way to determine if the commit is HEAD - if (historyItem.labels?.some(l => ThemeIcon.isThemeIcon(l.icon) && l.icon.id === 'target')) { + if (historyItem.references?.some(ref => ThemeIcon.isThemeIcon(ref.icon) && ref.icon.id === 'target')) { const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, circleColor); svg.append(outerCircle); } @@ -246,7 +246,7 @@ export function renderSCMHistoryGraphPlaceholder(columns: ISCMHistoryItemGraphNo return elements.root; } -export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map()): ISCMHistoryItemViewModel[] { +export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map()): ISCMHistoryItemViewModel[] { let colorIndex = -1; const viewModels: ISCMHistoryItemViewModel[] = []; @@ -302,11 +302,11 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], }); } - // Add colors to labels - const labels = (historyItem.labels ?? []) - .map(label => { - let color = colorMap.get(label.title); - if (!color && colorMap.has('*')) { + // Add colors to references + const references = (historyItem.references ?? []) + .map(ref => { + let color = colorMap.get(ref.id); + if (colorMap.has(ref.id) && color === undefined) { // Find the history item in the input swimlanes const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id); @@ -318,13 +318,13 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemGroupLocal; } - return { ...label, color }; + return { ...ref, color }; }); viewModels.push({ historyItem: { ...historyItem, - labels + references }, inputSwimlanes, outputSwimlanes, diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 51916fe9657..00660ae54a1 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -34,7 +34,7 @@ import { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../ import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { renderSCMHistoryItemGraph, historyItemGroupLocal, historyItemGroupRemote, historyItemGroupBase, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverDeletionsForeground, historyItemHoverLabelForeground, historyItemHoverAdditionsForeground, historyItemHoverDefaultLabelForeground, historyItemHoverDefaultLabelBackground } from './scmHistory.js'; import { isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js'; -import { ISCMHistoryItem, ISCMHistoryItemGroup, ISCMHistoryItemViewModel, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { stripIcons } from '../../../../base/common/iconLabels.js'; @@ -45,10 +45,10 @@ import { Sequencer, Throttler } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/actions.js'; -import { tail } from '../../../../base/common/arrays.js'; +import { delta, groupBy, tail } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; -import { constObservable, derivedConstOnceDefined, latestChangedValue, observableFromEvent } from '../../../../base/common/observableInternal/utils.js'; +import { constObservable, derivedConstOnceDefined, latestChangedValue, observableFromEvent, runOnChange } from '../../../../base/common/observableInternal/utils.js'; import { ContextKeys } from './scmViewPane.js'; import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IDropdownMenuActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; @@ -60,6 +60,7 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { clamp } from '../../../../base/common/numbers.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { structuralEquals } from '../../../../base/common/equals.js'; +import { compare } from '../../../../base/common/strings.js'; type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; @@ -76,10 +77,31 @@ class SCMRepositoryActionViewItem extends ActionViewItem { } } +class SCMHistoryItemRefsActionViewItem extends ActionViewItem { + constructor(private readonly _historyItemsFilter: HistoryItemRefsFilter, action: IAction, options?: IDropdownMenuActionViewItemOptions) { + super(null, action, { ...options, icon: false, label: true }); + } + + protected override updateLabel(): void { + if (this.options.label && this.label) { + this.label.classList.add('scm-graph-history-item-picker'); + if (this._historyItemsFilter === 'all') { + reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${localize('all', "All")}`)); + } else if (this._historyItemsFilter === 'auto') { + reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${localize('auto', "Auto")}`)); + } else if (this._historyItemsFilter.length === 1) { + reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${this._historyItemsFilter[0].name}`)); + } else { + reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${this._historyItemsFilter.length} ${localize('items', "Items")}`)); + } + } + } +} + registerAction2(class extends ViewAction { constructor() { super({ - id: 'workbench.scm.action.repository', + id: 'workbench.scm.graph.action.pickRepository', title: '', viewId: HISTORY_VIEW_PANE_ID, f1: false, @@ -97,6 +119,28 @@ registerAction2(class extends ViewAction { } }); +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.scm.graph.action.pickHistoryItemRefs', + title: '', + icon: Codicon.gitBranch, + viewId: HISTORY_VIEW_PANE_ID, + f1: false, + menu: { + id: MenuId.SCMHistoryTitle, + group: 'navigation', + order: 0 + } + }); + } + + async runInView(_: ServicesAccessor, view: SCMHistoryViewPane): Promise { + view.pickHistoryItemRef(); + } +}); + + registerAction2(class extends ViewAction { constructor() { super({ @@ -241,36 +285,36 @@ class HistoryItemRenderer implements ITreeRenderer { const labelConfig = this._badgesConfig.read(reader); templateData.labelContainer.textContent = ''; - const firstColoredLabel = historyItem.labels?.find(label => label.color); + const firstColoredRef = historyItem.references?.find(ref => ref.color); - for (const label of historyItem.labels ?? []) { - if (!label.color && labelConfig === 'filter') { + for (const ref of historyItem.references ?? []) { + if (!ref.color && labelConfig === 'filter') { continue; } - if (label.icon && ThemeIcon.isThemeIcon(label.icon)) { + if (ref.icon && ThemeIcon.isThemeIcon(ref.icon)) { const elements = h('div.label', { style: { - color: label.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(foreground), - backgroundColor: label.color ? asCssVariable(label.color) : asCssVariable(historyItemHoverDefaultLabelBackground) + color: ref.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(foreground), + backgroundColor: ref.color ? asCssVariable(ref.color) : asCssVariable(historyItemHoverDefaultLabelBackground) } }, [ h('div.icon@icon'), h('div.description@description') ]); - elements.icon.classList.add(...ThemeIcon.asClassNameArray(label.icon)); + elements.icon.classList.add(...ThemeIcon.asClassNameArray(ref.icon)); - elements.description.textContent = label.title; - elements.description.style.display = label === firstColoredLabel ? '' : 'none'; + elements.description.textContent = ref.name; + elements.description.style.display = ref === firstColoredRef ? '' : 'none'; append(templateData.labelContainer, elements.root); } @@ -320,15 +364,15 @@ class HistoryItemRenderer implements ITreeRenderer 0) { + if ((historyItem.references ?? []).length > 0) { markdown.appendMarkdown(`\n\n---\n\n`); - markdown.appendMarkdown((historyItem.labels ?? []).map(label => { - const labelIconId = ThemeIcon.isThemeIcon(label.icon) ? label.icon.id : ''; + markdown.appendMarkdown((historyItem.references ?? []).map(ref => { + const labelIconId = ThemeIcon.isThemeIcon(ref.icon) ? ref.icon.id : ''; - const labelBackgroundColor = label.color ? asCssVariable(label.color) : asCssVariable(historyItemHoverDefaultLabelBackground); - const labelForegroundColor = label.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(historyItemHoverDefaultLabelForeground); + const labelBackgroundColor = ref.color ? asCssVariable(ref.color) : asCssVariable(historyItemHoverDefaultLabelBackground); + const labelForegroundColor = ref.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(historyItemHoverDefaultLabelForeground); - return ` $(${labelIconId}) ${label.title} `; + return ` $(${labelIconId}) ${ref.name} `; }).join('  ')); } @@ -510,8 +554,6 @@ class SCMHistoryTreeKeyboardNavigationLabelProvider implements IKeyboardNavigati } } -type HistoryItemState = { currentHistoryItemGroup: ISCMHistoryItemGroup; items: ISCMHistoryItem[]; loadMore: boolean }; - class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource { async getChildren(inputOrElement: SCMHistoryViewModel | TreeElement): Promise> { @@ -543,6 +585,9 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource(this, 'auto'); - - readonly historyItemGroupFilter = derived(reader => { - const filter = this._historyItemGroupFilter.read(reader); - if (Array.isArray(filter)) { - return filter; - } - - if (filter === 'all') { - return []; - } - - const repository = this.repository.get(); - const historyProvider = repository?.provider.historyProvider.get(); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get(); - - if (!currentHistoryItemGroup) { - return []; - } - - return [ - currentHistoryItemGroup.revision ?? currentHistoryItemGroup.id, - ...currentHistoryItemGroup.remote ? [currentHistoryItemGroup.remote.revision ?? currentHistoryItemGroup.remote.id] : [], - ...currentHistoryItemGroup.base ? [currentHistoryItemGroup.base.revision ?? currentHistoryItemGroup.base.id] : [], - ]; - }); + readonly historyItemsFilter = observableValue(this, 'auto'); private readonly _state = new Map(); @@ -652,24 +671,44 @@ class SCMHistoryViewModel extends Disposable { let state = this._state.get(repository); const historyProvider = repository.provider.historyProvider.get(); - const currentHistoryItemGroup = state?.currentHistoryItemGroup ?? historyProvider?.currentHistoryItemGroup.get(); - if (!historyProvider || !currentHistoryItemGroup) { + if (!historyProvider) { return []; } if (!state || state.loadMore) { const existingHistoryItems = state?.items ?? []; + let historyItemRefs = state?.historyItemRefs; + + if (!historyItemRefs) { + const historyItemsFilter = this.historyItemsFilter.get(); + + switch (historyItemsFilter) { + case 'all': + historyItemRefs = await historyProvider.provideHistoryItemRefs() ?? []; + break; + case 'auto': + historyItemRefs = [ + historyProvider.currentHistoryItemRef.get(), + historyProvider.currentHistoryItemRemoteRef.get(), + historyProvider.currentHistoryItemBaseRef.get(), + ].filter(ref => !!ref); + break; + default: + historyItemRefs = historyItemsFilter; + break; + } + } - const historyItemGroupIds = this.historyItemGroupFilter.get(); const limit = clamp(this._configurationService.getValue('scm.graph.pageSize'), 1, 1000); + const historyItemGroupIds = historyItemRefs.map(ref => ref.revision ?? ref.id); const historyItems = await historyProvider.provideHistoryItems({ historyItemGroupIds, limit, skip: existingHistoryItems.length }) ?? []; state = { - currentHistoryItemGroup, + historyItemRefs, items: [...existingHistoryItems, ...historyItems], loadMore: false }; @@ -678,7 +717,7 @@ class SCMHistoryViewModel extends Disposable { } // Create the color map - const colorMap = this._getGraphColorMap(currentHistoryItemGroup); + const colorMap = this._getGraphColorMap(state.historyItemRefs); return toISCMHistoryItemViewModelArray(state.items, colorMap) .map(historyItemViewModel => ({ @@ -692,18 +731,34 @@ class SCMHistoryViewModel extends Disposable { this._selectedRepository.set(repository, undefined); } - private _getGraphColorMap(currentHistoryItemGroup: ISCMHistoryItemGroup): Map { - const colorMap = new Map([ - [currentHistoryItemGroup.name, historyItemGroupLocal] - ]); - if (currentHistoryItemGroup.remote) { - colorMap.set(currentHistoryItemGroup.remote.name, historyItemGroupRemote); + setHistoryItemsFilter(filter: 'all' | 'auto' | ISCMHistoryItemRef[]): void { + this.historyItemsFilter.set(filter, undefined); + } + + private _getGraphColorMap(historyItemRefs: ISCMHistoryItemRef[]): Map { + const repository = this.repository.get(); + const historyProvider = repository?.provider.historyProvider.get(); + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get(); + + const colorMap = new Map(); + if (currentHistoryItemGroup) { + colorMap.set(currentHistoryItemGroup.id, historyItemGroupLocal); + if (currentHistoryItemGroup.remote) { + colorMap.set(currentHistoryItemGroup.remote.id, historyItemGroupRemote); + } + if (currentHistoryItemGroup.base) { + colorMap.set(currentHistoryItemGroup.base.id, historyItemGroupBase); + } } - if (currentHistoryItemGroup.base) { - colorMap.set(currentHistoryItemGroup.base.name, historyItemGroupBase); - } - if (this._historyItemGroupFilter.get() === 'all') { - colorMap.set('*', ''); + + // Add the remaining history item references to the color map + // if not already present. These history item references will + // be colored using the color of the history item to which they + // point to. + for (const ref of historyItemRefs) { + if (!colorMap.has(ref.id)) { + colorMap.set(ref.id, undefined); + } } return colorMap; @@ -715,6 +770,166 @@ class SCMHistoryViewModel extends Disposable { } } +type RepositoryQuickPickItem = IQuickPickItem & { repository: 'auto' | ISCMRepository }; + +class RepositoryPicker extends Disposable { + private readonly _autoQuickPickItem: RepositoryQuickPickItem = { + label: localize('auto', "Auto"), + description: localize('activeRepository', "Show the source control graph for the active repository"), + repository: 'auto' + }; + + constructor( + private readonly _scmViewService: ISCMViewService, + private readonly _quickInputService: IQuickInputService, + ) { + super(); + } + + async pickRepository(): Promise { + const picks: (RepositoryQuickPickItem | IQuickPickSeparator)[] = [ + this._autoQuickPickItem, + { type: 'separator' }]; + + picks.push(...this._scmViewService.repositories.map(r => ({ + label: r.provider.name, + description: r.provider.rootUri?.fsPath, + iconClass: ThemeIcon.asClassName(Codicon.repo), + repository: r + }))); + + return this._quickInputService.pick(picks, { + placeHolder: localize('scmGraphRepository', "Select the repository to view, type to filter all repositories") + }); + } +} + +type HistoryItemRefQuickPickItem = IQuickPickItem & { historyItemRef: 'all' | 'auto' | ISCMHistoryItemRef }; + +class HistoryItemRefPicker extends Disposable { + private readonly _allQuickPickItem: HistoryItemRefQuickPickItem = { + id: 'all', + label: localize('all', "All"), + description: localize('allHistoryItemRefs', "Show all history item references"), + historyItemRef: 'all' + }; + + private readonly _autoQuickPickItem: HistoryItemRefQuickPickItem = { + id: 'auto', + label: localize('auto', "Auto"), + description: localize('currentHistoryItemRef', "Show the current history item reference"), + historyItemRef: 'auto' + }; + + constructor( + private readonly _historyProvider: ISCMHistoryProvider, + private readonly _historyItemsFilter: 'all' | 'auto' | ISCMHistoryItemRef[], + @IQuickInputService private readonly _quickInputService: IQuickInputService, + ) { + super(); + } + + async pickHistoryItemRef(): Promise<'all' | 'auto' | ISCMHistoryItemRef[] | undefined> { + const quickPick = this._quickInputService.createQuickPick({ useSeparators: true }); + this._store.add(quickPick); + + quickPick.placeholder = localize('scmGraphHistoryItemRef', "Select one/more history item references to view, type to filter"); + quickPick.canSelectMany = true; + quickPick.hideCheckAll = true; + quickPick.busy = true; + quickPick.show(); + + quickPick.items = await this._createQuickPickItems(); + quickPick.busy = false; + + // Set initial selection + let selectedItems: HistoryItemRefQuickPickItem[] = []; + if (this._historyItemsFilter === 'all') { + selectedItems.push(this._allQuickPickItem); + quickPick.selectedItems = [this._allQuickPickItem]; + } else if (this._historyItemsFilter === 'auto') { + selectedItems.push(this._autoQuickPickItem); + quickPick.selectedItems = [this._autoQuickPickItem]; + } else { + for (const item of quickPick.items) { + if (item.type === 'separator') { + continue; + } + + if (this._historyItemsFilter.some(ref => ref.id === item.id)) { + selectedItems.push(item); + } + } + + quickPick.selectedItems = selectedItems; + } + + return new Promise<'all' | 'auto' | ISCMHistoryItemRef[] | undefined>(resolve => { + this._store.add(quickPick.onDidChangeSelection(items => { + const { added } = delta(selectedItems, items, (a, b) => compare(a.id ?? '', b.id ?? '')); + if (added.length > 0) { + if (added[0].historyItemRef === 'all' || added[0].historyItemRef === 'auto') { + quickPick.selectedItems = [added[0]]; + } else { + // Remove 'all' and 'auto' items if present + quickPick.selectedItems = [...quickPick.selectedItems + .filter(i => i.historyItemRef !== 'all' && i.historyItemRef !== 'auto')]; + } + } + + selectedItems = [...quickPick.selectedItems]; + })); + + this._store.add(quickPick.onDidAccept(() => { + if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'all') { + resolve('all'); + } else if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'auto') { + resolve('auto'); + } else { + resolve(selectedItems.map(item => item.historyItemRef) as ISCMHistoryItemRef[]); + } + + quickPick.hide(); + })); + + this._store.add(quickPick.onDidHide(() => { + resolve(undefined); + this.dispose(); + })); + }); + } + + private async _createQuickPickItems(): Promise<(HistoryItemRefQuickPickItem | IQuickPickSeparator)[]> { + const picks: (HistoryItemRefQuickPickItem | IQuickPickSeparator)[] = [ + this._allQuickPickItem, this._autoQuickPickItem + ]; + + const historyItemRefs = await this._historyProvider.provideHistoryItemRefs() ?? []; + const historyItemRefsByCategory = groupBy(historyItemRefs, (a, b) => compare(a.category ?? '', b.category ?? '')); + + for (const refs of historyItemRefsByCategory) { + if (refs.length === 0) { + continue; + } + + picks.push({ type: 'separator', label: refs[0].category }); + + picks.push(...refs.map(ref => { + return { + id: ref.id, + label: ref.name, + description: ref.description, + iconClass: ThemeIcon.isThemeIcon(ref.icon) ? + ThemeIcon.asClassName(ref.icon) : undefined, + historyItemRef: ref + }; + })); + } + + return picks; + } +} + export class SCMHistoryViewPane extends ViewPane { private _treeContainer!: HTMLElement; @@ -736,9 +951,8 @@ export class SCMHistoryViewPane extends ViewPane { constructor( options: IViewPaneOptions, @ICommandService private readonly _commandService: ICommandService, - @ISCMViewService private readonly _scmViewService: ISCMViewService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IProgressService private readonly _progressService: IProgressService, - @IQuickInputService private readonly _quickInputService: IQuickInputService, @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @@ -871,6 +1085,11 @@ export class SCMHistoryViewPane extends ViewPane { } })); + // HistoryItemRefs filter changed + store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => { + this.refresh(); + })); + if (changeSummary.refresh) { this.refresh(); } @@ -891,11 +1110,16 @@ export class SCMHistoryViewPane extends ViewPane { } override getActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined { - if (action.id === 'workbench.scm.action.repository') { + if (action.id === 'workbench.scm.graph.action.pickRepository') { const repository = this._treeViewModel?.repository.get(); if (repository) { return new SCMRepositoryActionViewItem(repository, action, options); } + } else if (action.id === 'workbench.scm.graph.action.pickHistoryItemRefs') { + const historyItemsFilter = this._treeViewModel?.historyItemsFilter.get(); + if (historyItemsFilter) { + return new SCMHistoryItemRefsActionViewItem(historyItemsFilter, action, options); + } } return super.getActionViewItem(action, options); @@ -910,33 +1134,31 @@ export class SCMHistoryViewPane extends ViewPane { } async pickRepository(): Promise { - const picks: (IQuickPickItem & { repository: 'auto' | ISCMRepository } | IQuickPickSeparator)[] = [ - { - label: localize('auto', "Auto"), - description: localize('activeRepository', "Show the source control graph for the active repository"), - repository: 'auto' - }, - { - type: 'separator' - }, - ]; - - picks.push(...this._scmViewService.repositories.map(r => ({ - label: r.provider.name, - description: r.provider.rootUri?.fsPath, - iconClass: ThemeIcon.asClassName(Codicon.repo), - repository: r - }))); - - const result = await this._quickInputService.pick(picks, { - placeHolder: localize('scmGraphRepository', "Select the repository to view, type to filter all repositories") - }); + const picker = this._instantiationService.createInstance(RepositoryPicker); + const result = await picker.pickRepository(); if (result) { this._treeViewModel.setRepository(result.repository); } } + async pickHistoryItemRef(): Promise { + const repository = this._treeViewModel.repository.get(); + const historyProvider = repository?.provider.historyProvider.get(); + const historyItemsFilter = this._treeViewModel.historyItemsFilter.get(); + + if (!historyProvider) { + return; + } + + const picker = this._instantiationService.createInstance(HistoryItemRefPicker, historyProvider, historyItemsFilter); + const result = await picker.pickHistoryItemRef(); + + if (result) { + this._treeViewModel.setHistoryItemsFilter(result); + } + } + private _createTree(container: HTMLElement): void { this._treeIdentityProvider = new SCMHistoryTreeIdentityProvider(); diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index f5d4de3fa52..dd61d7362e4 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -17,6 +17,11 @@ export interface ISCMHistoryProviderMenus { export interface ISCMHistoryProvider { readonly currentHistoryItemGroup: IObservable; + readonly currentHistoryItemRef: IObservable; + readonly currentHistoryItemRemoteRef: IObservable; + readonly currentHistoryItemBaseRef: IObservable; + + provideHistoryItemRefs(): Promise; provideHistoryItems(options: ISCMHistoryOptions): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise; @@ -43,10 +48,14 @@ export interface ISCMHistoryItemStatistics { readonly deletions: number; } -export interface ISCMHistoryItemLabel { - readonly title: string; - readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; +export interface ISCMHistoryItemRef { + readonly id: string; + readonly name: string; + readonly revision?: string; + readonly category?: string; + readonly description?: string; readonly color?: ColorIdentifier; + readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; } export interface ISCMHistoryItem { @@ -58,7 +67,7 @@ export interface ISCMHistoryItem { readonly author?: string; readonly timestamp?: number; readonly statistics?: ISCMHistoryItemStatistics; - readonly labels?: ISCMHistoryItemLabel[]; + readonly references?: ISCMHistoryItemRef[]; } export interface ISCMHistoryItemGraphNode { diff --git a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts index 9da7394ecd7..0cb64cfdda0 100644 --- a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -7,10 +7,10 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ColorIdentifier } from '../../../../../platform/theme/common/colorUtils.js'; import { colorRegistry, historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote, toISCMHistoryItemViewModelArray } from '../../browser/scmHistory.js'; -import { ISCMHistoryItem, ISCMHistoryItemLabel } from '../../common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemRef } from '../../common/history.js'; -function toSCMHistoryItem(id: string, parentIds: string[], labels?: ISCMHistoryItemLabel[]): ISCMHistoryItem { - return { id, parentIds, subject: '', message: '', labels } satisfies ISCMHistoryItem; +function toSCMHistoryItem(id: string, parentIds: string[], references?: ISCMHistoryItemRef[]): ISCMHistoryItem { + return { id, parentIds, subject: '', message: '', references } satisfies ISCMHistoryItem; } suite('toISCMHistoryItemViewModelArray', () => { @@ -517,12 +517,12 @@ suite('toISCMHistoryItemViewModelArray', () => { */ test('graph with color map', () => { const models = [ - toSCMHistoryItem('a', ['b'], [{ title: 'topic' }]), + toSCMHistoryItem('a', ['b'], [{ id: 'topic', name: 'topic' }]), toSCMHistoryItem('b', ['c']), - toSCMHistoryItem('c', ['d'], [{ title: 'origin/topic' }]), + toSCMHistoryItem('c', ['d'], [{ id: 'origin/topic', name: 'origin/topic' }]), toSCMHistoryItem('d', ['e']), toSCMHistoryItem('e', ['f', 'g']), - toSCMHistoryItem('g', ['h'], [{ title: 'origin/main' }]) + toSCMHistoryItem('g', ['h'], [{ id: 'origin/main', name: 'origin/main' }]) ]; const colorMap = new Map([ diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 39ee5f8281e..fcd0286936f 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -20,10 +20,11 @@ declare module 'vscode' { onDidChangeCurrentHistoryItemGroup: Event; /** - * Fires when the history item groups change (ex: commit, push, fetch) + * Fires when history item refs change */ - // onDidChangeHistoryItemGroups: Event; + onDidChangeHistory: Event; + provideHistoryItemRefs(token: CancellationToken): ProviderResult; provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; @@ -51,11 +52,6 @@ declare module 'vscode' { readonly deletions: number; } - export interface SourceControlHistoryItemLabel { - readonly title: string; - readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; - } - export interface SourceControlHistoryItem { readonly id: string; readonly parentIds: string[]; @@ -64,7 +60,16 @@ declare module 'vscode' { readonly author?: string; readonly timestamp?: number; readonly statistics?: SourceControlHistoryItemStatistics; - readonly labels?: SourceControlHistoryItemLabel[]; + readonly references?: SourceControlHistoryItemRef[]; + } + + export interface SourceControlHistoryItemRef { + readonly id: string; + readonly name: string; + readonly description?: string; + readonly revision?: string; + readonly category?: string; + readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; } export interface SourceControlHistoryItemChange { @@ -74,10 +79,9 @@ declare module 'vscode' { readonly renameUri: Uri | undefined; } - // export interface SourceControlHistoryChangeEvent { - // readonly added: Iterable; - // readonly removed: Iterable; - // readonly modified: Iterable; - // } - + export interface SourceControlHistoryChangeEvent { + readonly added: Iterable; + readonly removed: Iterable; + readonly modified: Iterable; + } } From 6abee1b852670b0efec8d8dbda89c0470211a6a4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 9 Sep 2024 12:19:30 +0200 Subject: [PATCH 178/286] esm - restore ability to `require.resolve` with `paths` (#227934) * esm - restore ability to `require.resolve` with `paths` * fill in paths --- src/vs/workbench/api/node/extHostExtensionService.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 98c7296433b..af286171bce 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -46,6 +46,18 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { return originalLookup.call(this, applyAlternatives(request), parent); }; + const originalResolveFilename = node_module._resolveFilename; + node_module._resolveFilename = function resolveFilename(request: string, parent: unknown, isMain: boolean, options?: { paths?: string[] }) { + if (request === 'vsda' && Array.isArray(options?.paths) && options.paths.length === 0) { + // ESM: ever since we moved to ESM, `require.main` will be `undefined` for extensions + // Some extensions have been using `require.resolve('vsda', { paths: require.main.paths })` + // to find the `vsda` module in our app root. To be backwards compatible with this pattern, + // we help by filling in the `paths` array with the node modules paths of the current module. + options.paths = node_module._nodeModulePaths(import.meta.dirname); + } + return originalResolveFilename.call(this, request, parent, isMain, options); + }; + const applyAlternatives = (request: string) => { for (const alternativeModuleName of that._alternatives) { const alternative = alternativeModuleName(request); From 9b4a6d5e5ec9e858e3e8b791457d20c13c426f1d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Mon, 9 Sep 2024 12:32:18 +0200 Subject: [PATCH 179/286] fix #227778 (#227958) --- .../contrib/debug/browser/debugCommands.ts | 8 +-- .../browser/editSessions.contribution.ts | 8 +-- .../browser/deprecatedExtensionsChecker.ts | 10 +--- .../extensions/browser/extensionEditor.ts | 18 ++---- ...ensionRecommendationNotificationService.ts | 20 +------ .../browser/extensions.contribution.ts | 52 +++++++--------- .../extensions/browser/extensionsActions.ts | 60 +++---------------- .../browser/extensionsQuickAccess.ts | 25 ++------ .../browser/extensionsWorkbenchService.ts | 12 +++- .../contrib/extensions/common/extensions.ts | 1 + .../format/browser/formatActionsNone.ts | 15 +---- .../localization.contribution.ts | 17 +----- .../browser/view/cellParts/cellOutput.ts | 10 +--- .../notebookKernelQuickPickStrategy.ts | 16 +---- .../contrib/remote/browser/remoteIndicator.ts | 13 +--- .../testing/browser/testExplorerActions.ts | 9 +-- .../themes/browser/themes.contribution.ts | 28 +++------ 17 files changed, 86 insertions(+), 236 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 33f2b8f83c3..096a909b74a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -10,7 +10,7 @@ import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keyb import { IListService } from '../../../../platform/list/browser/listService.js'; import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, State, getStateLabel, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, VIEWLET_ID, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_REPL, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, isFrameDeemphasized } from '../common/debug.js'; import { Expression, Variable, Breakpoint, FunctionBreakpoint, DataBreakpoint, Thread } from '../common/debugModel.js'; -import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { MenuRegistry, MenuId, Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -937,14 +937,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: undefined, primary: undefined, handler: async (accessor, query: string) => { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const viewlet = (await paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); let searchFor = `@category:debuggers`; if (typeof query === 'string') { searchFor += ` ${query}`; } - viewlet.search(searchFor); - viewlet.focus(); + return extensionsWorkbenchService.openSearch(searchFor); } }); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 4dc0f23670a..fba1ac91de1 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -61,8 +61,7 @@ import { ILocalizedString } from '../../../../platform/action/common/action.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; -import { IExtensionsViewPaneContainer, VIEWLET_ID } from '../../extensions/common/extensions.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { WorkspaceStateSynchroniser } from '../common/workspaceStateSync.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IRequestService } from '../../../../platform/request/common/request.js'; @@ -101,10 +100,7 @@ registerAction2(class extends Action2 { } async run(accessor: ServicesAccessor): Promise { - const paneCompositePartService = accessor.get(IPaneCompositePartService); - const viewlet = await paneCompositePartService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; - view?.search('@tag:continueOn'); + return accessor.get(IExtensionsWorkbenchService).openSearch('@tag:continueOn'); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts b/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts index e6d5b02c2da..bf3d9421e15 100644 --- a/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts +++ b/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts @@ -8,8 +8,6 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { localize } from '../../../../nls.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { SearchExtensionsAction } from './extensionsActions.js'; import { distinct } from '../../../../base/common/arrays.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -24,7 +22,6 @@ export class DeprecatedExtensionsChecker extends Disposable implements IWorkbenc @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, - @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.checkForDeprecatedExtensions(); @@ -56,12 +53,7 @@ export class DeprecatedExtensionsChecker extends Disposable implements IWorkbenc label: localize('showDeprecated', "Show Deprecated Extensions"), run: async () => { this.setNotifiedDeprecatedExtensions(toNotify.map(e => e.identifier.id.toLowerCase())); - const action = this.instantiationService.createInstance(SearchExtensionsAction, toNotify.map(extension => `@id:${extension.identifier.id}`).join(' ')); - try { - await action.run(); - } finally { - action.dispose(); - } + await this.extensionsWorkbenchService.openSearch(toNotify.map(extension => `@id:${extension.identifier.id}`).join(' ')); } }, { label: localize('neverShowAgain', "Don't Show Again"), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 0aa9ed34913..a80cf76fe05 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -46,7 +46,6 @@ import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticip import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; import { IEditorOpenContext } from '../../../common/editor.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ExtensionFeaturesTab } from './extensionFeaturesTab.js'; import { ButtonWithDropDownExtensionAction, @@ -76,7 +75,7 @@ import { import { Delegate } from './extensionsList.js'; import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from './extensionsViewer.js'; import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, VerifiedPublisherWidget, onClick } from './extensionsWidgets.js'; -import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from '../common/extensions.js'; +import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsWorkbenchService } from '../common/extensions.js'; import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js'; import { IExplorerService } from '../../files/browser/files.js'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js'; @@ -85,7 +84,6 @@ import { IEditorGroup } from '../../../services/editor/common/editorGroupsServic import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -240,7 +238,6 @@ export class ExtensionEditor extends EditorPane { group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IThemeService themeService: IThemeService, @@ -586,11 +583,7 @@ export class ExtensionEditor extends EditorPane { if (extension.url) { this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(extension.url!)))); this.transientDisposables.add(onClick(template.rating, () => this.openerService.open(URI.parse(`${extension.url}&ssr=false#review-details`)))); - this.transientDisposables.add(onClick(template.publisher, () => { - this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => viewlet.search(`publisher:"${extension.publisherDisplayName}"`)); - })); + this.transientDisposables.add(onClick(template.publisher, () => this.extensionsWorkbenchService.openSearch(`publisher:"${extension.publisherDisplayName}"`))); } const manifest = await this.extensionManifest.get().promise; @@ -944,11 +937,8 @@ export class ExtensionEditor extends EditorPane { append(categoriesContainer, $('.additional-details-title', undefined, localize('categories', "Categories"))); const categoriesElement = append(categoriesContainer, $('.categories')); for (const category of extension.categories) { - this.transientDisposables.add(onClick(append(categoriesElement, $('span.category', { tabindex: '0' }, category)), () => { - this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => viewlet.search(`@category:"${category}"`)); - })); + this.transientDisposables.add(onClick(append(categoriesElement, $('span.category', { tabindex: '0' }, category)), + () => this.extensionsWorkbenchService.openSearch(`@category:"${category}"`))); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 45ef74f3bd5..1fc880f2465 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction } from '../../../../base/common/actions.js'; import { distinct } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, Promises, raceCancellablePromises, raceCancellation, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, isDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; @@ -17,13 +16,11 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; -import { SearchExtensionsAction } from './extensionsActions.js'; import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; @@ -138,7 +135,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @@ -282,7 +278,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple let accepted = false; const choices: (IPromptChoice | IPromptChoiceWithMenu)[] = []; const installExtensions = async (isMachineScoped: boolean) => { - this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); + this.extensionsWorkbenchService.openSearch(searchValue); onDidInstallRecommendedExtensions(extensions); const galleryExtensions: IGalleryExtension[] = [], resourceExtensions: IExtension[] = []; for (const extension of extensions) { @@ -313,7 +309,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple for (const extension of extensions) { this.extensionsWorkbenchService.open(extension, { pinned: true }); } - this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); + this.extensionsWorkbenchService.openSearch(searchValue); } }, { label: donotShowAgainLabel, @@ -464,16 +460,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple return result; } - private async runAction(action: IAction): Promise { - try { - await action.run(); - } finally { - if (isDisposable(action)) { - action.dispose(); - } - } - } - private addToImportantRecommendationsIgnore(id: string) { const importantRecommendationsIgnoreList = [...this.ignoredRecommendations]; if (!importantRecommendationsIgnoreList.includes(id.toLowerCase())) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index b5dc1e178fc..6e7306a6186 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -14,7 +14,7 @@ import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsServi import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType } from '../common/extensions.js'; -import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; import { ExtensionsInput } from '../common/extensionsInput.js'; import { ExtensionEditor } from './extensionEditor.js'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from './extensionsViewlet.js'; @@ -68,7 +68,6 @@ import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from '../../../services/workspaces/ import { ExtensionsCompletionItemsProvider } from './extensionsCompletionItemsProvider.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Event } from '../../../../base/common/event.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { UnsupportedExtensionsMigrationContrib } from './unsupportedExtensionsMigrationContribution.js'; import { isWeb } from '../../../../base/common/platform.js'; import { ExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js'; @@ -433,15 +432,7 @@ CommandsRegistry.registerCommand({ ] }, handler: async (accessor, query: string = '') => { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const viewlet = await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - - if (!viewlet) { - return; - } - - (viewlet.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); - viewlet.focus(); + return accessor.get(IExtensionsWorkbenchService).openSearch(query); } }); @@ -489,7 +480,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IContextKeyService contextKeyService: IContextKeyService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -561,7 +552,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi category: ExtensionsLocalizedLabel, f1: true, run: async (accessor: ServicesAccessor) => { - await accessor.get(IPaneCompositePartService).openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); + await accessor.get(IExtensionsWorkbenchService).openSearch(''); } }); @@ -593,7 +584,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [MenuId.EditorTitle.id]: localize('importKeyboardShortcutsFroms', "Migrate Keyboard Shortcuts from...") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended:keymaps ')) + run: () => this.extensionsWorkbenchService.openSearch('@recommended:keymaps ') }); this.registerExtensionAction({ @@ -604,7 +595,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.CommandPalette, when: CONTEXT_HAS_GALLERY }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended:languages ')) + run: () => this.extensionsWorkbenchService.openSearch('@recommended:languages ') }); this.registerExtensionAction({ @@ -624,7 +615,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi await this.extensionsWorkbenchService.checkForUpdates(); const outdated = this.extensionsWorkbenchService.outdated; if (outdated.length) { - return runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@outdated ')); + return this.extensionsWorkbenchService.openSearch('@outdated '); } else { return this.dialogService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); } @@ -937,7 +928,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('featured filter', "Featured") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@featured ')) + run: () => this.extensionsWorkbenchService.openSearch('@featured ') }); this.registerExtensionAction({ @@ -956,7 +947,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('most popular filter', "Most Popular") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@popular ')) + run: () => this.extensionsWorkbenchService.openSearch('@popular ') }); this.registerExtensionAction({ @@ -975,7 +966,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('most popular recommended', "Recommended") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended ')) + run: () => this.extensionsWorkbenchService.openSearch('@recommended ') }); this.registerExtensionAction({ @@ -994,7 +985,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('recently published filter', "Recently Published") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recentlyPublished ')) + run: () => this.extensionsWorkbenchService.openSearch('@recentlyPublished ') }); const extensionsCategoryFilterSubMenu = new MenuId('extensionsCategoryFilterSubMenu'); @@ -1015,7 +1006,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi when: CONTEXT_HAS_GALLERY, order: index, }], - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, `@category:"${category.toLowerCase()}"`)) + run: () => this.extensionsWorkbenchService.openSearch(`@category:"${category.toLowerCase()}"`) }); }); @@ -1034,7 +1025,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('builtin filter', "Built-in") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@builtin ')) + run: () => this.extensionsWorkbenchService.openSearch('@builtin ') }); this.registerExtensionAction({ @@ -1052,7 +1043,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('extension updates filter', "Updates") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@updates')) + run: () => this.extensionsWorkbenchService.openSearch('@updates') }); this.registerExtensionAction({ @@ -1071,7 +1062,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('workspace unsupported filter', "Workspace Unsupported") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@workspaceUnsupported')) + run: () => this.extensionsWorkbenchService.openSearch('@workspaceUnsupported') }); this.registerExtensionAction({ @@ -1089,7 +1080,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('enabled filter', "Enabled") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@enabled ')) + run: () => this.extensionsWorkbenchService.openSearch('@enabled ') }); this.registerExtensionAction({ @@ -1107,7 +1098,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('disabled filter', "Disabled") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@disabled ')) + run: () => this.extensionsWorkbenchService.openSearch('@disabled ') }); const extensionsSortSubMenu = new MenuId('extensionsSortSubMenu'); @@ -1137,11 +1128,10 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }], toggled: ExtensionsSortByContext.isEqualTo(id), run: async () => { - const viewlet = await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const extensionsViewPaneContainer = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer; - const currentQuery = Query.parse(extensionsViewPaneContainer.searchValue || ''); - extensionsViewPaneContainer.search(new Query(currentQuery.value, id).toString()); - extensionsViewPaneContainer.focus(); + const extensionsViewPaneContainer = ((await this.viewsService.openViewContainer(VIEWLET_ID, true))?.getViewPaneContainer()) as IExtensionsViewPaneContainer | undefined; + const currentQuery = Query.parse(extensionsViewPaneContainer?.searchValue ?? ''); + extensionsViewPaneContainer?.search(new Query(currentQuery.value, id).toString()); + extensionsViewPaneContainer?.focus(); } }); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index a263b5ba6f1..974c01deb12 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import * as json from '../../../../base/common/json.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { disposeIfDisposable } from '../../../../base/common/lifecycle.js'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js'; import { ExtensionsConfigurationInitialContent } from '../common/extensionsFileTemplate.js'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; @@ -60,8 +60,6 @@ import { IExtensionManifestPropertiesService } from '../../../services/extension import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { isVirtualWorkspace } from '../../../../platform/workspace/common/virtualWorkspace.js'; import { escapeMarkdownSyntaxTokens, IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { fromNow } from '../../../../base/common/date.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { getLocale } from '../../../../platform/languagePacks/common/languagePacks.js'; @@ -2015,7 +2013,6 @@ export class ShowRecommendedExtensionAction extends Action { constructor( extensionId: string, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { super(ShowRecommendedExtensionAction.ID, ShowRecommendedExtensionAction.LABEL, undefined, false); @@ -2023,10 +2020,7 @@ export class ShowRecommendedExtensionAction extends Action { } override async run(): Promise { - const paneComposite = await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const paneContainer = paneComposite?.getViewPaneContainer() as IExtensionsViewPaneContainer; - paneContainer.search(`@id:${this.extensionId}`); - paneContainer.focus(); + await this.extensionWorkbenchService.openSearch(`@id:${this.extensionId}`); const [extension] = await this.extensionWorkbenchService.getExtensions([{ id: this.extensionId }], { source: 'install-recommendation' }, CancellationToken.None); if (extension) { return this.extensionWorkbenchService.open(extension); @@ -2044,7 +2038,6 @@ export class InstallRecommendedExtensionAction extends Action { constructor( extensionId: string, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { @@ -2053,10 +2046,7 @@ export class InstallRecommendedExtensionAction extends Action { } override async run(): Promise { - const viewlet = await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const viewPaneContainer = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer; - viewPaneContainer.search(`@id:${this.extensionId}`); - viewPaneContainer.focus(); + await this.extensionWorkbenchService.openSearch(`@id:${this.extensionId}`); const [extension] = await this.extensionWorkbenchService.getExtensions([{ id: this.extensionId }], { source: 'install-recommendation' }, CancellationToken.None); if (extension) { await this.extensionWorkbenchService.open(extension); @@ -2115,22 +2105,6 @@ export class UndoIgnoreExtensionRecommendationAction extends Action { } } -export class SearchExtensionsAction extends Action { - - constructor( - private readonly searchValue: string, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService - ) { - super('extensions.searchExtensions', localize('search recommendations', "Search Extensions"), undefined, true); - } - - override async run(): Promise { - const viewPaneContainer = (await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; - viewPaneContainer.search(this.searchValue); - viewPaneContainer.focus(); - } -} - export abstract class AbstractConfigureRecommendedExtensionsAction extends Action { constructor( @@ -2786,7 +2760,6 @@ export class ReinstallAction extends Action { @IQuickInputService private readonly quickInputService: IQuickInputService, @INotificationService private readonly notificationService: INotificationService, @IHostService private readonly hostService: IHostService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionService private readonly extensionService: IExtensionService ) { super(id, label); @@ -2819,7 +2792,7 @@ export class ReinstallAction extends Action { } private reinstallExtension(extension: IExtension): Promise { - return this.instantiationService.createInstance(SearchExtensionsAction, '@installed ').run() + return this.extensionsWorkbenchService.openSearch('@installed ') .then(() => { return this.extensionsWorkbenchService.reinstall(extension) .then(extension => { @@ -2865,7 +2838,7 @@ export class InstallSpecificVersionOfExtensionAction extends Action { if (extensionPick && extensionPick.extension) { const action = this.instantiationService.createInstance(InstallAnotherVersionAction, extensionPick.extension, true); await action.run(); - await this.instantiationService.createInstance(SearchExtensionsAction, extensionPick.extension.identifier.id).run(); + await this.extensionsWorkbenchService.openSearch(extensionPick.extension.identifier.id); } } @@ -3107,29 +3080,14 @@ export class InstallRemoteExtensionsInLocalAction extends AbstractInstallExtensi } CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForLanguage', function (accessor: ServicesAccessor, fileExtension: string) { - const paneCompositeService = accessor.get(IPaneCompositePartService); - - return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(`ext:${fileExtension.replace(/^\./, '')}`); - viewlet.focus(); - }); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + return extensionsWorkbenchService.openSearch(`ext:${fileExtension.replace(/^\./, '')}`); }); export const showExtensionsWithIdsCommandId = 'workbench.extensions.action.showExtensionsWithIds'; CommandsRegistry.registerCommand(showExtensionsWithIdsCommandId, function (accessor: ServicesAccessor, extensionIds: string[]) { - const paneCompositeService = accessor.get(IPaneCompositePartService); - - return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - const query = extensionIds - .map(id => `@id:${id}`) - .join(' '); - viewlet.search(query); - viewlet.focus(); - }); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + return extensionsWorkbenchService.openSearch(extensionIds.map(id => `@id:${id}`).join(' ')); }); registerColor('extensionButton.background', { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts index 23eb15336fe..05166b41268 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts @@ -7,20 +7,18 @@ import { IQuickPickSeparator } from '../../../../platform/quickinput/common/quic import { IPickerQuickAccessItem, PickerQuickAccessProvider } from '../../../../platform/quickinput/browser/pickerQuickAccess.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { localize } from '../../../../nls.js'; -import { VIEWLET_ID, IExtensionsViewPaneContainer } from '../common/extensions.js'; import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; +import { IExtensionsWorkbenchService } from '../common/extensions.js'; export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvider { static PREFIX = 'ext install '; constructor( - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IExtensionManagementService private readonly extensionsService: IExtensionManagementService, @INotificationService private readonly notificationService: INotificationService, @@ -40,7 +38,7 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid const genericSearchPickItem: IPickerQuickAccessItem = { label: localize('searchFor', "Press Enter to search for extension '{0}'.", filter), - accept: () => this.searchExtension(filter) + accept: () => this.extensionsWorkbenchService.openSearch(filter) }; // Extension ID typed: try to find it @@ -80,37 +78,26 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid private async installExtension(extension: IGalleryExtension, name: string): Promise { try { - await openExtensionsViewlet(this.paneCompositeService, `@id:${name}`); + await this.extensionsWorkbenchService.openSearch(`@id:${name}`); await this.extensionsService.installFromGallery(extension); } catch (error) { this.notificationService.error(error); } } - - private async searchExtension(name: string): Promise { - openExtensionsViewlet(this.paneCompositeService, name); - } } export class ManageExtensionsQuickAccessProvider extends PickerQuickAccessProvider { static PREFIX = 'ext '; - constructor(@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService) { + constructor(@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService) { super(ManageExtensionsQuickAccessProvider.PREFIX); } protected _getPicks(): Array { return [{ label: localize('manage', "Press Enter to manage your extensions."), - accept: () => openExtensionsViewlet(this.paneCompositeService) + accept: () => this.extensionsWorkbenchService.openSearch('') }]; } } - -async function openExtensionsViewlet(paneCompositeService: IPaneCompositePartService, search = ''): Promise { - const viewlet = await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; - view?.search(search); - view?.focus(); -} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 08a136c6a60..d3deb35f309 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -25,7 +25,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { URI } from '../../../../base/common/uri.js'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey } from '../common/extensions.js'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey, VIEWLET_ID, IExtensionsViewPaneContainer } from '../common/extensions.js'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js'; import { IURLService, IURLHandler, IOpenURLOptions } from '../../../../platform/url/common/url.js'; import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js'; @@ -61,6 +61,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; interface IExtensionStateProvider { (extension: Extension): T; @@ -947,6 +948,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IUpdateService private readonly updateService: IUpdateService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IViewsService private readonly viewsService: IViewsService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -1389,6 +1391,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), options, options?.sideByside ? SIDE_GROUP : ACTIVE_GROUP); } + async openSearch(searchValue: string, preserveFoucs?: boolean): Promise { + const viewPaneContainer = (await this.viewsService.openViewContainer(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + viewPaneContainer.search(searchValue); + if (!preserveFoucs) { + viewPaneContainer.focus(); + } + } + getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined { const extensionsStatus = this.extensionService.getExtensionsStatus(); for (const id of Object.keys(extensionsStatus)) { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 0d0ce95fbe3..4c5e1efa44c 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -141,6 +141,7 @@ export interface IExtensionsWorkbenchService { shouldRequireConsentToUpdate(extension: IExtension): Promise; updateAutoUpdateForAllExtensions(value: boolean): Promise; open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise; + openSearch(searchValue: string, focus?: boolean): Promise; getAutoUpdateValue(): AutoUpdateConfigurationValue; checkForUpdates(): Promise; getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; diff --git a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts index 8154ea44b41..a01e47e17c0 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts @@ -12,19 +12,10 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { VIEWLET_ID, IExtensionsViewPaneContainer } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -async function showExtensionQuery(paneCompositeService: IPaneCompositePartService, query: string) { - const viewlet = await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - if (viewlet) { - (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); - } -} - registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { constructor() { @@ -48,7 +39,7 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { } const commandService = accessor.get(ICommandService); - const paneCompositeService = accessor.get(IPaneCompositePartService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); const notificationService = accessor.get(INotificationService); const dialogService = accessor.get(IDialogService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); @@ -70,7 +61,7 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { primaryButton: nls.localize({ key: 'install.formatter', comment: ['&& denotes a mnemonic'] }, "&&Install Formatter...") }); if (confirmed) { - showExtensionQuery(paneCompositeService, `category:formatters ${langName}`); + extensionsWorkbenchService.openSearch(`category:formatters ${langName}`); } } } diff --git a/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts index 39795a322bf..9dfa4afa29a 100644 --- a/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts +++ b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts @@ -12,12 +12,10 @@ import { IExtensionManagementService, IExtensionGalleryService, InstallOperation import { INotificationService, NeverShowAgainScope } from '../../../../platform/notification/common/notification.js'; import Severity from '../../../../base/common/severity.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { VIEWLET_ID as EXTENSIONS_VIEWLET_ID, IExtensionsViewPaneContainer } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { minimumTranslatedStrings } from './minimalTranslations.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ILocaleService } from '../../../services/localization/common/locale.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { BaseLocalizationWorkbenchContribution } from '../common/localization.contribution.js'; @@ -32,7 +30,7 @@ class NativeLocalizationWorkbenchContribution extends BaseLocalizationWorkbenchC @IStorageService private readonly storageService: IStorageService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); @@ -168,16 +166,7 @@ class NativeLocalizationWorkbenchContribution extends BaseLocalizationWorkbenchC label: translations['searchMarketplace'], run: async () => { logUserReaction('search'); - const viewlet = await this.paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true); - if (!viewlet) { - return; - } - const container = viewlet.getViewPaneContainer(); - if (!container) { - return; - } - (container as IExtensionsViewPaneContainer).search(`tag:lp-${locale}`); - container.focus(); + await this.extensionsWorkbenchService.openSearch(`tag:lp-${locale}`); } }; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 735bdad7d94..e561cf6e626 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -19,8 +19,7 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IQuickInputService, IQuickPickItem } from '../../../../../../platform/quickinput/common/quickInput.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; -import { ViewContainerLocation } from '../../../../../common/views.js'; -import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSION_VIEWLET_ID } from '../../../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../../../extensions/common/extensions.js'; import { ICellOutputViewModel, ICellViewModel, IInsetRenderOutput, INotebookEditorDelegate, JUPYTER_EXTENSION_ID, RenderOutputType } from '../../notebookBrowser.js'; import { mimetypeIcon } from '../../notebookIcons.js'; import { CellContentPart } from '../cellPart.js'; @@ -31,7 +30,6 @@ import { CellUri, IOrderedMimeType, NotebookCellExecutionState, NotebookCellOutp import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; import { INotebookKernel } from '../../../common/notebookKernelService.js'; import { INotebookService } from '../../../common/notebookService.js'; -import { IPaneCompositePartService } from '../../../../../services/panecomposite/browser/panecomposite.js'; import { COPY_OUTPUT_COMMAND_ID } from '../../controller/cellOutputActions.js'; import { TEXT_BASED_MIMETYPES } from '../../contrib/clipboard/cellOutputClipboard.js'; import { autorun, observableValue } from '../../../../../../base/common/observable.js'; @@ -80,7 +78,7 @@ class CellOutputElement extends Disposable { @IQuickInputService private readonly quickInputService: IQuickInputService, @IContextKeyService parentContextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -430,9 +428,7 @@ class CellOutputElement extends Disposable { } private async _showJupyterExtension() { - const viewlet = await this.paneCompositeService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; - view?.search(`@id:${JUPYTER_EXTENSION_ID}`); + await this.extensionsWorkbenchService.openSearch(`@id:${JUPYTER_EXTENSION_ID}`); } private _generateRendererInfo(renderId: string): string { diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 9acc12688d7..f14a7619562 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -22,15 +22,13 @@ import { IProductService } from '../../../../../platform/product/common/productS import { ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, QuickPickInput } from '../../../../../platform/quickinput/common/quickInput.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { ViewContainerLocation } from '../../../../common/views.js'; -import { IExtension, IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID as EXTENSION_VIEWLET_ID } from '../../../extensions/common/extensions.js'; +import { IExtension, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { IActiveNotebookEditor, INotebookExtensionRecommendation, JUPYTER_EXTENSION_ID, KERNEL_RECOMMENDATIONS } from '../notebookBrowser.js'; import { NotebookEditorWidget } from '../notebookEditorWidget.js'; import { executingStateIcon, selectKernelIcon } from '../notebookIcons.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; import { INotebookKernel, INotebookKernelHistoryService, INotebookKernelMatchResult, INotebookKernelService, ISourceAction } from '../../common/notebookKernelService.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; -import { IPaneCompositePartService } from '../../../../services/panecomposite/browser/panecomposite.js'; import { URI } from '../../../../../base/common/uri.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { INotebookTextModel } from '../../common/notebookCommon.js'; @@ -105,7 +103,6 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { protected readonly _quickInputService: IQuickInputService, protected readonly _labelService: ILabelService, protected readonly _logService: ILogService, - protected readonly _paneCompositePartService: IPaneCompositePartService, protected readonly _extensionWorkbenchService: IExtensionsWorkbenchService, protected readonly _extensionService: IExtensionService, protected readonly _commandService: ICommandService @@ -255,7 +252,6 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { // actions if (isSearchMarketplacePick(pick)) { await this._showKernelExtension( - this._paneCompositePartService, this._extensionWorkbenchService, this._extensionService, editor.textModel.viewType, @@ -264,7 +260,6 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { // suggestedExtension must be defined for this option to be shown, but still check to make TS happy } else if (isInstallExtensionPick(pick)) { await this._showKernelExtension( - this._paneCompositePartService, this._extensionWorkbenchService, this._extensionService, editor.textModel.viewType, @@ -284,7 +279,6 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { } protected async _showKernelExtension( - paneCompositePartService: IPaneCompositePartService, extensionWorkbenchService: IExtensionsWorkbenchService, extensionService: IExtensionService, viewType: string, @@ -337,10 +331,8 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { return; } - const viewlet = await paneCompositePartService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join(''); - view?.search(`@tag:notebookKernel${pascalCased}`); + await extensionWorkbenchService.openSearch(`@tag:notebookKernel${pascalCased}`); } private async _showInstallKernelExtensionRecommendation( @@ -441,7 +433,6 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { @IQuickInputService _quickInputService: IQuickInputService, @ILabelService _labelService: ILabelService, @ILogService _logService: ILogService, - @IPaneCompositePartService _paneCompositePartService: IPaneCompositePartService, @IExtensionsWorkbenchService _extensionWorkbenchService: IExtensionsWorkbenchService, @IExtensionService _extensionService: IExtensionService, @ICommandService _commandService: ICommandService, @@ -455,7 +446,6 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { _quickInputService, _labelService, _logService, - _paneCompositePartService, _extensionWorkbenchService, _extensionService, _commandService, @@ -617,7 +607,6 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { } } else if (isSearchMarketplacePick(selectedKernelPickItem)) { await this._showKernelExtension( - this._paneCompositePartService, this._extensionWorkbenchService, this._extensionService, editor.textModel.viewType, @@ -626,7 +615,6 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { return true; } else if (isInstallExtensionPick(selectedKernelPickItem)) { await this._showKernelExtension( - this._paneCompositePartService, this._extensionWorkbenchService, this._extensionService, editor.textModel.viewType, diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index bf781d717da..101650b7dbd 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -30,12 +30,10 @@ import { getCodiconAriaLabel } from '../../../../base/common/iconLabels.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ReloadWindowAction } from '../../../browser/actions/windowActions.js'; import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { IExtensionsViewPaneContainer, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, VIEWLET_ID } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from '../../extensions/common/extensions.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; import { RemoteNameContext, VirtualWorkspaceContext } from '../../../common/contextkeys.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -227,13 +225,8 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr }); } run = (accessor: ServicesAccessor, input: string) => { - const paneCompositeService = accessor.get(IPaneCompositePartService); - return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true).then(viewlet => { - if (viewlet) { - (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(`@recommended:remotes`); - viewlet.focus(); - } - }); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + return extensionsWorkbenchService.openSearch(`@recommended:remotes`); }; })); } diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index c5de8a8adaa..5557570e892 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -38,8 +38,7 @@ import { widgetClose } from '../../../../platform/theme/common/iconRegistry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ViewAction } from '../../../browser/parts/views/viewPane.js'; import { FocusedViewContext } from '../../../common/contextkeys.js'; -import { ViewContainerLocation } from '../../../common/views.js'; -import { VIEWLET_ID as EXTENSIONS_VIEWLET_ID, IExtensionsViewPaneContainer } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { TestExplorerTreeElement, TestItemTreeElement } from './explorerProjections/index.js'; import * as icons from './icons.js'; import { TestingExplorerView } from './testingExplorerView.js'; @@ -58,7 +57,6 @@ import { ITestingContinuousRunService } from '../common/testingContinuousRunServ import { ITestingPeekOpener } from '../common/testingPeekOpener.js'; import { isFailedState } from '../common/testingStates.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; const category = Categories.Test; @@ -1556,10 +1554,7 @@ export class SearchForTestExtension extends Action2 { } public async run(accessor: ServicesAccessor) { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const viewlet = (await paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; - viewlet.search('@category:"testing"'); - viewlet.focus(); + accessor.get(IExtensionsWorkbenchService).openSearch('@category:"testing"'); } } diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index b7a983ccd40..93599087f54 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -10,7 +10,7 @@ import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme, ThemeSettings, ThemeSettingDefaults } from '../../../services/themes/common/workbenchThemeService.js'; -import { VIEWLET_ID, IExtensionsViewPaneContainer } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IColorRegistry, Extensions as ColorRegistryExtensions } from '../../../../platform/theme/common/colorRegistry.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -20,8 +20,6 @@ import { colorThemeSchemaId } from '../../../services/themes/common/colorThemeSc import { isCancellationError, onUnexpectedError } from '../../../../base/common/errors.js'; import { IQuickInputButton, IQuickInputService, IQuickInputToggle, IQuickPick, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { DEFAULT_PRODUCT_ICON_THEME_ID, ProductIconThemeData } from '../../../services/themes/browser/productIconThemeData.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ThrottledDelayer } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -81,7 +79,7 @@ class MarketplaceThemesPicker { @IQuickInputService private readonly quickInputService: IQuickInputService, @ILogService private readonly logService: ILogService, @IProgressService private readonly progressService: IProgressService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IDialogService private readonly dialogService: IDialogService ) { this._installedExtensions = extensionManagementService.getInstalled().then(installed => { @@ -197,9 +195,9 @@ class MarketplaceThemesPicker { if (isItem(e.item)) { const extensionId = e.item.theme?.extensionData?.extensionId; if (extensionId) { - openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`); + this.extensionsWorkbenchService.openSearch(`@id:${extensionId}`); } else { - openExtensionViewlet(this.paneCompositeService, `${this.marketplaceQuery} ${quickpick.value}`); + this.extensionsWorkbenchService.openSearch(`${this.marketplaceQuery} ${quickpick.value}`); } } })); @@ -248,7 +246,7 @@ class MarketplaceThemesPicker { } private async installExtension(galleryExtension: IGalleryExtension) { - openExtensionViewlet(this.paneCompositeService, `@id:${galleryExtension.identifier.id}`); + this.extensionsWorkbenchService.openSearch(`@id:${galleryExtension.identifier.id}`); const result = await this.dialogService.confirm({ message: localize('installExtension.confirm', "This will install extension '{0}' published by '{1}'. Do you want to continue?", galleryExtension.displayName, galleryExtension.publisherDisplayName), primaryButton: localize('installExtension.button.ok', "OK") @@ -303,7 +301,7 @@ class InstalledThemesPicker { private readonly getMarketplaceColorThemes: (publisher: string, name: string, version: string) => Promise, @IQuickInputService private readonly quickInputService: IQuickInputService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -361,7 +359,7 @@ class InstalledThemesPicker { const theme = quickpick.selectedItems[0]; if (!theme || theme.configureItem) { // 'pick in marketplace' entry if (!theme || theme.configureItem === ConfigureItem.EXTENSIONS_VIEW) { - openExtensionViewlet(this.paneCompositeService, `${this.options.marketplaceTag} ${quickpick.value}`); + this.extensionsWorkbenchService.openSearch(`${this.options.marketplaceTag} ${quickpick.value}`); } else if (theme.configureItem === ConfigureItem.BROWSE_GALLERY) { if (marketplaceThemePicker) { const res = await marketplaceThemePicker.openQuickPick(quickpick.value, currentTheme, selectTheme); @@ -389,9 +387,9 @@ class InstalledThemesPicker { if (isItem(e.item)) { const extensionId = e.item.theme?.extensionData?.extensionId; if (extensionId) { - openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`); + this.extensionsWorkbenchService.openSearch(`@id:${extensionId}`); } else { - openExtensionViewlet(this.paneCompositeService, `${this.options.marketplaceTag} ${quickpick.value}`); + this.extensionsWorkbenchService.openSearch(`${this.options.marketplaceTag} ${quickpick.value}`); } } })); @@ -606,14 +604,6 @@ function configurationEntry(label: string, configureItem: ConfigureItem): QuickP }; } -function openExtensionViewlet(paneCompositeService: IPaneCompositePartService, query: string) { - return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true).then(viewlet => { - if (viewlet) { - (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); - viewlet.focus(); - } - }); -} interface ThemeItem extends IQuickPickItem { readonly id: string | undefined; readonly theme?: IWorkbenchTheme; From 88d7b97c93ffff269c9e660a9afc7077b3371a78 Mon Sep 17 00:00:00 2001 From: hj Date: Mon, 9 Sep 2024 18:32:51 +0800 Subject: [PATCH 180/286] chore: rm unreached ignore items when build extensions. (#227906) chore: rm unreached ignore items when compiling extensions. --- extensions/configuration-editing/.vscodeignore | 1 - extensions/css-language-features/.vscodeignore | 2 -- extensions/debug-auto-launch/.vscodeignore | 1 - extensions/debug-server-ready/.vscodeignore | 1 - extensions/emmet/.vscodeignore | 1 - extensions/extension-editing/.vscodeignore | 1 - extensions/git/.vscodeignore | 1 - extensions/github-authentication/.vscodeignore | 1 - extensions/github/.vscodeignore | 1 - extensions/grunt/.vscodeignore | 1 - extensions/gulp/.vscodeignore | 1 - extensions/html-language-features/.vscodeignore | 2 -- extensions/ipynb/.vscodeignore | 1 - extensions/jake/.vscodeignore | 1 - extensions/json-language-features/.vscodeignore | 2 -- extensions/json-language-features/server/.npmignore | 1 - extensions/markdown-language-features/.vscodeignore | 1 - extensions/markdown-math/.vscodeignore | 1 - extensions/media-preview/.vscodeignore | 1 - extensions/merge-conflict/.vscodeignore | 1 - extensions/microsoft-authentication/.vscodeignore | 1 - extensions/npm/.vscodeignore | 1 - extensions/php-language-features/.vscodeignore | 1 - extensions/references-view/.vscodeignore | 1 - extensions/search-result/.vscodeignore | 1 - extensions/simple-browser/.vscodeignore | 1 - extensions/tunnel-forwarding/.vscodeignore | 1 - extensions/typescript-language-features/.vscodeignore | 1 - 28 files changed, 31 deletions(-) diff --git a/extensions/configuration-editing/.vscodeignore b/extensions/configuration-editing/.vscodeignore index ba1754c45fa..679a6d6859f 100644 --- a/extensions/configuration-editing/.vscodeignore +++ b/extensions/configuration-editing/.vscodeignore @@ -4,7 +4,6 @@ tsconfig.json out/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json build/** schemas/devContainer.codespaces.schema.json diff --git a/extensions/css-language-features/.vscodeignore b/extensions/css-language-features/.vscodeignore index 73d4b9ba388..f6411e76fdb 100644 --- a/extensions/css-language-features/.vscodeignore +++ b/extensions/css-language-features/.vscodeignore @@ -11,10 +11,8 @@ server/tsconfig.json server/test/** server/bin/** server/build/** -server/yarn.lock server/package-lock.json server/.npmignore -yarn.lock package-lock.json server/extension.webpack.config.js extension.webpack.config.js diff --git a/extensions/debug-auto-launch/.vscodeignore b/extensions/debug-auto-launch/.vscodeignore index 74215d83d91..360fcfd1c99 100644 --- a/extensions/debug-auto-launch/.vscodeignore +++ b/extensions/debug-auto-launch/.vscodeignore @@ -2,5 +2,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/debug-server-ready/.vscodeignore b/extensions/debug-server-ready/.vscodeignore index ca4c65e8cce..536dd0720a3 100644 --- a/extensions/debug-server-ready/.vscodeignore +++ b/extensions/debug-server-ready/.vscodeignore @@ -2,6 +2,5 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json .vscode diff --git a/extensions/emmet/.vscodeignore b/extensions/emmet/.vscodeignore index 1ced958403c..ccf478fb97f 100644 --- a/extensions/emmet/.vscodeignore +++ b/extensions/emmet/.vscodeignore @@ -7,6 +7,5 @@ extension.webpack.config.js extension-browser.webpack.config.js CONTRIBUTING.md cgmanifest.json -yarn.lock package-lock.json .vscode diff --git a/extensions/extension-editing/.vscodeignore b/extensions/extension-editing/.vscodeignore index 15a37fd80be..8d4da76b9cb 100644 --- a/extensions/extension-editing/.vscodeignore +++ b/extensions/extension-editing/.vscodeignore @@ -4,5 +4,4 @@ tsconfig.json out/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/git/.vscodeignore b/extensions/git/.vscodeignore index 61b7b9eae5b..1e6130d5c7d 100644 --- a/extensions/git/.vscodeignore +++ b/extensions/git/.vscodeignore @@ -4,5 +4,4 @@ out/** tsconfig.json build/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/github-authentication/.vscodeignore b/extensions/github-authentication/.vscodeignore index f4dce5682cf..0d0ea746e79 100644 --- a/extensions/github-authentication/.vscodeignore +++ b/extensions/github-authentication/.vscodeignore @@ -6,5 +6,4 @@ build/** extension.webpack.config.js extension-browser.webpack.config.js tsconfig.json -yarn.lock package-lock.json diff --git a/extensions/github/.vscodeignore b/extensions/github/.vscodeignore index 1ee958238d0..24b1bd4e73a 100644 --- a/extensions/github/.vscodeignore +++ b/extensions/github/.vscodeignore @@ -4,5 +4,4 @@ out/** build/** extension.webpack.config.js tsconfig.json -yarn.lock package-lock.json diff --git a/extensions/grunt/.vscodeignore b/extensions/grunt/.vscodeignore index 211cee1f60a..698898bf9df 100644 --- a/extensions/grunt/.vscodeignore +++ b/extensions/grunt/.vscodeignore @@ -3,5 +3,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/gulp/.vscodeignore b/extensions/gulp/.vscodeignore index 74215d83d91..360fcfd1c99 100644 --- a/extensions/gulp/.vscodeignore +++ b/extensions/gulp/.vscodeignore @@ -2,5 +2,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/html-language-features/.vscodeignore b/extensions/html-language-features/.vscodeignore index dd12019eef7..b1586b6bc84 100644 --- a/extensions/html-language-features/.vscodeignore +++ b/extensions/html-language-features/.vscodeignore @@ -13,10 +13,8 @@ server/test/** server/bin/** server/build/** server/lib/cgmanifest.json -server/yarn.lock server/package-lock.json server/.npmignore -yarn.lock package-lock.json server/extension.webpack.config.js extension.webpack.config.js diff --git a/extensions/ipynb/.vscodeignore b/extensions/ipynb/.vscodeignore index f157f8e9653..2d13f5a0c22 100644 --- a/extensions/ipynb/.vscodeignore +++ b/extensions/ipynb/.vscodeignore @@ -5,7 +5,6 @@ out/** tsconfig.json extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json .gitignore esbuild.js diff --git a/extensions/jake/.vscodeignore b/extensions/jake/.vscodeignore index 74215d83d91..360fcfd1c99 100644 --- a/extensions/jake/.vscodeignore +++ b/extensions/jake/.vscodeignore @@ -2,5 +2,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/json-language-features/.vscodeignore b/extensions/json-language-features/.vscodeignore index 39da57a9aa4..807b3e4cbae 100644 --- a/extensions/json-language-features/.vscodeignore +++ b/extensions/json-language-features/.vscodeignore @@ -10,11 +10,9 @@ server/tsconfig.json server/test/** server/bin/** server/build/** -server/yarn.lock server/package-lock.json server/.npmignore server/README.md -yarn.lock package-lock.json CONTRIBUTING.md server/extension.webpack.config.js diff --git a/extensions/json-language-features/server/.npmignore b/extensions/json-language-features/server/.npmignore index 9b97a6afd92..960a01cc7b5 100644 --- a/extensions/json-language-features/server/.npmignore +++ b/extensions/json-language-features/server/.npmignore @@ -5,7 +5,6 @@ src/ test/ tsconfig.json .gitignore -yarn.lock package-lock.json extension.webpack.config.js vscode-json-languageserver-*.tgz diff --git a/extensions/markdown-language-features/.vscodeignore b/extensions/markdown-language-features/.vscodeignore index bbd727a2479..0d35b62002d 100644 --- a/extensions/markdown-language-features/.vscodeignore +++ b/extensions/markdown-language-features/.vscodeignore @@ -9,7 +9,6 @@ out/** extension.webpack.config.js extension-browser.webpack.config.js cgmanifest.json -yarn.lock package-lock.json preview-src/** webpack.config.js diff --git a/extensions/markdown-math/.vscodeignore b/extensions/markdown-math/.vscodeignore index 4a8c70b1633..85f550b7d7b 100644 --- a/extensions/markdown-math/.vscodeignore +++ b/extensions/markdown-math/.vscodeignore @@ -4,7 +4,6 @@ extension-browser.webpack.config.js extension.webpack.config.js esbuild.js cgmanifest.json -yarn.lock package-lock.json webpack.config.js tsconfig.json diff --git a/extensions/media-preview/.vscodeignore b/extensions/media-preview/.vscodeignore index 601e6fa300c..532c87f6f2e 100644 --- a/extensions/media-preview/.vscodeignore +++ b/extensions/media-preview/.vscodeignore @@ -6,7 +6,6 @@ out/** extension.webpack.config.js extension-browser.webpack.config.js cgmanifest.json -yarn.lock package-lock.json preview-src/** webpack.config.js diff --git a/extensions/merge-conflict/.vscodeignore b/extensions/merge-conflict/.vscodeignore index b12e30a1894..3a8a2a96a6c 100644 --- a/extensions/merge-conflict/.vscodeignore +++ b/extensions/merge-conflict/.vscodeignore @@ -3,5 +3,4 @@ tsconfig.json out/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/microsoft-authentication/.vscodeignore b/extensions/microsoft-authentication/.vscodeignore index 0e10c404a9a..98b90d34d82 100644 --- a/extensions/microsoft-authentication/.vscodeignore +++ b/extensions/microsoft-authentication/.vscodeignore @@ -4,7 +4,6 @@ out/test/** out/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json src/** .gitignore diff --git a/extensions/npm/.vscodeignore b/extensions/npm/.vscodeignore index c7df59ef78a..f05a79416be 100644 --- a/extensions/npm/.vscodeignore +++ b/extensions/npm/.vscodeignore @@ -4,5 +4,4 @@ tsconfig.json .vscode/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/php-language-features/.vscodeignore b/extensions/php-language-features/.vscodeignore index 7cddadb7bcc..e326d20ef52 100644 --- a/extensions/php-language-features/.vscodeignore +++ b/extensions/php-language-features/.vscodeignore @@ -3,5 +3,4 @@ src/** out/** tsconfig.json extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/references-view/.vscodeignore b/extensions/references-view/.vscodeignore index fd73b3fe680..4d2ffa699e4 100644 --- a/extensions/references-view/.vscodeignore +++ b/extensions/references-view/.vscodeignore @@ -3,5 +3,4 @@ src/** out/** tsconfig.json *.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/search-result/.vscodeignore b/extensions/search-result/.vscodeignore index 005af0b037c..35b808e16f7 100644 --- a/extensions/search-result/.vscodeignore +++ b/extensions/search-result/.vscodeignore @@ -3,6 +3,5 @@ out/** tsconfig.json extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json syntaxes/generateTMLanguage.js diff --git a/extensions/simple-browser/.vscodeignore b/extensions/simple-browser/.vscodeignore index ddcbdff84f4..c69acedcc24 100644 --- a/extensions/simple-browser/.vscodeignore +++ b/extensions/simple-browser/.vscodeignore @@ -8,7 +8,6 @@ extension.webpack.config.js extension-browser.webpack.config.js cgmanifest.json .gitignore -yarn.lock package-lock.json preview-src/** webpack.config.js diff --git a/extensions/tunnel-forwarding/.vscodeignore b/extensions/tunnel-forwarding/.vscodeignore index 74215d83d91..360fcfd1c99 100644 --- a/extensions/tunnel-forwarding/.vscodeignore +++ b/extensions/tunnel-forwarding/.vscodeignore @@ -2,5 +2,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/typescript-language-features/.vscodeignore b/extensions/typescript-language-features/.vscodeignore index 4c9a34b7edc..5c4f06d4cd3 100644 --- a/extensions/typescript-language-features/.vscodeignore +++ b/extensions/typescript-language-features/.vscodeignore @@ -8,5 +8,4 @@ tsconfig.json extension.webpack.config.js extension-browser.webpack.config.js cgmanifest.json -yarn.lock package-lock.json From 7d6bfad18097641d29eca980f1eefe8d617b5968 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 9 Sep 2024 13:22:43 +0200 Subject: [PATCH 181/286] Using the model state instead of view state in native edit context (#227965) using the model state instead of view state in native edit context --- .../controller/editContext/native/nativeEditContext.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 01353f1ceb8..62749f46311 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -94,7 +94,7 @@ export class NativeEditContext extends AbstractEditContext { this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => { const compositionRangeWithinEditor = this._compositionRangeWithinEditor; if (compositionRangeWithinEditor) { - const position = this._context.viewModel.getPrimaryCursorState().viewState.position; + const position = this._context.viewModel.getPrimaryCursorState().modelState.position; const newCompositionRangeWithinEditor = Range.fromPositions(compositionRangeWithinEditor.getStartPosition(), position); this._compositionRangeWithinEditor = newCompositionRangeWithinEditor; } @@ -105,7 +105,7 @@ export class NativeEditContext extends AbstractEditContext { this._screenReaderSupport.writeScreenReaderContent(); })); this._register(editContextAddDisposableListener(this._editContext, 'compositionstart', (e) => { - const position = this._context.viewModel.getPrimaryCursorState().viewState.position; + const position = this._context.viewModel.getPrimaryCursorState().modelState.position; const newCompositionRange = Range.fromPositions(position, position); this._compositionRangeWithinEditor = newCompositionRange; // Utlimately fires onDidCompositionStart() on the editor to notify for example suggest model of composition state @@ -262,12 +262,12 @@ export class NativeEditContext extends AbstractEditContext { if (i === this._primarySelection.endLineNumber) { selectionEndOffset += this._primarySelection.endColumn - 1; } else { - selectionEndOffset += this._context.viewModel.getLineMaxColumn(i); + selectionEndOffset += this._context.viewModel.model.getLineMaxColumn(i); } } - const endColumnOfEndLineNumber = this._context.viewModel.getLineMaxColumn(this._primarySelection.endLineNumber); + const endColumnOfEndLineNumber = this._context.viewModel.model.getLineMaxColumn(this._primarySelection.endLineNumber); const rangeOfText = new Range(this._primarySelection.startLineNumber, 1, this._primarySelection.endLineNumber, endColumnOfEndLineNumber); - const text = this._context.viewModel.getValueInRange(rangeOfText, EndOfLinePreference.TextDefined); + const text = this._context.viewModel.model.getValueInRange(rangeOfText, EndOfLinePreference.TextDefined); const textStartPositionWithinEditor = rangeOfText.getStartPosition(); return { text, From 3f0893b2739373118f9a38d40f4ad683b32faab4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:40:56 +0200 Subject: [PATCH 182/286] SCM - cleanup history provider proposed API (#227960) --- extensions/git/src/decorationProvider.ts | 2 +- extensions/git/src/historyProvider.ts | 20 +++++++++---------- src/vs/workbench/api/browser/mainThreadSCM.ts | 4 ++-- .../workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostSCM.ts | 4 ++-- .../contrib/scm/browser/scmHistoryViewPane.ts | 4 ++-- .../workbench/contrib/scm/common/history.ts | 5 ++--- .../vscode.proposed.scmHistoryProvider.d.ts | 5 ++--- 8 files changed, 22 insertions(+), 24 deletions(-) diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 8c86834c92c..6de2cfae31d 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -224,7 +224,7 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider return []; } - const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor([currentHistoryItemGroup.id, currentHistoryItemGroup.remote.id]); + const ancestor = await historyProvider.resolveHistoryItemRefsCommonAncestor([currentHistoryItemGroup.id, currentHistoryItemGroup.remote.id]); if (!ancestor) { return []; } diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 6190c2a538b..e9aefa63dd1 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -128,12 +128,12 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems(options: SourceControlHistoryOptions): Promise { - if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { + if (!this.currentHistoryItemGroup || !options.historyItemRefs) { return []; } // Deduplicate refNames - const refNames = Array.from(new Set(options.historyItemGroupIds)); + const refNames = Array.from(new Set(options.historyItemRefs)); let logOptions: LogOptions = { refNames, shortStats: true }; @@ -211,21 +211,21 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return historyItemChanges; } - async resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise { + async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise { try { - if (historyItemGroupIds.length === 0) { + if (historyItemRefs.length === 0) { // TODO@lszomoru - log return undefined; - } else if (historyItemGroupIds.length === 1 && historyItemGroupIds[0] === this.currentHistoryItemGroup?.id) { + } else if (historyItemRefs.length === 1 && historyItemRefs[0] === this.currentHistoryItemGroup?.id) { // Remote if (this.currentHistoryItemGroup.remote) { - const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.remote.id); + const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemGroup.remote.id); return ancestor; } // Base if (this.currentHistoryItemGroup.base) { - const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.base.id); + const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemGroup.base.id); return ancestor; } @@ -234,13 +234,13 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec if (commits.length > 0) { return commits[0].hash; } - } else if (historyItemGroupIds.length > 1) { - const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], historyItemGroupIds[1], ...historyItemGroupIds.slice(2)); + } else if (historyItemRefs.length > 1) { + const ancestor = await this.repository.getMergeBase(historyItemRefs[0], historyItemRefs[1], ...historyItemRefs.slice(2)); return ancestor; } } catch (err) { - this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Failed to resolve common ancestor for ${historyItemGroupIds.join(',')}: ${err}`); + this.logger.error(`[GitHistoryProvider][resolveHistoryItemRefsCommonAncestor] Failed to resolve common ancestor for ${historyItemRefs.join(',')}: ${err}`); } return undefined; diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 232a052bb7e..7ab6d69e6ae 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -220,8 +220,8 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } - async resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise { - return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupIds, CancellationToken.None); + async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise { + return this.proxy.$resolveHistoryItemRefsCommonAncestor(this.handle, historyItemRefs, CancellationToken.None); } async provideHistoryItemRefs(): Promise { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8b4cdc0a075..2f517f05a6d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2367,7 +2367,7 @@ export interface ExtHostSCMShape { $provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise; $provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; - $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise; + $resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: CancellationToken): Promise; } export interface ExtHostQuickDiffShape { diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index a600b14bd04..3976177cc7e 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -977,9 +977,9 @@ export class ExtHostSCM implements ExtHostSCMShape { return Promise.resolve(undefined); } - async $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise { + async $resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: CancellationToken): Promise { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; - return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupIds, token) ?? undefined; + return await historyProvider?.resolveHistoryItemRefsCommonAncestor(historyItemRefs, token) ?? undefined; } async $provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise { diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 00660ae54a1..e1b490de8d2 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -190,7 +190,7 @@ registerAction2(class extends Action2 { const historyProvider = provider.historyProvider.get(); if (historyItems.length > 1) { - const ancestor = await historyProvider?.resolveHistoryItemGroupCommonAncestor([historyItem.id, historyItemLast.id]); + const ancestor = await historyProvider?.resolveHistoryItemRefsCommonAncestor([historyItem.id, historyItemLast.id]); if (!ancestor || (ancestor !== historyItem.id && ancestor !== historyItemLast.id)) { return; } @@ -704,7 +704,7 @@ class SCMHistoryViewModel extends Disposable { const historyItemGroupIds = historyItemRefs.map(ref => ref.revision ?? ref.id); const historyItems = await historyProvider.provideHistoryItems({ - historyItemGroupIds, limit, skip: existingHistoryItems.length + historyItemRefs: historyItemGroupIds, limit, skip: existingHistoryItems.length }) ?? []; state = { diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index dd61d7362e4..fbc0d6c0f1f 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -24,14 +24,13 @@ export interface ISCMHistoryProvider { provideHistoryItemRefs(): Promise; provideHistoryItems(options: ISCMHistoryOptions): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; - resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise; + resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise; } export interface ISCMHistoryOptions { - readonly cursor?: string; readonly skip?: number; readonly limit?: number | { id?: string }; - readonly historyItemGroupIds?: readonly string[]; + readonly historyItemRefs?: readonly string[]; } export interface ISCMHistoryItemGroup { diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index fcd0286936f..d465d599ba2 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -28,14 +28,13 @@ declare module 'vscode' { provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; - resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[], token: CancellationToken): ProviderResult; + resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[], token: CancellationToken): ProviderResult; } export interface SourceControlHistoryOptions { - readonly cursor?: string; readonly skip?: number; readonly limit?: number | { id?: string }; - readonly historyItemGroupIds?: readonly string[]; + readonly historyItemRefs?: readonly string[]; } export interface SourceControlHistoryItemGroup { From d0b2168a691be86907a2709f701acea482e8a584 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:41:20 +0200 Subject: [PATCH 183/286] SCM Graph - improve picker rendering (#227970) --- .../contrib/scm/browser/media/scm.css | 7 ++ .../contrib/scm/browser/scmHistoryViewPane.ts | 67 ++++++++++++++++--- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index eb98f680a0b..81c5fd8ec42 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -507,6 +507,13 @@ font-size: 14px; } +.monaco-toolbar .action-label.scm-graph-repository-picker > .name, +.monaco-toolbar .action-label.scm-graph-history-item-picker > .name { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; +} + .scm-history-view .scm-provider .label-name { font-weight: bold; } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index e1b490de8d2..d6f366299e8 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -61,39 +61,83 @@ import { clamp } from '../../../../base/common/numbers.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { structuralEquals } from '../../../../base/common/equals.js'; import { compare } from '../../../../base/common/strings.js'; +import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; class SCMRepositoryActionViewItem extends ActionViewItem { constructor(private readonly _repository: ISCMRepository, action: IAction, options?: IDropdownMenuActionViewItemOptions) { - super(null, action, { ...options, icon: false, label: true }); + super(null, action, { ...options, icon: false, label: true, hoverDelegate: createInstantHoverDelegate() }); } protected override updateLabel(): void { if (this.options.label && this.label) { this.label.classList.add('scm-graph-repository-picker'); - reset(this.label, ...renderLabelWithIcons(`$(repo) ${this._repository.provider.name}`)); + + const icon = $('.icon'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.repo)); + + const name = $('.name'); + name.textContent = this._repository.provider.name; + + + reset(this.label, icon, name); } } + + protected override getTooltip(): string | undefined { + return this._repository.provider.name; + } } class SCMHistoryItemRefsActionViewItem extends ActionViewItem { - constructor(private readonly _historyItemsFilter: HistoryItemRefsFilter, action: IAction, options?: IDropdownMenuActionViewItemOptions) { - super(null, action, { ...options, icon: false, label: true }); + constructor( + private readonly _repository: ISCMRepository, + private readonly _historyItemsFilter: HistoryItemRefsFilter, + action: IAction, + options?: IDropdownMenuActionViewItemOptions + ) { + super(null, action, { ...options, icon: false, label: true, hoverDelegate: createInstantHoverDelegate() }); } protected override updateLabel(): void { if (this.options.label && this.label) { this.label.classList.add('scm-graph-history-item-picker'); + + const icon = $('.icon'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.gitBranch)); + + const name = $('.name'); if (this._historyItemsFilter === 'all') { - reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${localize('all', "All")}`)); + name.textContent = localize('all', "All"); } else if (this._historyItemsFilter === 'auto') { - reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${localize('auto', "Auto")}`)); + name.textContent = localize('auto', "Auto"); } else if (this._historyItemsFilter.length === 1) { + name.textContent = this._historyItemsFilter[0].name; reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${this._historyItemsFilter[0].name}`)); } else { - reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${this._historyItemsFilter.length} ${localize('items', "Items")}`)); + name.textContent = localize('items', "{0} Items", this._historyItemsFilter.length); } + + reset(this.label, icon, name); + } + } + + protected override getTooltip(): string | undefined { + if (this._historyItemsFilter === 'all') { + return localize('allHistoryItemRefs', "All history item references"); + } else if (this._historyItemsFilter === 'auto') { + const historyProvider = this._repository.provider.historyProvider.get(); + + return [ + historyProvider?.currentHistoryItemRef.get()?.name, + historyProvider?.currentHistoryItemRemoteRef.get()?.name, + historyProvider?.currentHistoryItemBaseRef.get()?.name + ].filter(ref => !!ref).join(', '); + } else if (this._historyItemsFilter.length === 1) { + return this._historyItemsFilter[0].name; + } else { + return this._historyItemsFilter.map(ref => ref.name).join(', '); } } } @@ -810,14 +854,14 @@ class HistoryItemRefPicker extends Disposable { private readonly _allQuickPickItem: HistoryItemRefQuickPickItem = { id: 'all', label: localize('all', "All"), - description: localize('allHistoryItemRefs', "Show all history item references"), + description: localize('allHistoryItemRefs', "All history item references"), historyItemRef: 'all' }; private readonly _autoQuickPickItem: HistoryItemRefQuickPickItem = { id: 'auto', label: localize('auto', "Auto"), - description: localize('currentHistoryItemRef', "Show the current history item reference"), + description: localize('currentHistoryItemRef', "Current history item reference(s)"), historyItemRef: 'auto' }; @@ -1116,9 +1160,10 @@ export class SCMHistoryViewPane extends ViewPane { return new SCMRepositoryActionViewItem(repository, action, options); } } else if (action.id === 'workbench.scm.graph.action.pickHistoryItemRefs') { + const repository = this._treeViewModel?.repository.get(); const historyItemsFilter = this._treeViewModel?.historyItemsFilter.get(); - if (historyItemsFilter) { - return new SCMHistoryItemRefsActionViewItem(historyItemsFilter, action, options); + if (repository && historyItemsFilter) { + return new SCMHistoryItemRefsActionViewItem(repository, historyItemsFilter, action, options); } } From 446a66e07142d5aaebf343a65e2b0a5d1d29be75 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 9 Sep 2024 16:15:05 +0200 Subject: [PATCH 184/286] resetting the selection to what it was before the selection change --- .../controller/editContext/native/screenReaderSupport.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts index 1db00edef5f..f103d8172a2 100644 --- a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts +++ b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from '../../../../../base/browser/dom.js'; +import { getActiveWindow, isHTMLElement } from '../../../../../base/browser/dom.js'; import { FastDomNode } from '../../../../../base/browser/fastDomNode.js'; import { AccessibilitySupport } from '../../../../../platform/accessibility/common/accessibility.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; @@ -149,10 +149,14 @@ export class ScreenReaderSupport { if (!textContent) { return; } + const focusedElement = getActiveWindow().document.activeElement; const range = new globalThis.Range(); range.setStart(textContent, selectionOffsetStart); range.setEnd(textContent, selectionOffsetEnd); activeDocumentSelection.removeAllRanges(); activeDocumentSelection.addRange(range); + if (isHTMLElement(focusedElement)) { + focusedElement.focus(); + } } } From 746ba5bc6881820beb3bdf3689829210b57f7d5a Mon Sep 17 00:00:00 2001 From: Robo Date: Mon, 9 Sep 2024 23:21:08 +0900 Subject: [PATCH 185/286] cli: revert "code server-web when offline" (#227981) * Revert "Merge pull request #227830 from microsoft/connor4312/cli-offline-serve" This reverts commit 136a5c6f5fdd19966ddb36f8aef080a6e94706a7, reversing changes made to 9d388bbf8bf9ac0c3c978ec84431eee302cb7712. * chore: bump distro --- cli/Cargo.toml | 1 - cli/src/auth.rs | 2 +- cli/src/commands/serve_web.rs | 73 ++++++---------------------------- cli/src/download_cache.rs | 8 +--- cli/src/tunnels/code_server.rs | 2 +- package.json | 2 +- 6 files changed, 17 insertions(+), 71 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 2907ff3d7e7..b820ffcc50f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -81,5 +81,4 @@ codegen-units = 1 [features] default = [] -vsda = [] vscode-encrypt = [] diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 51942c96c75..2d9162c5483 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -723,7 +723,7 @@ impl Auth { match &init_code_json.message { Some(m) => self.log.result(m), - None => self.log.result(format!( + None => self.log.result(&format!( "To grant access to the server, please log into {} and use code {}", init_code_json.verification_uri, init_code_json.user_code )), diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 4acb9a18c73..d8d2a49bb1a 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -15,7 +15,7 @@ use std::time::{Duration, Instant}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::{pin, time}; +use tokio::pin; use crate::async_pipe::{ get_socket_name, get_socket_rw_stream, listen_socket_rw_stream, AsyncPipe, @@ -50,7 +50,7 @@ const SERVER_IDLE_TIMEOUT_SECS: u64 = 60 * 60; /// (should be large enough to basically never happen) const SERVER_ACTIVE_TIMEOUT_SECS: u64 = SERVER_IDLE_TIMEOUT_SECS * 24 * 30 * 12; /// How long to cache the "latest" version we get from the update service. -const RELEASE_CHECK_INTERVAL: u64 = 60 * 60; +const RELEASE_CACHE_SECS: u64 = 60 * 60; /// Number of bytes for the secret keys. See workbench.ts for their usage. const SECRET_KEY_BYTES: usize = 32; @@ -86,11 +86,7 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result = ConnectionManager::new(&ctx, platform, args.clone()); - let update_check_interval = 3600; - cm.clone() - .start_update_checker(Duration::from_secs(update_check_interval)); - + let cm = ConnectionManager::new(&ctx, platform, args.clone()); let key = get_server_key_half(&ctx.paths); let make_svc = move || { let ctx = HandleContext { @@ -179,7 +175,7 @@ async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response r, Err(e) => { error!(ctx.log, "error getting latest version: {}", e); @@ -542,67 +538,21 @@ impl ConnectionManager { pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { let base_path = normalize_base_path(args.server_base_path.as_deref().unwrap_or_default()); - let cache = DownloadCache::new(ctx.paths.web_server_storage()); - let target_kind = TargetKind::Web; - - let quality = VSCODE_CLI_QUALITY.map_or(Quality::Stable, |q| match Quality::try_from(q) { - Ok(q) => q, - Err(_) => Quality::Stable, - }); - - let latest_version = tokio::sync::Mutex::new(cache.get().first().map(|latest_commit| { - ( - Instant::now() - Duration::from_secs(RELEASE_CHECK_INTERVAL), - Release { - name: String::from("0.0.0"), // Version information not stored on cache - commit: latest_commit.clone(), - platform, - target: target_kind, - quality, - }, - ) - })); - Arc::new(Self { platform, args, base_path, log: ctx.log.clone(), - cache, + cache: DownloadCache::new(ctx.paths.web_server_storage()), update_service: UpdateService::new( ctx.log.clone(), Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())), ), state: ConnectionStateMap::default(), - latest_version, + latest_version: tokio::sync::Mutex::default(), }) } - // spawns a task that checks for updates every n seconds duration - pub fn start_update_checker(self: Arc, duration: Duration) { - tokio::spawn(async move { - let mut interval = time::interval(duration); - loop { - interval.tick().await; - - if let Err(e) = self.get_latest_release().await { - warning!(self.log, "error getting latest version: {}", e); - } - } - }); - } - - // Returns the latest release from the cache, if one exists. - pub async fn get_release_from_cache(&self) -> Result { - let latest = self.latest_version.lock().await; - if let Some((_, release)) = &*latest { - return Ok(release.clone()); - } - - drop(latest); - self.get_latest_release().await - } - /// Gets a connection to a server version pub async fn get_connection( &self, @@ -621,7 +571,11 @@ impl ConnectionManager { pub async fn get_latest_release(&self) -> Result { let mut latest = self.latest_version.lock().await; let now = Instant::now(); - let target_kind = TargetKind::Web; + if let Some((checked_at, release)) = &*latest { + if checked_at.elapsed() < Duration::from_secs(RELEASE_CACHE_SECS) { + return Ok(release.clone()); + } + } let quality = VSCODE_CLI_QUALITY .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) @@ -631,14 +585,13 @@ impl ConnectionManager { let release = self .update_service - .get_latest_commit(self.platform, target_kind, quality) + .get_latest_commit(self.platform, TargetKind::Web, quality) .await .map_err(|e| CodeError::UpdateCheckFailed(e.to_string())); // If the update service is unavailable and we have stale data, use that - if let (Err(e), Some((_, previous))) = (&release, latest.clone()) { + if let (Err(e), Some((_, previous))) = (&release, &*latest) { warning!(self.log, "error getting latest release, using stale: {}", e); - *latest = Some((now, previous.clone())); return Ok(previous.clone()); } diff --git a/cli/src/download_cache.rs b/cli/src/download_cache.rs index cd02b02d75a..d3f05d2237f 100644 --- a/cli/src/download_cache.rs +++ b/cli/src/download_cache.rs @@ -20,7 +20,6 @@ const KEEP_LRU: usize = 5; const STAGING_SUFFIX: &str = ".staging"; const RENAME_ATTEMPTS: u32 = 20; const RENAME_DELAY: std::time::Duration = std::time::Duration::from_millis(200); -const PERSISTED_STATE_FILE_NAME: &str = "lru.json"; #[derive(Clone)] pub struct DownloadCache { @@ -31,16 +30,11 @@ pub struct DownloadCache { impl DownloadCache { pub fn new(path: PathBuf) -> DownloadCache { DownloadCache { - state: PersistedState::new(path.join(PERSISTED_STATE_FILE_NAME)), + state: PersistedState::new(path.join("lru.json")), path, } } - /// Gets the value stored on the state - pub fn get(&self) -> Vec { - self.state.load() - } - /// Gets the download cache path. Names of cache entries can be formed by /// joining them to the path. pub fn path(&self) -> &Path { diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 465f6e24230..0579f8ef0dc 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -674,7 +674,7 @@ where let write_line = |line: &str| -> std::io::Result<()> { if let Some(mut f) = log_file.as_ref() { f.write_all(line.as_bytes())?; - f.write_all(b"\n")?; + f.write_all(&[b'\n'])?; } if write_directly { println!("{}", line); diff --git a/package.json b/package.json index 149a219512b..4104623cf41 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.94.0", - "distro": "fcaeb73de7ac6ff11a3e732c63987e01b88341e7", + "distro": "36c6d77f96e54b1ad2233cd24fed8e8f08d5a388", "author": { "name": "Microsoft Corporation" }, From ab2df1ffdc2853fe85bb4da95fe5a4fb89b2da68 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 9 Sep 2024 16:52:51 +0200 Subject: [PATCH 186/286] tweak chat file suggestions (#227991) * mark result as incomplete in all cases * don't include `#file:` in label * tweak sorting --- .../chat/browser/contrib/chatInputCompletions.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index a751a34bf6f..60a91948272 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -382,11 +382,12 @@ class BuiltinDynamicCompletions extends Disposable { const insertText = `${chatVariableLeader}file:${basename} `; return { - label: { label: `${chatVariableLeader}file:${basename}`, description: this.labelService.getUriLabel(resource) }, + label: { label: basename, description: this.labelService.getUriLabel(resource) }, + filterText: info.varWord?.word, insertText, range: info, kind: CompletionItemKind.File, - sortText: 'zz', + sortText: '{', // after `z` command: { id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { id: 'vscode.file', @@ -433,7 +434,8 @@ class BuiltinDynamicCompletions extends Disposable { if (pattern) { const query = this.queryBuilder.file(this.workspaceContextService.getWorkspace().folders, { filePattern: pattern, - maxResults: 25 + sortByScore: true, + maxResults: 100, }); const data = await this.searchService.fileSearch(query, token); @@ -444,11 +446,11 @@ class BuiltinDynamicCompletions extends Disposable { } result.suggestions.push(makeFileCompletionItem(match.resource)); } - - if (!data.limitHit) { - result.incomplete = true; - } } + + // mark results as incomplete because further typing might yield + // in more search results + result.incomplete = true; } private cmdAddReference(arg: ReferenceArgument) { From 8cfec4aca55f69aa3c17e68328c3a94ad693301b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:09:07 -0700 Subject: [PATCH 187/286] Don't show suggest widget when terminal isn't focused Fixes #227993 --- .../terminalContrib/suggest/browser/terminalSuggestAddon.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 4dea30b5e12..2897cc88612 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -348,6 +348,11 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } + // Only show the suggest widget if the terminal is focused + if (!dom.isAncestorOfActiveElement(terminal.element)) { + return; + } + let replacementIndex = 0; let replacementLength = this._promptInputModel.cursorIndex; From f4739315091bb87992a0606fcf343650b71e7b1a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:31:30 +0200 Subject: [PATCH 188/286] SCM Graph - improve picker hover (#227978) --- src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index d6f366299e8..b67f54ee8e2 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -61,13 +61,12 @@ import { clamp } from '../../../../base/common/numbers.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { structuralEquals } from '../../../../base/common/equals.js'; import { compare } from '../../../../base/common/strings.js'; -import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; class SCMRepositoryActionViewItem extends ActionViewItem { constructor(private readonly _repository: ISCMRepository, action: IAction, options?: IDropdownMenuActionViewItemOptions) { - super(null, action, { ...options, icon: false, label: true, hoverDelegate: createInstantHoverDelegate() }); + super(null, action, { ...options, icon: false, label: true }); } protected override updateLabel(): void { @@ -97,7 +96,7 @@ class SCMHistoryItemRefsActionViewItem extends ActionViewItem { action: IAction, options?: IDropdownMenuActionViewItemOptions ) { - super(null, action, { ...options, icon: false, label: true, hoverDelegate: createInstantHoverDelegate() }); + super(null, action, { ...options, icon: false, label: true }); } protected override updateLabel(): void { From 6b90336e96bf90354246ebe53922cf639486305e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:33:44 +0200 Subject: [PATCH 189/286] SCM Graph - add "Outdated" badge to view title (#227996) --- .../contrib/scm/browser/media/scm.css | 14 +++++++ .../contrib/scm/browser/scmHistoryViewPane.ts | 37 +++++++++++++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 81c5fd8ec42..1e6e2a8d9e2 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -491,6 +491,20 @@ /* Graph */ +.pane-header .scm-graph-view-badge-container { + display: flex; + align-items: center; + min-width: fit-content; +} + +.pane-header .scm-graph-view-badge-container > .scm-graph-view-badge.monaco-count-badge.long { + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + border: 1px solid var(--vscode-contrastBorder); + margin-left: 6px; + padding: 2px 4px; +} + .monaco-workbench .part.auxiliarybar > .title > .title-actions .action-label.scm-graph-repository-picker { display: flex; } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index b67f54ee8e2..9abd6a20719 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -980,7 +980,9 @@ export class SCMHistoryViewPane extends ViewPane { private _treeViewModel!: SCMHistoryViewModel; private _treeDataSource!: SCMHistoryTreeDataSource; private _treeIdentityProvider!: SCMHistoryTreeIdentityProvider; - private _repositoryLoadMore = observableValue(this, false); + + private readonly _repositoryLoadMore = observableValue(this, false); + private readonly _repositoryOutdated = observableValue(this, false); private readonly _actionRunner: IActionRunner; private readonly _visibilityDisposables = new DisposableStore(); @@ -1021,9 +1023,20 @@ export class SCMHistoryViewPane extends ViewPane { this._register(this._updateChildrenThrottler); } - protected override layoutBody(height: number, width: number): void { - super.layoutBody(height, width); - this._tree.layout(height, width); + protected override renderHeaderTitle(container: HTMLElement): void { + super.renderHeaderTitle(container, this.title); + + const element = h('div.scm-graph-view-badge-container', [ + h('div.scm-graph-view-badge.monaco-count-badge.long@badge') + ]); + + element.badge.textContent = 'Outdated'; + container.appendChild(element.root); + + this._register(autorun(reader => { + const outdated = this._repositoryOutdated.read(reader); + element.root.style.display = outdated ? '' : 'none'; + })); } protected override renderBody(container: HTMLElement): void { @@ -1105,7 +1118,10 @@ export class SCMHistoryViewPane extends ViewPane { return true; }, }, (reader, changeSummary) => { - if ((!historyItemGroup.read(reader) && !historyItemRemoteRevision.read(reader)) || changeSummary.refresh === false) { + const historyItemGroupValue = historyItemGroup.read(reader); + const historyItemRemoteRevisionValue = historyItemRemoteRevision.read(reader); + + if ((!historyItemGroupValue && !historyItemRemoteRevisionValue) || changeSummary.refresh === false) { return; } @@ -1123,8 +1139,8 @@ export class SCMHistoryViewPane extends ViewPane { return; } - // Set the "OUTDATED" description - this.updateTitleDescription(localize('outdated', "OUTDATED")); + // Show the "Outdated" badge on the view + this._repositoryOutdated.set(true, undefined); } })); @@ -1144,6 +1160,11 @@ export class SCMHistoryViewPane extends ViewPane { }); } + protected override layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + this._tree.layout(height, width); + } + override getActionRunner(): IActionRunner | undefined { return this._actionRunner; } @@ -1173,7 +1194,7 @@ export class SCMHistoryViewPane extends ViewPane { await this._updateChildren(true); this.updateActions(); - this.updateTitleDescription(undefined); + this._repositoryOutdated.set(false, undefined); this._tree.scrollTop = 0; } From 32ba9d05764ff54d313e4f9641a1078f7b7f78af Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:48:31 -0700 Subject: [PATCH 190/286] Handle @webgpu/types in standalone script --- build/lib/standalone.js | 7 ++++++- build/lib/standalone.ts | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/build/lib/standalone.js b/build/lib/standalone.js index d106585d28c..b724a009e8a 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -51,7 +51,12 @@ function extractEditor(options) { // Add extra .d.ts files from `node_modules/@types/` if (Array.isArray(options.compilerOptions?.types)) { options.compilerOptions.types.forEach((type) => { - options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + if (type === '@webgpu/types') { + options.typings.push(`../node_modules/${type}/dist/index.d.ts`); + } + else { + options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + } }); } const result = tss.shake(options); diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 546afcbb589..9563cd6670b 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -59,7 +59,11 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str // Add extra .d.ts files from `node_modules/@types/` if (Array.isArray(options.compilerOptions?.types)) { options.compilerOptions.types.forEach((type: string) => { - options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + if (type === '@webgpu/types') { + options.typings.push(`../node_modules/${type}/dist/index.d.ts`); + } else { + options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + } }); } From a1c4f50f402439c733bc049ce01f10b57714a577 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:52:38 -0700 Subject: [PATCH 191/286] Skip problematic test --- .../browser/view/gpu/atlas/textureAtlasAllocator.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 3c34b51a492..49c51836d54 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -67,11 +67,12 @@ suite('TextureAtlasAllocator', () => { suite('shared tests', () => { for (const { name, initAllocator } of allocatorDefinitions) { - test('single allocation', () => { + test(`(${name}) single allocation`, () => { const { canvas, allocator } = initAllocator(2, 2); assertIsValidGlyph(allocator.allocate(pixel1x1), canvas); }); - test(`(${name}) glyph too large for canvas`, () => { + // Skipping because it fails unexpectedly on web only when asserting the error message + test.skip(`(${name}) glyph too large for canvas`, () => { const { allocator } = initAllocator(1, 1); throws(() => allocateAndAssert(allocator, pixel2x1, undefined), new Error('Glyph is too large for the atlas page')); }); From 70849c674dfe2ced68a9bee7b556126a3b23439f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:10:48 +0200 Subject: [PATCH 192/286] =?UTF-8?q?SCM=20Graph=20-=20=F0=9F=92=84=20switch?= =?UTF-8?q?=20to=20waitForState=20in=20favor=20of=20an=20autorun=20(#22800?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contrib/scm/browser/scmHistoryViewPane.ts | 160 +++++++++--------- 1 file changed, 78 insertions(+), 82 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 9abd6a20719..ea5a76d79f6 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -16,7 +16,7 @@ import { fromNow } from '../../../../base/common/date.js'; import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, autorunWithStoreHandleChanges, derived, derivedOpts, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStoreHandleChanges, derived, derivedOpts, IObservable, observableValue, waitForState } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -48,7 +48,7 @@ import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/ac import { delta, groupBy, tail } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; -import { constObservable, derivedConstOnceDefined, latestChangedValue, observableFromEvent, runOnChange } from '../../../../base/common/observableInternal/utils.js'; +import { constObservable, latestChangedValue, observableFromEvent, runOnChange } from '../../../../base/common/observableInternal/utils.js'; import { ContextKeys } from './scmViewPane.js'; import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IDropdownMenuActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; @@ -1047,12 +1047,12 @@ export class SCMHistoryViewPane extends ViewPane { this._createTree(this._treeContainer); - this.onDidChangeBodyVisibility(visible => { + this.onDidChangeBodyVisibility(async visible => { if (visible) { this._treeViewModel = this.instantiationService.createInstance(SCMHistoryViewModel); this._visibilityDisposables.add(this._treeViewModel); - const firstRepository = derivedConstOnceDefined(this, reader => { + const firstRepositoryInitialized = derived(this, reader => { const repository = this._treeViewModel.repository.read(reader); const historyProvider = repository?.provider.historyProvider.read(reader); const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.read(reader); @@ -1060,100 +1060,96 @@ export class SCMHistoryViewPane extends ViewPane { return currentHistoryItemGroup !== undefined ? repository : undefined; }); - this._visibilityDisposables.add(autorunWithStore(async (reader, store) => { - const repository = firstRepository.read(reader); - if (!repository) { - return; - } + // Wait for first repository to be initialized + await waitForState(firstRepositoryInitialized); - this._treeOperationSequencer.queue(async () => { - await this._tree.setInput(this._treeViewModel); - this._tree.scrollTop = 0; - }); + this._treeOperationSequencer.queue(async () => { + await this._tree.setInput(this._treeViewModel); + this._tree.scrollTop = 0; + }); - // Repository change - store.add( - autorunWithStoreHandleChanges<{ refresh: boolean }>({ + // Repository change + this._visibilityDisposables.add( + autorunWithStoreHandleChanges<{ refresh: boolean }>({ + owner: this, + createEmptyChangeSummary: () => ({ refresh: false }), + handleChange(_, changeSummary) { + changeSummary.refresh = true; + return true; + }, + }, (reader, changeSummary, store) => { + const repository = this._treeViewModel.repository.read(reader); + const historyProvider = repository?.provider.historyProvider.read(reader); + if (!repository || !historyProvider) { + return; + } + + // Update context + this._scmProviderCtx.set(repository.provider.contextValue); + + // Checkout, Commit, and Publish + const historyItemGroup = derivedOpts<{ id: string; revision?: string; remoteId?: string } | undefined>({ owner: this, - createEmptyChangeSummary: () => ({ refresh: false }), - handleChange(_, changeSummary) { - changeSummary.refresh = true; - return true; - }, - }, (reader, changeSummary, store) => { - const repository = this._treeViewModel.repository.read(reader); - const historyProvider = repository?.provider.historyProvider.read(reader); - if (!repository || !historyProvider) { - return; - } + equalsFn: structuralEquals + }, reader => { + const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup.read(reader); + return currentHistoryItemGroup ? { + id: currentHistoryItemGroup.id, + revision: currentHistoryItemGroup.revision, + remoteId: currentHistoryItemGroup.remote?.id + } : undefined; + }); - // Update context - this._scmProviderCtx.set(repository.provider.contextValue); + // Fetch, Push + const historyItemRemoteRevision = derived(reader => { + return historyProvider.currentHistoryItemGroup.read(reader)?.remote?.revision; + }); - // Checkout, Commit, and Publish - const historyItemGroup = derivedOpts<{ id: string; revision?: string; remoteId?: string } | undefined>({ + // HistoryItemGroup change + store.add( + autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({ owner: this, - equalsFn: structuralEquals - }, reader => { - const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup.read(reader); - return currentHistoryItemGroup ? { - id: currentHistoryItemGroup.id, - revision: currentHistoryItemGroup.revision, - remoteId: currentHistoryItemGroup.remote?.id - } : undefined; - }); + createEmptyChangeSummary: () => ({ refresh: false }), + handleChange(context, changeSummary) { + changeSummary.refresh = context.didChange(historyItemRemoteRevision) ? 'ifScrollTop' : true; + return true; + }, + }, (reader, changeSummary) => { + const historyItemGroupValue = historyItemGroup.read(reader); + const historyItemRemoteRevisionValue = historyItemRemoteRevision.read(reader); - // Fetch, Push - const historyItemRemoteRevision = derived(reader => { - return historyProvider.currentHistoryItemGroup.read(reader)?.remote?.revision; - }); + if ((!historyItemGroupValue && !historyItemRemoteRevisionValue) || changeSummary.refresh === false) { + return; + } - // HistoryItemGroup change - store.add( - autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({ - owner: this, - createEmptyChangeSummary: () => ({ refresh: false }), - handleChange(context, changeSummary) { - changeSummary.refresh = context.didChange(historyItemRemoteRevision) ? 'ifScrollTop' : true; - return true; - }, - }, (reader, changeSummary) => { - const historyItemGroupValue = historyItemGroup.read(reader); - const historyItemRemoteRevisionValue = historyItemRemoteRevision.read(reader); + if (changeSummary.refresh === true) { + this.refresh(); + return; + } - if ((!historyItemGroupValue && !historyItemRemoteRevisionValue) || changeSummary.refresh === false) { - return; - } - - if (changeSummary.refresh === true) { + if (changeSummary.refresh === 'ifScrollTop') { + // Remote revision changes can occur as a result of a user action (Fetch, Push) but + // it can also occur as a result of background action (Auto Fetch). If the tree is + // scrolled to the top, we can safely refresh the tree. + if (this._tree.scrollTop === 0) { this.refresh(); return; } - if (changeSummary.refresh === 'ifScrollTop') { - // Remote revision changes can occur as a result of a user action (Fetch, Push) but - // it can also occur as a result of background action (Auto Fetch). If the tree is - // scrolled to the top, we can safely refresh the tree. - if (this._tree.scrollTop === 0) { - this.refresh(); - return; - } - - // Show the "Outdated" badge on the view - this._repositoryOutdated.set(true, undefined); - } - })); - - // HistoryItemRefs filter changed - store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => { - this.refresh(); + // Show the "Outdated" badge on the view + this._repositoryOutdated.set(true, undefined); + } })); - if (changeSummary.refresh) { - this.refresh(); - } + // HistoryItemRefs filter changed + store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => { + this.refresh(); })); - })); + + if (changeSummary.refresh) { + this.refresh(); + } + })); } else { this._visibilityDisposables.clear(); } From 4d221c6b85608abf3df9e6c6ac91ffb3285dff78 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 9 Sep 2024 08:52:50 -0700 Subject: [PATCH 193/286] cli: reapply "code server-web when offline" --- cli/Cargo.toml | 1 + cli/src/auth.rs | 2 +- cli/src/commands/serve_web.rs | 73 ++++++++++++++++++++++++++++------ cli/src/download_cache.rs | 8 +++- cli/src/tunnels/code_server.rs | 2 +- package.json | 2 +- 6 files changed, 71 insertions(+), 17 deletions(-) diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b820ffcc50f..2907ff3d7e7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -81,4 +81,5 @@ codegen-units = 1 [features] default = [] +vsda = [] vscode-encrypt = [] diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 2d9162c5483..51942c96c75 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -723,7 +723,7 @@ impl Auth { match &init_code_json.message { Some(m) => self.log.result(m), - None => self.log.result(&format!( + None => self.log.result(format!( "To grant access to the server, please log into {} and use code {}", init_code_json.verification_uri, init_code_json.user_code )), diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index d8d2a49bb1a..4acb9a18c73 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -15,7 +15,7 @@ use std::time::{Duration, Instant}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::pin; +use tokio::{pin, time}; use crate::async_pipe::{ get_socket_name, get_socket_rw_stream, listen_socket_rw_stream, AsyncPipe, @@ -50,7 +50,7 @@ const SERVER_IDLE_TIMEOUT_SECS: u64 = 60 * 60; /// (should be large enough to basically never happen) const SERVER_ACTIVE_TIMEOUT_SECS: u64 = SERVER_IDLE_TIMEOUT_SECS * 24 * 30 * 12; /// How long to cache the "latest" version we get from the update service. -const RELEASE_CACHE_SECS: u64 = 60 * 60; +const RELEASE_CHECK_INTERVAL: u64 = 60 * 60; /// Number of bytes for the secret keys. See workbench.ts for their usage. const SECRET_KEY_BYTES: usize = 32; @@ -86,7 +86,11 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result = ConnectionManager::new(&ctx, platform, args.clone()); + let update_check_interval = 3600; + cm.clone() + .start_update_checker(Duration::from_secs(update_check_interval)); + let key = get_server_key_half(&ctx.paths); let make_svc = move || { let ctx = HandleContext { @@ -175,7 +179,7 @@ async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response r, Err(e) => { error!(ctx.log, "error getting latest version: {}", e); @@ -538,21 +542,67 @@ impl ConnectionManager { pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { let base_path = normalize_base_path(args.server_base_path.as_deref().unwrap_or_default()); + let cache = DownloadCache::new(ctx.paths.web_server_storage()); + let target_kind = TargetKind::Web; + + let quality = VSCODE_CLI_QUALITY.map_or(Quality::Stable, |q| match Quality::try_from(q) { + Ok(q) => q, + Err(_) => Quality::Stable, + }); + + let latest_version = tokio::sync::Mutex::new(cache.get().first().map(|latest_commit| { + ( + Instant::now() - Duration::from_secs(RELEASE_CHECK_INTERVAL), + Release { + name: String::from("0.0.0"), // Version information not stored on cache + commit: latest_commit.clone(), + platform, + target: target_kind, + quality, + }, + ) + })); + Arc::new(Self { platform, args, base_path, log: ctx.log.clone(), - cache: DownloadCache::new(ctx.paths.web_server_storage()), + cache, update_service: UpdateService::new( ctx.log.clone(), Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())), ), state: ConnectionStateMap::default(), - latest_version: tokio::sync::Mutex::default(), + latest_version, }) } + // spawns a task that checks for updates every n seconds duration + pub fn start_update_checker(self: Arc, duration: Duration) { + tokio::spawn(async move { + let mut interval = time::interval(duration); + loop { + interval.tick().await; + + if let Err(e) = self.get_latest_release().await { + warning!(self.log, "error getting latest version: {}", e); + } + } + }); + } + + // Returns the latest release from the cache, if one exists. + pub async fn get_release_from_cache(&self) -> Result { + let latest = self.latest_version.lock().await; + if let Some((_, release)) = &*latest { + return Ok(release.clone()); + } + + drop(latest); + self.get_latest_release().await + } + /// Gets a connection to a server version pub async fn get_connection( &self, @@ -571,11 +621,7 @@ impl ConnectionManager { pub async fn get_latest_release(&self) -> Result { let mut latest = self.latest_version.lock().await; let now = Instant::now(); - if let Some((checked_at, release)) = &*latest { - if checked_at.elapsed() < Duration::from_secs(RELEASE_CACHE_SECS) { - return Ok(release.clone()); - } - } + let target_kind = TargetKind::Web; let quality = VSCODE_CLI_QUALITY .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) @@ -585,13 +631,14 @@ impl ConnectionManager { let release = self .update_service - .get_latest_commit(self.platform, TargetKind::Web, quality) + .get_latest_commit(self.platform, target_kind, quality) .await .map_err(|e| CodeError::UpdateCheckFailed(e.to_string())); // If the update service is unavailable and we have stale data, use that - if let (Err(e), Some((_, previous))) = (&release, &*latest) { + if let (Err(e), Some((_, previous))) = (&release, latest.clone()) { warning!(self.log, "error getting latest release, using stale: {}", e); + *latest = Some((now, previous.clone())); return Ok(previous.clone()); } diff --git a/cli/src/download_cache.rs b/cli/src/download_cache.rs index d3f05d2237f..cd02b02d75a 100644 --- a/cli/src/download_cache.rs +++ b/cli/src/download_cache.rs @@ -20,6 +20,7 @@ const KEEP_LRU: usize = 5; const STAGING_SUFFIX: &str = ".staging"; const RENAME_ATTEMPTS: u32 = 20; const RENAME_DELAY: std::time::Duration = std::time::Duration::from_millis(200); +const PERSISTED_STATE_FILE_NAME: &str = "lru.json"; #[derive(Clone)] pub struct DownloadCache { @@ -30,11 +31,16 @@ pub struct DownloadCache { impl DownloadCache { pub fn new(path: PathBuf) -> DownloadCache { DownloadCache { - state: PersistedState::new(path.join("lru.json")), + state: PersistedState::new(path.join(PERSISTED_STATE_FILE_NAME)), path, } } + /// Gets the value stored on the state + pub fn get(&self) -> Vec { + self.state.load() + } + /// Gets the download cache path. Names of cache entries can be formed by /// joining them to the path. pub fn path(&self) -> &Path { diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 0579f8ef0dc..465f6e24230 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -674,7 +674,7 @@ where let write_line = |line: &str| -> std::io::Result<()> { if let Some(mut f) = log_file.as_ref() { f.write_all(line.as_bytes())?; - f.write_all(&[b'\n'])?; + f.write_all(b"\n")?; } if write_directly { println!("{}", line); diff --git a/package.json b/package.json index 4104623cf41..149a219512b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.94.0", - "distro": "36c6d77f96e54b1ad2233cd24fed8e8f08d5a388", + "distro": "fcaeb73de7ac6ff11a3e732c63987e01b88341e7", "author": { "name": "Microsoft Corporation" }, From 2e4bae90c63c2d990061d450f359ce5ab0ac3eb4 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 9 Sep 2024 08:53:30 -0700 Subject: [PATCH 194/286] cli: update rs to avoid arm64 bug --- build/azure-pipelines/cli/install-rust-posix.yml | 2 +- build/azure-pipelines/cli/install-rust-win32.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines/cli/install-rust-posix.yml b/build/azure-pipelines/cli/install-rust-posix.yml index 89867143938..fee56e028f7 100644 --- a/build/azure-pipelines/cli/install-rust-posix.yml +++ b/build/azure-pipelines/cli/install-rust-posix.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.77 + default: 1.81 - name: targets default: [] type: object diff --git a/build/azure-pipelines/cli/install-rust-win32.yml b/build/azure-pipelines/cli/install-rust-win32.yml index 22fba8d7f6a..45a1cfd188e 100644 --- a/build/azure-pipelines/cli/install-rust-win32.yml +++ b/build/azure-pipelines/cli/install-rust-win32.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.77 + default: 1.81 - name: targets default: [] type: object From 1349397cf767bda1680b22c095cf18084e71cf43 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 9 Sep 2024 09:28:32 -0700 Subject: [PATCH 195/286] Remove vscode-nls-dev dependency (#228002) This was when extensions used the old localization stuff. None of this is used anymore. --- build/gulpfile.extensions.js | 5 - extensions/shared.webpack.config.js | 15 +- package-lock.json | 562 ---------------------------- package.json | 1 - 4 files changed, 2 insertions(+), 581 deletions(-) diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index f00fa7a1ac3..4f745aebdc7 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -101,7 +101,6 @@ const tasks = compilations.map(function (tsconfigFile) { } function createPipeline(build, emitError, transpileOnly) { - const nlsDev = require('vscode-nls-dev'); const tsb = require('./lib/tsb'); const sourcemaps = require('gulp-sourcemaps'); @@ -126,7 +125,6 @@ const tasks = compilations.map(function (tsconfigFile) { .pipe(tsFilter) .pipe(util.loadSourcemaps()) .pipe(compilation()) - .pipe(build ? nlsDev.rewriteLocalizeCalls() : es.through()) .pipe(build ? util.stripSourceMappingURL() : es.through()) .pipe(sourcemaps.write('.', { sourceMappingURL: !build ? null : f => `${baseUrl}/${f.relative}.map`, @@ -136,9 +134,6 @@ const tasks = compilations.map(function (tsconfigFile) { sourceRoot: '../src/', })) .pipe(tsFilter.restore) - .pipe(build ? nlsDev.bundleMetaDataFiles(headerId, headerOut) : es.through()) - // Filter out *.nls.json file. We needed them only to bundle meta data file. - .pipe(filter(['**', '!**/*.nls.json'], { dot: true })) .pipe(reporter.end(emitError)); return es.duplex(input, output); diff --git a/extensions/shared.webpack.config.js b/extensions/shared.webpack.config.js index 4c258280812..6e5b9fd95ac 100644 --- a/extensions/shared.webpack.config.js +++ b/extensions/shared.webpack.config.js @@ -12,7 +12,6 @@ const path = require('path'); const fs = require('fs'); const merge = require('merge-options'); const CopyWebpackPlugin = require('copy-webpack-plugin'); -const { NLSBundlePlugin } = require('vscode-nls-dev/lib/webpack-bundler'); const { DefinePlugin, optimize } = require('webpack'); const tsLoaderOptions = { @@ -40,13 +39,6 @@ function withNodeDefaults(/**@type WebpackConfig & { context: string }*/extConfi test: /\.ts$/, exclude: /node_modules/, use: [{ - // vscode-nls-dev loader: - // * rewrite nls-calls - loader: 'vscode-nls-dev/lib/webpack-loader', - options: { - base: path.join(extConfig.context, 'src') - } - }, { // configure TypeScript loader: // * enable sources maps for end-to-end source maps loader: 'ts-loader', @@ -97,8 +89,7 @@ function nodePlugins(context) { patterns: [ { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } ] - }), - new NLSBundlePlugin(id) + }) ]; } /** @@ -196,9 +187,7 @@ function browserPlugins(context) { 'process.platform': JSON.stringify('web'), 'process.env': JSON.stringify({}), 'process.env.BROWSER_ENV': JSON.stringify('true') - }), - // TODO: bring this back once vscode-nls-dev supports browser - // new NLSBundlePlugin(id) + }) ]; } diff --git a/package-lock.json b/package-lock.json index 88d1a66925e..21f08c77576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -156,7 +156,6 @@ "tslib": "^2.6.3", "typescript": "^5.7.0-dev.20240903", "util": "^0.12.4", - "vscode-nls-dev": "^3.3.1", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", @@ -6699,119 +6698,6 @@ "node": ">=0.8.x" } }, - "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/execa/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/execa/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "node_modules/execa/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -11119,15 +11005,6 @@ "node": ">= 12" } }, - "node_modules/is": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", - "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU= sha512-NgGnzF+/wucMxle6i32obg/UjUqQruwlJUtjBpDEMXNq8vPLuaf28U4q+yes1I6J89FoErr7kDtBGICoNaY7rQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -12565,18 +12442,6 @@ "node": ">=0.10.0" } }, - "node_modules/map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -12847,20 +12712,6 @@ "node": ">= 0.6" } }, - "node_modules/mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -13872,27 +13723,6 @@ "which": "bin/which" } }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -14401,33 +14231,6 @@ "node": ">=8" } }, - "node_modules/p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -17746,15 +17549,6 @@ "node": ">=0.10.0" } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -19258,362 +19052,6 @@ "node": ">= 0.10" } }, - "node_modules/vscode-nls-dev": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/vscode-nls-dev/-/vscode-nls-dev-3.3.1.tgz", - "integrity": "sha512-fug18D7CXb8pv8JoQ0D0JmZaIYDQoKLiyZxkAy5P8Cln/FwlNsdzwQILDph62EdGY5pvsJ2Jd1T5qgHAExe/tg==", - "dev": true, - "dependencies": { - "ansi-colors": "^3.2.3", - "clone": "^2.1.1", - "event-stream": "^3.3.4", - "fancy-log": "^1.3.3", - "glob": "^7.1.2", - "iconv-lite": "^0.4.19", - "is": "^3.2.1", - "source-map": "^0.6.1", - "typescript": "^2.6.2", - "vinyl": "^2.1.0", - "xml2js": "^0.4.19", - "yargs": "^13.2.4" - }, - "bin": { - "vscl": "lib/vscl.js" - } - }, - "node_modules/vscode-nls-dev/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-nls-dev/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/vscode-nls-dev/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/vscode-nls-dev/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/vscode-nls-dev/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/vscode-nls-dev/node_modules/invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-nls-dev/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-nls-dev/node_modules/lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "dependencies": { - "invert-kv": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "dependencies": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vscode-nls-dev/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-nls-dev/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vscode-nls-dev/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vscode-nls-dev/node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true - }, - "node_modules/vscode-nls-dev/node_modules/yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", diff --git a/package.json b/package.json index 149a219512b..138ae5f7954 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,6 @@ "tslib": "^2.6.3", "typescript": "^5.7.0-dev.20240903", "util": "^0.12.4", - "vscode-nls-dev": "^3.3.1", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", From 287757bd0a4ce8ed4669326e1257bb619dcc005b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Mon, 9 Sep 2024 09:43:24 -0700 Subject: [PATCH 196/286] add status bar pinned action aria label (#227845) --- .../languageStatus/browser/languageStatus.contribution.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 36e4187a4b2..28a38f4a198 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -331,17 +331,19 @@ class LanguageStatus { // -- pin const actionBar = new ActionBar(right, { hoverDelegate: nativeHoverDelegate }); + const actionLabel: string = isPinned ? localize('unpin', "Remove from Status Bar") : localize('pin', "Add to Status Bar"); + actionBar.setAriaLabel(actionLabel); store.add(actionBar); let action: Action; if (!isPinned) { - action = new Action('pin', localize('pin', "Add to Status Bar"), ThemeIcon.asClassName(Codicon.pin), true, () => { + action = new Action('pin', actionLabel, ThemeIcon.asClassName(Codicon.pin), true, () => { this._dedicated.add(status.id); this._statusBarService.updateEntryVisibility(status.id, true); this._update(); this._storeState(); }); } else { - action = new Action('unpin', localize('unpin', "Remove from Status Bar"), ThemeIcon.asClassName(Codicon.pinned), true, () => { + action = new Action('unpin', actionLabel, ThemeIcon.asClassName(Codicon.pinned), true, () => { this._dedicated.delete(status.id); this._statusBarService.updateEntryVisibility(status.id, false); this._update(); From dc9412125d4e0a480593962ae2687e74e64af728 Mon Sep 17 00:00:00 2001 From: Raymond Zhao <7199958+rzhao271@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:59:31 -0700 Subject: [PATCH 197/286] chore: bump micromatch (#228005) --- extensions/npm/package-lock.json | 20 +++++++++++--------- extensions/package-lock.json | 9 +++++---- package-lock.json | 9 +++++---- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/extensions/npm/package-lock.json b/extensions/npm/package-lock.json index 21a7c83df8e..4ee7ee7a0e3 100644 --- a/extensions/npm/package-lock.json +++ b/extensions/npm/package-lock.json @@ -193,15 +193,16 @@ } }, "node_modules/micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, "node_modules/minimatch": { @@ -252,9 +253,10 @@ } }, "node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, diff --git a/extensions/package-lock.json b/extensions/package-lock.json index 63476fbb316..792ee4281c2 100644 --- a/extensions/package-lock.json +++ b/extensions/package-lock.json @@ -547,12 +547,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { diff --git a/package-lock.json b/package-lock.json index 21f08c77576..ea7a1817800 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12802,11 +12802,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { From 3a89990b2c4d0df8b98195b4e4d016596dbcb762 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 02:11:56 +0900 Subject: [PATCH 198/286] Bump yargs-parser and yargs (#227800) Bumps [yargs-parser](https://github.com/yargs/yargs-parser) and [yargs](https://github.com/yargs/yargs). These dependencies needed to be updated together. Updates `yargs-parser` from 13.1.1 to 21.1.1 - [Release notes](https://github.com/yargs/yargs-parser/releases) - [Changelog](https://github.com/yargs/yargs-parser/blob/main/CHANGELOG.md) - [Commits](https://github.com/yargs/yargs-parser/compare/v13.1.1...yargs-parser-v21.1.1) Updates `yargs` from 7.1.1 to 17.7.2 - [Release notes](https://github.com/yargs/yargs/releases) - [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md) - [Commits](https://github.com/yargs/yargs/commits/v17.7.2) --- updated-dependencies: - dependency-name: yargs-parser dependency-type: indirect - dependency-name: yargs dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea7a1817800..f4a1e6017c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8731,7 +8731,7 @@ "node_modules/gulp-cli/node_modules/camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo= sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -8833,9 +8833,9 @@ "dev": true }, "node_modules/gulp-cli/node_modules/yargs": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", - "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", "dev": true, "dependencies": { "camelcase": "^3.0.0", @@ -8850,13 +8850,13 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "5.0.0-security.0" + "yargs-parser": "^5.0.1" } }, "node_modules/gulp-cli/node_modules/yargs-parser": { - "version": "5.0.0-security.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", - "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", "dev": true, "dependencies": { "camelcase": "^3.0.0", From dcc7af186a48d966210e12c41a0050deb1511ff0 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 9 Sep 2024 19:18:38 +0200 Subject: [PATCH 199/286] Increase the max width of hover to a higher value (#227941) increasing the width to a higher value --- src/vs/editor/contrib/hover/browser/contentHoverWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts index 2ac48a73c46..5dc30190980 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts @@ -291,7 +291,7 @@ export class ContentHoverWidget extends ResizableContentWidget { private _updateMaxDimensions() { const height = Math.max(this._editor.getLayoutInfo().height / 4, 250, ContentHoverWidget._lastDimensions.height); - const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 500, ContentHoverWidget._lastDimensions.width); + const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 750, ContentHoverWidget._lastDimensions.width); this._setHoverWidgetMaxDimensions(width, height); } From 7ca44a4a047256bfa306265a754ffdee7cb8b9fe Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:33:16 -0700 Subject: [PATCH 200/286] Focus xterm in integration tests --- .../test/browser/terminalSuggestAddon.integrationTest.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts index 01cc24341c9..c5c983f05ac 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts @@ -63,7 +63,7 @@ interface IRecordedSessionResizeEvent { rows: number; } -suite('Terminal Contrib Suggest Recordings', () => { +suite.only('Terminal Contrib Suggest Recordings', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let xterm: Terminal; @@ -114,6 +114,8 @@ suite('Terminal Contrib Suggest Recordings', () => { xterm.loadAddon(shellIntegrationAddon); xterm.loadAddon(suggestAddon); + + xterm.focus(); }); for (const testCase of recordedTestCases) { From 6092f245833f7c7431825878819766716684a5b0 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Mon, 9 Sep 2024 11:39:52 -0700 Subject: [PATCH 201/286] WordHighlighter: fix queries against disposed models + blur logic (#227607) * wordHighlighter: fix queries against disposed models (use URIs) * add blur logic + clear old workerReq logic from when highlighting was only singleFile * imports * dispose models that we ref for the occurrence request * WHOOPS don't do that, it destroys every editor hah --- .../browser/wordHighlighter.ts | 162 +++++++++++------- .../browser/view/cellParts/codeCell.ts | 19 +- 2 files changed, 108 insertions(+), 73 deletions(-) diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index 656b2cf8ef9..4af05cb5aa3 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -10,6 +10,13 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; +import { matchesScheme, Schemas } from '../../../../base/common/network.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IActiveCodeEditor, ICodeEditor, isDiffEditor } from '../../../browser/editorBrowser.js'; import { EditorAction, EditorContributionInstantiation, IActionOptions, registerEditorAction, registerEditorContribution, registerModelAndPositionCommand } from '../../../browser/editorExtensions.js'; import { ICodeEditorService } from '../../../browser/services/codeEditorService.js'; @@ -21,20 +28,15 @@ import { IWordAtPosition } from '../../../common/core/wordHelper.js'; import { CursorChangeReason, ICursorPositionChangedEvent } from '../../../common/cursorEvents.js'; import { IDiffEditor, IEditorContribution, IEditorDecorationsCollection } from '../../../common/editorCommon.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; +import { registerEditorFeature } from '../../../common/editorFeatures.js'; import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js'; import { DocumentHighlight, DocumentHighlightProvider, MultiDocumentHighlightProvider } from '../../../common/languages.js'; +import { score } from '../../../common/languageSelector.js'; import { IModelDeltaDecoration, ITextModel, shouldSynchronizeModel } from '../../../common/model.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; +import { ITextModelService } from '../../../common/services/resolverService.js'; import { getHighlightDecorationOptions } from './highlightDecorations.js'; -import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { matchesScheme, Schemas } from '../../../../base/common/network.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { score } from '../../../common/languageSelector.js'; -import { isEqual } from '../../../../base/common/resources.js'; import { TextualMultiDocumentHighlightFeature } from './textualHighlightProvider.js'; -import { registerEditorFeature } from '../../../common/editorFeatures.js'; const ctxHasWordHighlights = new RawContextKey('hasWordHighlights', false); @@ -58,7 +60,7 @@ export function getOccurrencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, wordSeparators: string, token: CancellationToken, otherModels: ITextModel[]): Promise | null | undefined> { +export function getOccurrencesAcrossMultipleModels(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken, otherModels: ITextModel[]): Promise | null | undefined> { const orderedByScore = registry.ordered(model); // in order of score ask the occurrences provider @@ -84,10 +86,9 @@ interface IOccurenceAtPositionRequest { interface IWordHighlighterQuery { modelInfo: { - model: ITextModel; + modelURI: URI; selection: Selection; } | null; - readonly word: IWordAtPosition | null; } abstract class OccurenceAtPositionRequest implements IOccurenceAtPositionRequest { @@ -175,7 +176,7 @@ class MultiModelOccurenceRequest extends OccurenceAtPositionRequest { } protected override _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Promise> { - return getOccurrencesAcrossMultipleModels(this._providers, model, selection.getPosition(), wordSeparators, token, this._otherModels).then(value => { + return getOccurrencesAcrossMultipleModels(this._providers, model, selection.getPosition(), token, this._otherModels).then(value => { if (!value) { return new ResourceMap(); } @@ -185,11 +186,11 @@ class MultiModelOccurenceRequest extends OccurenceAtPositionRequest { } -function computeOccurencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string): IOccurenceAtPositionRequest { +function computeOccurencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, wordSeparators: string): IOccurenceAtPositionRequest { return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators, registry); } -function computeOccurencesMultiModel(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string, otherModels: ITextModel[]): IOccurenceAtPositionRequest { +function computeOccurencesMultiModel(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, wordSeparators: string, otherModels: ITextModel[]): IOccurenceAtPositionRequest { return new MultiModelOccurenceRequest(model, selection, wordSeparators, registry, otherModels); } @@ -207,7 +208,10 @@ class WordHighlighter { private readonly model: ITextModel; private readonly decorations: IEditorDecorationsCollection; private readonly toUnhook = new DisposableStore(); + + private readonly textModelService: ITextModelService; private readonly codeEditorService: ICodeEditorService; + private occurrencesHighlight: string; private workerRequestTokenId: number = 0; @@ -221,20 +225,31 @@ class WordHighlighter { private readonly _hasWordHighlights: IContextKey; private _ignorePositionChangeEvent: boolean; - private readonly runDelayer: Delayer = this.toUnhook.add(new Delayer(50)); + private readonly runDelayer: Delayer = this.toUnhook.add(new Delayer(25)); private static storedDecorationIDs: ResourceMap = new ResourceMap(); private static query: IWordHighlighterQuery | null = null; - constructor(editor: IActiveCodeEditor, providers: LanguageFeatureRegistry, multiProviders: LanguageFeatureRegistry, contextKeyService: IContextKeyService, @ICodeEditorService codeEditorService: ICodeEditorService) { + constructor( + editor: IActiveCodeEditor, + providers: LanguageFeatureRegistry, + multiProviders: LanguageFeatureRegistry, + contextKeyService: IContextKeyService, + @ITextModelService textModelService: ITextModelService, + @ICodeEditorService codeEditorService: ICodeEditorService, + ) { this.editor = editor; this.providers = providers; this.multiDocumentProviders = multiProviders; + this.codeEditorService = codeEditorService; + this.textModelService = textModelService; + this._hasWordHighlights = ctxHasWordHighlights.bindTo(contextKeyService); this._ignorePositionChangeEvent = false; this.occurrencesHighlight = this.editor.getOption(EditorOption.occurrencesHighlight); this.model = this.editor.getModel(); + this.toUnhook.add(editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { if (this._ignorePositionChangeEvent) { // We are changing the position => ignore this event @@ -267,10 +282,8 @@ class WordHighlighter { this.toUnhook.add(editor.onDidChangeModel((e) => { if (!e.newModelUrl && e.oldModelUrl) { this._stopSingular(); - } else { - if (WordHighlighter.query) { - this._run(); - } + } else if (WordHighlighter.query) { + this._run(); } })); this.toUnhook.add(editor.onDidChangeConfiguration((e) => { @@ -282,7 +295,7 @@ class WordHighlighter { this._stopAll(); break; case 'singleFile': - this._stopAll(WordHighlighter.query?.modelInfo?.model); + this._stopAll(WordHighlighter.query?.modelInfo?.modelURI); break; case 'multiFile': if (WordHighlighter.query) { @@ -295,6 +308,19 @@ class WordHighlighter { } } })); + this.toUnhook.add(editor.onDidBlurEditorWidget(() => { + // logic is as follows + // - didBlur => active null => stopall + // - didBlur => active nb => if this.editor is notebook, do nothing (new cell, so we don't want to stopAll) + // active nb => if this.editor is NOT nb, stopAll + + const activeEditor = this.codeEditorService.getFocusedCodeEditor(); + if (!activeEditor) { // clicked into nb cell list, outline, terminal, etc + this._stopAll(); + } else if (activeEditor.getModel()?.uri.scheme === Schemas.vscodeNotebookCell && this.editor.getModel()?.uri.scheme !== Schemas.vscodeNotebookCell) { // switched tabs from non-nb to nb + this._stopAll(); + } + })); this.decorations = this.editor.createDecorationsCollection(); this.workerRequestTokenId = 0; @@ -396,12 +422,12 @@ class WordHighlighter { } } - private _removeAllDecorations(preservedModel?: ITextModel): void { + private _removeAllDecorations(preservedModel?: URI): void { const currentEditors = this.codeEditorService.listCodeEditors(); const deleteURI = []; // iterate over editors and store models in currentModels for (const editor of currentEditors) { - if (!editor.hasModel() || isEqual(editor.getModel().uri, preservedModel?.uri)) { + if (!editor.hasModel() || isEqual(editor.getModel().uri, preservedModel)) { continue; } @@ -435,7 +461,7 @@ class WordHighlighter { this._removeSingleDecorations(); if (this.editor.hasTextFocus()) { - if (this.editor.getModel()?.uri.scheme !== Schemas.vscodeNotebookCell && WordHighlighter.query?.modelInfo?.model.uri.scheme !== Schemas.vscodeNotebookCell) { // clear query if focused non-nb editor + if (this.editor.getModel()?.uri.scheme !== Schemas.vscodeNotebookCell && WordHighlighter.query?.modelInfo?.modelURI.scheme !== Schemas.vscodeNotebookCell) { // clear query if focused non-nb editor WordHighlighter.query = null; this._run(); // TODO: @Yoyokrazy -- investigate why we need a full rerun here. likely addressed a case/patch in the first iteration of this feature } else { // remove modelInfo to account for nb cell being disposed @@ -464,7 +490,7 @@ class WordHighlighter { } } - private _stopAll(preservedModel?: ITextModel): void { + private _stopAll(preservedModel?: URI): void { // Remove any existing decorations // TODO: @Yoyokrazy -- this triggers as notebooks scroll, causing highlights to disappear momentarily. // maybe a nb type check? @@ -582,9 +608,8 @@ class WordHighlighter { return currentModels; } - private _run(multiFileConfigChange?: boolean): void { + private async _run(multiFileConfigChange?: boolean): Promise { - let workerRequestIsValid; const hasTextFocus = this.editor.hasTextFocus(); if (!hasTextFocus) { // new nb cell scrolled in, didChangeModel fires @@ -615,41 +640,18 @@ class WordHighlighter { return; } - // All the effort below is trying to achieve this: - // - when cursor is moved to a word, trigger immediately a findOccurrences request - // - 250ms later after the last cursor move event, render the occurrences - // - no flickering! - workerRequestIsValid = (this.workerRequest && this.workerRequest.isValid(this.model, editorSelection, this.decorations)); - WordHighlighter.query = { modelInfo: { - model: this.model, + modelURI: this.model.uri, selection: editorSelection, - }, - word: word + } }; } - // There are 4 cases: - // a) old workerRequest is valid & completed, renderDecorationsTimer fired - // b) old workerRequest is valid & completed, renderDecorationsTimer not fired - // c) old workerRequest is valid, but not completed - // d) old workerRequest is not valid - - // For a) no action is needed - // For c), member 'lastCursorPositionChangeTime' will be used when installing the timer so no action is needed this.lastCursorPositionChangeTime = (new Date()).getTime(); - if (workerRequestIsValid) { - if (this.workerRequestCompleted && this.renderDecorationsTimer !== -1) { - // case b) - // Delay the firing of renderDecorationsTimer by an extra 250 ms - clearTimeout(this.renderDecorationsTimer); - this.renderDecorationsTimer = -1; - this._beginRenderDecorations(); - } - } else if (isEqual(this.editor.getModel().uri, WordHighlighter.query.modelInfo?.model.uri)) { // only trigger new worker requests from the primary model that initiated the query + if (isEqual(this.editor.getModel().uri, WordHighlighter.query.modelInfo?.modelURI)) { // only trigger new worker requests from the primary model that initiated the query // case d) // check if the new queried word is contained in the range of a stored decoration for this model @@ -664,7 +666,7 @@ class WordHighlighter { // stop all previous actions if new word is highlighted // if we trigger the run off a setting change -> multifile highlighting, we do not want to remove decorations from this model - this._stopAll(multiFileConfigChange ? this.model : undefined); + this._stopAll(multiFileConfigChange ? this.model.uri : undefined); const myRequestId = ++this.workerRequestTokenId; this.workerRequestCompleted = false; @@ -675,10 +677,35 @@ class WordHighlighter { // 1) we have text focus, and a valid query was updated. // 2) we do not have text focus, and a valid query is cached. // the query will ALWAYS have the correct data for the current highlight request, so it can always be passed to the workerRequest safely - if (!WordHighlighter.query || !WordHighlighter.query.modelInfo || WordHighlighter.query.modelInfo.model.isDisposed()) { + if (!WordHighlighter.query || !WordHighlighter.query.modelInfo) { return; } - this.workerRequest = this.computeWithModel(WordHighlighter.query.modelInfo.model, WordHighlighter.query.modelInfo.selection, WordHighlighter.query.word, otherModelsToHighlight); + const queryModelRef = await this.textModelService.createModelReference(WordHighlighter.query.modelInfo.modelURI); + const queryModel = queryModelRef.object.textEditorModel; + this.workerRequest = this.computeWithModel(queryModel, WordHighlighter.query.modelInfo.selection, otherModelsToHighlight); + + this.workerRequest?.result.then(data => { + if (myRequestId === this.workerRequestTokenId) { + this.workerRequestCompleted = true; + this.workerRequestValue = data || []; + this._beginRenderDecorations(); + } + }, onUnexpectedError); + } else if (this.model.uri.scheme === Schemas.vscodeNotebookCell) { + // new wordHighlighter coming from a different model, NOT the query model, need to create a textModel ref + + // this._stopAll(multiFileConfigChange ? this.model.uri : undefined); + + const myRequestId = ++this.workerRequestTokenId; + this.workerRequestCompleted = false; + + if (!WordHighlighter.query || !WordHighlighter.query.modelInfo) { + return; + } + + const queryModelRef = await this.textModelService.createModelReference(WordHighlighter.query.modelInfo.modelURI); + const queryModel = queryModelRef.object.textEditorModel; + this.workerRequest = this.computeWithModel(queryModel, WordHighlighter.query.modelInfo.selection, [this.model]); this.workerRequest?.result.then(data => { if (myRequestId === this.workerRequestTokenId) { @@ -690,11 +717,11 @@ class WordHighlighter { } } - private computeWithModel(model: ITextModel, selection: Selection, word: IWordAtPosition | null, otherModels: ITextModel[]): IOccurenceAtPositionRequest | null { + private computeWithModel(model: ITextModel, selection: Selection, otherModels: ITextModel[]): IOccurenceAtPositionRequest | null { if (!otherModels.length) { - return computeOccurencesAtPosition(this.providers, model, selection, word, this.editor.getOption(EditorOption.wordSeparators)); + return computeOccurencesAtPosition(this.providers, model, selection, this.editor.getOption(EditorOption.wordSeparators)); } else { - return computeOccurencesMultiModel(this.multiDocumentProviders, model, selection, word, this.editor.getOption(EditorOption.wordSeparators), otherModels); + return computeOccurencesMultiModel(this.multiDocumentProviders, model, selection, this.editor.getOption(EditorOption.wordSeparators), otherModels); } } @@ -755,6 +782,9 @@ class WordHighlighter { } } } + + // clear the worker request when decorations are completed + this.workerRequest = null; } public dispose(): void { @@ -773,16 +803,26 @@ export class WordHighlighterContribution extends Disposable implements IEditorCo private _wordHighlighter: WordHighlighter | null; - constructor(editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @ICodeEditorService codeEditorService: ICodeEditorService) { + constructor( + editor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @ITextModelService textModelService: ITextModelService, + ) { super(); this._wordHighlighter = null; const createWordHighlighterIfPossible = () => { if (editor.hasModel() && !editor.getModel().isTooLargeForTokenization()) { - this._wordHighlighter = new WordHighlighter(editor, languageFeaturesService.documentHighlightProvider, languageFeaturesService.multiDocumentHighlightProvider, contextKeyService, codeEditorService); + this._wordHighlighter = new WordHighlighter(editor, languageFeaturesService.documentHighlightProvider, languageFeaturesService.multiDocumentHighlightProvider, contextKeyService, textModelService, codeEditorService); } }; this._register(editor.onDidChangeModel((e) => { if (this._wordHighlighter) { + if (!e.newModelUrl && e.oldModelUrl?.scheme !== Schemas.vscodeNotebookCell) { // happens when switching tabs to a notebook that has focus in the cell list, no new model URI (this also doesn't make it to the wordHighlighter, bc no editor.hasModel) + this.wordHighlighter?.stop(); + } + this._wordHighlighter.dispose(); this._wordHighlighter = null; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index f44b10e4e84..8cdb4f05629 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -3,36 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from '../../../../../../nls.js'; import * as DOM from '../../../../../../base/browser/dom.js'; import { raceCancellation } from '../../../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { Event } from '../../../../../../base/common/event.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { clamp } from '../../../../../../base/common/numbers.js'; import * as strings from '../../../../../../base/common/strings.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { EditorOption } from '../../../../../../editor/common/config/editorOptions.js'; import { IDimension } from '../../../../../../editor/common/core/dimension.js'; import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { tokenizeToStringSync } from '../../../../../../editor/common/languages/textToHtmlTokenizer.js'; import { IReadonlyTextBuffer, ITextModel } from '../../../../../../editor/common/model.js'; -import { localize } from '../../../../../../nls.js'; +import { CodeActionController } from '../../../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; +import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, IActiveNotebookEditorDelegate } from '../../notebookBrowser.js'; +import { CodeCellViewModel, outputDisplayLimit } from '../../viewModel/codeCellViewModel.js'; import { CellPartsCollection } from '../cellPart.js'; +import { NotebookCellEditorPool } from '../notebookCellEditorPool.js'; +import { CodeCellRenderTemplate } from '../notebookRenderingCommon.js'; import { CellEditorOptions } from './cellEditorOptions.js'; import { CellOutputContainer } from './cellOutput.js'; import { CollapsedCodeCellExecutionIcon } from './codeCellExecutionIcon.js'; -import { CodeCellRenderTemplate } from '../notebookRenderingCommon.js'; -import { CodeCellViewModel, outputDisplayLimit } from '../../viewModel/codeCellViewModel.js'; -import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; -import { WordHighlighterContribution } from '../../../../../../editor/contrib/wordHighlighter/browser/wordHighlighter.js'; -import { CodeActionController } from '../../../../../../editor/contrib/codeAction/browser/codeActionController.js'; -import { NotebookCellEditorPool } from '../notebookCellEditorPool.js'; export class CodeCell extends Disposable { private _outputContainerRenderer: CellOutputContainer; @@ -354,13 +353,9 @@ export class CodeCell extends Disposable { })); this._register(this.templateData.editor.onDidBlurEditorWidget(() => { - WordHighlighterContribution.get(this.templateData.editor)?.stopHighlighting(); CodeActionController.get(this.templateData.editor)?.hideCodeActions(); CodeActionController.get(this.templateData.editor)?.hideLightBulbWidget(); })); - this._register(this.templateData.editor.onDidFocusEditorWidget(() => { - WordHighlighterContribution.get(this.templateData.editor)?.restoreViewState(true); - })); } private _reigsterModelListeners(model: ITextModel) { From 1ed956dfa9bfc7bd73a97f104c05aec4c5e26383 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 9 Sep 2024 13:45:09 -0700 Subject: [PATCH 202/286] Remove `SymbolInformation` change --- src/vs/workbench/api/common/extHostTypeConverters.ts | 2 +- src/vs/workbench/api/common/extHostTypes.ts | 4 ++-- .../contrib/chat/browser/media/chatInlineAnchorWidget.css | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index c0652cccd45..1abbe45add0 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2505,7 +2505,7 @@ export namespace ChatResponseAnchorPart { URI.isUri(value.inlineReference) ? value.inlineReference : 'location' in value.inlineReference - ? WorkspaceSymbol.to(value.inlineReference) + ? WorkspaceSymbol.to(value.inlineReference) as vscode.SymbolInformation : Location.to(value.inlineReference), part.name ); diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1811501aef7..58e0404d8ce 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1346,14 +1346,14 @@ export class SymbolInformation { location!: Location; kind: SymbolKind; tags?: SymbolTag[]; - containerName: string; + containerName: string | undefined; constructor(name: string, kind: SymbolKind, containerName: string | undefined, location: Location); constructor(name: string, kind: SymbolKind, range: Range, uri?: URI, containerName?: string); constructor(name: string, kind: SymbolKind, rangeOrContainer: string | undefined | Range, locationOrUri?: Location | URI, containerName?: string) { this.name = name; this.kind = kind; - this.containerName = containerName ?? ''; + this.containerName = containerName; if (typeof rangeOrContainer === 'string') { this.containerName = rangeOrContainer; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css index cd4255fcefe..11900cf4ca1 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css @@ -9,8 +9,7 @@ } .chat-inline-anchor-widget { - outline: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); - outline-offset: -1px; + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); border-radius: 4px; } From 378fb48f25244e29719af86b50647c204bf121ad Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 9 Sep 2024 13:53:58 -0700 Subject: [PATCH 203/286] Tweak css --- .../contrib/chat/browser/chatInlineAnchorWidget.ts | 5 ++++- .../contrib/chat/browser/media/chatInlineAnchorWidget.css | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts index 4dfab620aff..6f3660886e7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -57,9 +57,11 @@ export class InlineAnchorWidget extends Disposable { let location: { readonly uri: URI; readonly range?: IRange }; let contextMenuId: MenuId; + let contextMenuArg: URI | { readonly uri: URI; readonly range?: IRange }; if (data.kind === 'symbol') { location = data.symbol.location; contextMenuId = MenuId.ChatInlineSymbolAnchorContext; + contextMenuArg = location; const icon = SymbolKinds.toIcon(data.symbol.kind); resourceLabel.setLabel(`$(${icon.id}) ${data.symbol.name}`, undefined, {}); @@ -83,6 +85,7 @@ export class InlineAnchorWidget extends Disposable { } else { location = data; contextMenuId = MenuId.ChatInlineResourceAnchorContext; + contextMenuArg = location.uri; const label = labelService.getUriBasenameLabel(location.uri); const title = location.range && data.kind !== 'symbol' ? @@ -106,7 +109,7 @@ export class InlineAnchorWidget extends Disposable { contextKeyService, getAnchor: () => event, getActions: () => { - const menu = menuService.getMenuActions(contextMenuId, contextKeyService, { arg: location }); + const menu = menuService.getMenuActions(contextMenuId, contextKeyService, { arg: contextMenuArg }); const primary: IAction[] = []; createAndFillInContextMenuActions(menu, primary); return primary; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css index 11900cf4ca1..22bc19f8bd0 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css @@ -13,6 +13,10 @@ border-radius: 4px; } +.chat-inline-anchor-widget:hover { + background-color: var(--vscode-list-hoverBackground); +} + .chat-inline-anchor-widget .monaco-icon-label { display: inline-flex; margin: 0 1px; @@ -31,10 +35,6 @@ color: reset-layer !important; } -.chat-inline-anchor-widget:hover .monaco-icon-label { - background-color: var(--vscode-list-hoverBackground); -} - .chat-inline-anchor-widget .monaco-icon-label::before { height: auto; padding-right: 3px; From 76fb6affe4efcb8638228defecc6ea590383b49d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:32:42 +0200 Subject: [PATCH 204/286] =?UTF-8?q?SCM=20Graph=20-=20=F0=9F=92=84=20more?= =?UTF-8?q?=20observable=20clean-up=20(#228021)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SCM Graph - clean-up graph rendering * Fix regression with repository picker --- .../base/common/observableInternal/utils.ts | 9 ++ .../contrib/scm/browser/scmHistoryViewPane.ts | 137 ++++++++---------- 2 files changed, 72 insertions(+), 74 deletions(-) diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index b4230118e88..3ce3805792e 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -301,6 +301,15 @@ class ObservableSignal extends BaseObservable implements } } +export function signalFromObservable(owner: DebugOwner | undefined, observable: IObservable): IObservable { + return derivedOpts({ + owner, + equalsFn: () => false, + }, reader => { + observable.read(reader); + }); +} + /** * @deprecated Use `debouncedObservable2` instead. */ diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index ea5a76d79f6..828e8d60f33 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -16,7 +16,7 @@ import { fromNow } from '../../../../base/common/date.js'; import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStoreHandleChanges, derived, derivedOpts, IObservable, observableValue, waitForState } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, autorunWithStoreHandleChanges, derived, IObservable, observableValue, waitForState } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -48,7 +48,7 @@ import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/ac import { delta, groupBy, tail } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; -import { constObservable, latestChangedValue, observableFromEvent, runOnChange } from '../../../../base/common/observableInternal/utils.js'; +import { constObservable, latestChangedValue, observableFromEvent, runOnChange, signalFromObservable } from '../../../../base/common/observableInternal/utils.js'; import { ContextKeys } from './scmViewPane.js'; import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IDropdownMenuActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; @@ -59,7 +59,6 @@ import { Event } from '../../../../base/common/event.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { clamp } from '../../../../base/common/numbers.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { structuralEquals } from '../../../../base/common/equals.js'; import { compare } from '../../../../base/common/strings.js'; type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; @@ -823,8 +822,8 @@ class RepositoryPicker extends Disposable { }; constructor( - private readonly _scmViewService: ISCMViewService, - private readonly _quickInputService: IQuickInputService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @ISCMViewService private readonly _scmViewService: ISCMViewService ) { super(); } @@ -1063,93 +1062,83 @@ export class SCMHistoryViewPane extends ViewPane { // Wait for first repository to be initialized await waitForState(firstRepositoryInitialized); + // Set tree input this._treeOperationSequencer.queue(async () => { await this._tree.setInput(this._treeViewModel); this._tree.scrollTop = 0; }); // Repository change - this._visibilityDisposables.add( - autorunWithStoreHandleChanges<{ refresh: boolean }>({ - owner: this, - createEmptyChangeSummary: () => ({ refresh: false }), - handleChange(_, changeSummary) { - changeSummary.refresh = true; - return true; - }, - }, (reader, changeSummary, store) => { - const repository = this._treeViewModel.repository.read(reader); - const historyProvider = repository?.provider.historyProvider.read(reader); - if (!repository || !historyProvider) { - return; - } + let isFirstRun = true; + this._visibilityDisposables.add(autorunWithStore((reader, store) => { + const repository = this._treeViewModel.repository.read(reader); + const historyProvider = repository?.provider.historyProvider.read(reader); + if (!repository || !historyProvider) { + return; + } - // Update context - this._scmProviderCtx.set(repository.provider.contextValue); + // Update context + this._scmProviderCtx.set(repository.provider.contextValue); - // Checkout, Commit, and Publish - const historyItemGroup = derivedOpts<{ id: string; revision?: string; remoteId?: string } | undefined>({ + // Commit, Checkout + const historyItemRefSignal = signalFromObservable(this, historyProvider.currentHistoryItemRef); + + // Publish + const historyItemRefRemoteIdSignal = signalFromObservable(this, derived(reader => { + return historyProvider.currentHistoryItemRemoteRef.read(reader)?.id; + })); + + // Fetch, Push + const historyItemRefRemoteRevisionSignal = signalFromObservable(this, derived(reader => { + return historyProvider.currentHistoryItemRemoteRef.read(reader)?.revision; + })); + + // HistoryItemRefs changed + store.add( + autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({ owner: this, - equalsFn: structuralEquals - }, reader => { - const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup.read(reader); - return currentHistoryItemGroup ? { - id: currentHistoryItemGroup.id, - revision: currentHistoryItemGroup.revision, - remoteId: currentHistoryItemGroup.remote?.id - } : undefined; - }); + createEmptyChangeSummary: () => ({ refresh: false }), + handleChange(context, changeSummary) { + changeSummary.refresh = context.didChange(historyItemRefRemoteRevisionSignal) ? 'ifScrollTop' : true; + return true; + }, + }, (reader, changeSummary) => { + historyItemRefSignal.read(reader); + historyItemRefRemoteIdSignal.read(reader); + historyItemRefRemoteRevisionSignal.read(reader); - // Fetch, Push - const historyItemRemoteRevision = derived(reader => { - return historyProvider.currentHistoryItemGroup.read(reader)?.remote?.revision; - }); + if (changeSummary.refresh === true) { + this.refresh(); + return; + } - // HistoryItemGroup change - store.add( - autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({ - owner: this, - createEmptyChangeSummary: () => ({ refresh: false }), - handleChange(context, changeSummary) { - changeSummary.refresh = context.didChange(historyItemRemoteRevision) ? 'ifScrollTop' : true; - return true; - }, - }, (reader, changeSummary) => { - const historyItemGroupValue = historyItemGroup.read(reader); - const historyItemRemoteRevisionValue = historyItemRemoteRevision.read(reader); - - if ((!historyItemGroupValue && !historyItemRemoteRevisionValue) || changeSummary.refresh === false) { - return; - } - - if (changeSummary.refresh === true) { + if (changeSummary.refresh === 'ifScrollTop') { + // Remote revision changes can occur as a result of a user action (Fetch, Push) but + // it can also occur as a result of background action (Auto Fetch). If the tree is + // scrolled to the top, we can safely refresh the tree. + if (this._tree.scrollTop === 0) { this.refresh(); return; } - if (changeSummary.refresh === 'ifScrollTop') { - // Remote revision changes can occur as a result of a user action (Fetch, Push) but - // it can also occur as a result of background action (Auto Fetch). If the tree is - // scrolled to the top, we can safely refresh the tree. - if (this._tree.scrollTop === 0) { - this.refresh(); - return; - } - - // Show the "Outdated" badge on the view - this._repositoryOutdated.set(true, undefined); - } - })); - - // HistoryItemRefs filter changed - store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => { - this.refresh(); + // Show the "Outdated" badge on the view + this._repositoryOutdated.set(true, undefined); + } })); - if (changeSummary.refresh) { - this.refresh(); - } + // HistoryItemRefs filter changed + store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => { + this.refresh(); })); + + // We skip refreshing the graph on the first execution of the autorun + // since the graph for the first repository is rendered when the tree + // input is set. + if (!isFirstRun) { + this.refresh(); + } + isFirstRun = false; + })); } else { this._visibilityDisposables.clear(); } From dd9e7ee3443317022e62d20623d46c9b1ecf81b3 Mon Sep 17 00:00:00 2001 From: John Murray Date: Mon, 9 Sep 2024 22:33:55 +0100 Subject: [PATCH 205/286] Add actions to preview editor tab's hover (#226023) * WIP: show hover with actions on preview editor tab * Refactor to support `"workbench.editor.showTabs": "single"` * Manage hover delegates by index to handle closing correctly * Add some dispose calls * Experiment with a quieter hover * Fix problems caused by commented-out code * Safer use of editor title in hover markdown * Subtler link to preview mode doc * Use gear icon and go oto settings editor * Make clickable hover easier to mouse onto * Remove linkHandler experiment * Use simplified hover on single-tab mode too * Simplify now that we aren't adding statusbar commands to hovers * Reinstate lost blank line --------- Co-authored-by: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> --- .../browser/parts/editor/editorTabsControl.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 477f0115bf1..d03297e004f 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -47,6 +47,8 @@ import { ServiceCollection } from '../../../../platform/instantiation/common/ser import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; export class EditorCommandsContextActionRunner extends ActionRunner { @@ -452,8 +454,17 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC return this.groupsView.partOptions.tabHeight !== 'compact' ? EditorTabsControl.EDITOR_TAB_HEIGHT.normal : EditorTabsControl.EDITOR_TAB_HEIGHT.compact; } - protected getHoverTitle(editor: EditorInput): string { - return editor.getTitle(Verbosity.LONG); + protected getHoverTitle(editor: EditorInput): string | IManagedHoverTooltipMarkdownString { + const title = editor.getTitle(Verbosity.LONG); + if (!this.tabsModel.isPinned(editor)) { + return { + markdown: new MarkdownString('', { supportThemeIcons: true, isTrusted: true }). + appendText(title). + appendMarkdown(' (_preview_ [$(gear)](command:workbench.action.openSettings?%5B%22workbench.editor.enablePreview%22%5D "Configure Preview Mode"))'), + markdownNotSupportedFallback: title + ' (preview)' + }; + } + return title; } protected getHoverDelegate(): IHoverDelegate { From d8af24e15daeb1ad5bd0ee4858852e60ea545c66 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 01:51:31 +0200 Subject: [PATCH 206/286] SCM - wire-up the onDidChangeHistoryItemRefs event (#228042) Wire-up the event --- extensions/git/src/historyProvider.ts | 85 ++++++++++++------- extensions/git/src/util.ts | 57 ++++++++++++- src/vs/workbench/api/browser/mainThreadSCM.ts | 37 +++++++- .../workbench/api/common/extHost.protocol.ts | 7 ++ src/vs/workbench/api/common/extHostSCM.ts | 11 +++ .../workbench/contrib/scm/common/history.ts | 8 ++ .../vscode.proposed.scmHistoryProvider.d.ts | 10 +-- 7 files changed, 177 insertions(+), 38 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index e9aefa63dd1..872582dae6e 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,21 +4,53 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryChangeEvent, SourceControlHistoryItemRef, l10n } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; -import { IDisposable, dispose } from './util'; +import { IDisposable, deltaHistoryItemRefs, dispose } from './util'; import { toGitUri } from './uri'; -import { Branch, LogOptions, RefType } from './api/git'; +import { Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; +function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { + switch (ref.type) { + case RefType.RemoteHead: + return { + id: `refs/remotes/${ref.remote}/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('cloud'), + category: l10n.t('remote branches') + }; + case RefType.Tag: + return { + id: `refs/tags/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('tag'), + category: l10n.t('tags') + }; + default: + return { + id: `refs/heads/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? ref.commit.substring(0, 8) : undefined, + revision: ref.commit, + icon: new ThemeIcon('git-branch'), + category: l10n.t('branches') + }; + } +} + export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter(); readonly onDidChangeCurrentHistoryItemGroup: Event = this._onDidChangeCurrentHistoryItemGroup.event; - private readonly _onDidChangeHistory = new EventEmitter(); - readonly onDidChangeHistory: Event = this._onDidChangeHistory.event; + private readonly _onDidChangeHistoryItemRefs = new EventEmitter(); + readonly onDidChangeHistoryItemRefs: Event = this._onDidChangeHistoryItemRefs.event; private readonly _onDidChangeDecorations = new EventEmitter(); readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; @@ -30,6 +62,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this._onDidChangeCurrentHistoryItemGroup.fire(); } + private historyItemRefs: SourceControlHistoryItemRef[] = []; private historyItemDecorations = new Map(); private disposables: Disposable[] = []; @@ -80,6 +113,21 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec }; this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup: ${JSON.stringify(this.currentHistoryItemGroup)}`); + + // Refs (alphabetically) + const refs = await this.repository.getRefs({ sort: 'alphabetically' }); + const historyItemRefs = refs.map(ref => toSourceControlHistoryItemRef(ref)); + + const delta = deltaHistoryItemRefs(this.historyItemRefs, historyItemRefs); + this._onDidChangeHistoryItemRefs.fire(delta); + this.historyItemRefs = historyItemRefs; + + const deltaLog = { + added: delta.added.map(ref => ref.id), + modified: delta.modified.map(ref => ref.id), + removed: delta.removed.map(ref => ref.id) + }; + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] historyItemRefs: ${JSON.stringify(deltaLog)}`); } async provideHistoryItemRefs(): Promise { @@ -92,34 +140,13 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec for (const ref of refs) { switch (ref.type) { case RefType.RemoteHead: - remoteBranches.push({ - id: `refs/remotes/${ref.remote}/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined, - revision: ref.commit, - icon: new ThemeIcon('cloud'), - category: l10n.t('remote branches') - }); + remoteBranches.push(toSourceControlHistoryItemRef(ref)); break; case RefType.Tag: - tags.push({ - id: `refs/tags/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined, - revision: ref.commit, - icon: new ThemeIcon('tag'), - category: l10n.t('tags') - }); + tags.push(toSourceControlHistoryItemRef(ref)); break; default: - branches.push({ - id: `refs/heads/${ref.name}`, - name: ref.name ?? '', - description: ref.commit ? ref.commit.substring(0, 8) : undefined, - revision: ref.commit, - icon: new ThemeIcon('git-branch'), - category: l10n.t('branches') - }); + branches.push(toSourceControlHistoryItemRef(ref)); break; } } diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index eac6f0384b9..bf33411c3b3 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Disposable, EventEmitter } from 'vscode'; +import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef } from 'vscode'; import { dirname, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; @@ -513,3 +513,58 @@ export namespace Versions { return from(major, minor, patch, pre); } } + +export function deltaHistoryItemRefs(before: SourceControlHistoryItemRef[], after: SourceControlHistoryItemRef[]): { + added: SourceControlHistoryItemRef[]; + modified: SourceControlHistoryItemRef[]; + removed: SourceControlHistoryItemRef[]; +} { + if (before.length === 0) { + return { added: after, modified: [], removed: [] }; + } + + const added: SourceControlHistoryItemRef[] = []; + const modified: SourceControlHistoryItemRef[] = []; + const removed: SourceControlHistoryItemRef[] = []; + + let beforeIdx = 0; + let afterIdx = 0; + + while (true) { + if (beforeIdx === before.length) { + added.push(...after.slice(afterIdx)); + break; + } + if (afterIdx === after.length) { + removed.push(...before.slice(beforeIdx)); + break; + } + + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + + const result = beforeElement.id.localeCompare(afterElement.id); + + if (result === 0) { + if (beforeElement.revision !== afterElement.revision) { + // modified + modified.push(afterElement); + } + + beforeIdx += 1; + afterIdx += 1; + } else if (result < 0) { + // beforeElement is smaller -> before element removed + removed.push(beforeElement); + + beforeIdx += 1; + } else if (result > 0) { + // beforeElement is greater -> after element added + added.push(afterElement); + + afterIdx += 1; + } + } + + return { added, modified, removed }; +} diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 7ab6d69e6ae..d713066de80 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -6,10 +6,10 @@ import { Barrier } from '../../../base/common/async.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { Event, Emitter } from '../../../base/common/event.js'; -import { derivedOpts, observableValue, observableValueOpts } from '../../../base/common/observable.js'; +import { derivedOpts, IObservable, observableValue, observableValueOpts } from '../../../base/common/observable.js'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from '../../contrib/scm/common/scm.js'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol.js'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto, SCMHistoryItemRefsChangeEventDto } from '../common/extHost.protocol.js'; import { Command } from '../../../editor/common/languages.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; @@ -17,7 +17,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemRef, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemRef, ISCMHistoryItemRefsChangeEvent, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; import { ResourceTree } from '../../../base/common/resourceTree.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js'; @@ -218,6 +218,9 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { } : undefined; }); + private readonly _historyItemRefChanges = observableValue(this, { added: [], modified: [], removed: [] }); + get historyItemRefChanges(): IObservable { return this._historyItemRefChanges; } + constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise { @@ -247,6 +250,14 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { this._currentHistoryItemGroup.set(historyItemGroup, undefined); } + + $onDidChangeHistoryItemRefs(historyItemRefs: SCMHistoryItemRefsChangeEventDto): void { + const added = historyItemRefs.added.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); + const modified = historyItemRefs.modified.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); + const removed = historyItemRefs.removed.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); + + this._historyItemRefChanges.set({ added, modified, removed }, undefined); + } } class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { @@ -484,6 +495,14 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this._historyProvider.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); } + $onDidChangeHistoryProviderHistoryItemRefs(historyItemRefs: SCMHistoryItemRefsChangeEventDto): void { + if (!this.historyProvider.get()) { + return; + } + + this._historyProvider.get()?.$onDidChangeHistoryItemRefs(historyItemRefs); + } + toJSON(): any { return { $mid: MarshalledId.ScmProvider, @@ -728,4 +747,16 @@ export class MainThreadSCM implements MainThreadSCMShape { const provider = repository.provider as MainThreadSCMProvider; provider.$onDidChangeHistoryProviderCurrentHistoryItemGroup(historyItemGroup); } + + async $onDidChangeHistoryProviderHistoryItemRefs(sourceControlHandle: number, historyItemRefs: SCMHistoryItemRefsChangeEventDto): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); + const repository = this._repositories.get(sourceControlHandle); + + if (!repository) { + return; + } + + const provider = repository.provider as MainThreadSCMProvider; + provider.$onDidChangeHistoryProviderHistoryItemRefs(historyItemRefs); + } } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2f517f05a6d..2055d997aa0 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1559,6 +1559,12 @@ export interface SCMHistoryItemRefDto { readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; } +export interface SCMHistoryItemRefsChangeEventDto { + readonly added: readonly SCMHistoryItemRefDto[]; + readonly modified: readonly SCMHistoryItemRefDto[]; + readonly removed: readonly SCMHistoryItemRefDto[]; +} + export interface SCMHistoryItemDto { readonly id: string; readonly parentIds: string[]; @@ -1601,6 +1607,7 @@ export interface MainThreadSCMShape extends IDisposable { $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise; $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise; + $onDidChangeHistoryProviderHistoryItemRefs(sourceControlHandle: number, historyItemRefs: SCMHistoryItemRefsChangeEventDto): Promise; } export interface MainThreadQuickDiffShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 3976177cc7e..1f15f83d60a 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -597,6 +597,17 @@ class ExtHostSourceControl implements vscode.SourceControl { this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup); })); + this._historyProviderDisposable.value.add(historyProvider.onDidChangeHistoryItemRefs((e) => { + if (e.added.length === 0 && e.modified.length === 0 && e.removed.length === 0) { + return; + } + + const added = e.added.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })); + const modified = e.modified.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })); + const removed = e.removed.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })); + + this.#proxy.$onDidChangeHistoryProviderHistoryItemRefs(this.handle, { added, modified, removed }); + })); } } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index fbc0d6c0f1f..6a388fcb460 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -21,6 +21,8 @@ export interface ISCMHistoryProvider { readonly currentHistoryItemRemoteRef: IObservable; readonly currentHistoryItemBaseRef: IObservable; + readonly historyItemRefChanges: IObservable; + provideHistoryItemRefs(): Promise; provideHistoryItems(options: ISCMHistoryOptions): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; @@ -57,6 +59,12 @@ export interface ISCMHistoryItemRef { readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; } +export interface ISCMHistoryItemRefsChangeEvent { + readonly added: readonly ISCMHistoryItemRef[]; + readonly removed: readonly ISCMHistoryItemRef[]; + readonly modified: readonly ISCMHistoryItemRef[]; +} + export interface ISCMHistoryItem { readonly id: string; readonly parentIds: string[]; diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index d465d599ba2..b6a4893dd54 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -22,7 +22,7 @@ declare module 'vscode' { /** * Fires when history item refs change */ - onDidChangeHistory: Event; + onDidChangeHistoryItemRefs: Event; provideHistoryItemRefs(token: CancellationToken): ProviderResult; provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; @@ -78,9 +78,9 @@ declare module 'vscode' { readonly renameUri: Uri | undefined; } - export interface SourceControlHistoryChangeEvent { - readonly added: Iterable; - readonly removed: Iterable; - readonly modified: Iterable; + export interface SourceControlHistoryItemRefsChangeEvent { + readonly added: readonly SourceControlHistoryItemRef[]; + readonly removed: readonly SourceControlHistoryItemRef[]; + readonly modified: readonly SourceControlHistoryItemRef[]; } } From c4d1cc2e6760a40db1244080da9ee7658da07be2 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 9 Sep 2024 19:39:37 -0700 Subject: [PATCH 207/286] Fix GitHub account ids being numbers (#228045) For a long time the account id wasn't handled correctly. It should be a string, but the API returns a number. This ensures it's a string and does some migration logic. --- .../github-authentication/src/github.ts | 25 ++++++++++++++++--- .../github-authentication/src/githubServer.ts | 4 +-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index ed584c65f8b..231c793e6a4 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -18,7 +18,9 @@ interface SessionData { account?: { label?: string; displayName?: string; - id: string; + // Unfortunately, for some time the id was a number, so we need to support both. + // This can be removed once we are confident that all users have migrated to the new id. + id: string | number; }; scopes: string[]; accessToken: string; @@ -239,9 +241,14 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return []; } + // Unfortunately, we were using a number secretly for the account id for some time... this is due to a bad `any`. + // AuthenticationSession's account id is a string, so we need to detect when there is a number accountId and re-store + // the sessions to migrate away from the bad number usage. + // TODO@TylerLeonhardt: Remove this after we are confident that all users have migrated to the new id. + let seenNumberAccountId: boolean = false; // TODO: eventually remove this Set because we should only have one session per set of scopes. const scopesSeen = new Set(); - const sessionPromises = sessionData.map(async (session: SessionData) => { + const sessionPromises = sessionData.map(async (session: SessionData): Promise => { // For GitHub scope list, order doesn't matter so we immediately sort the scopes const scopesStr = [...session.scopes].sort().join(' '); if (!this._supportsMultipleAccounts && scopesSeen.has(scopesStr)) { @@ -262,13 +269,23 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this._logger.trace(`Read the following session from the keychain with the following scopes: ${scopesStr}`); scopesSeen.add(scopesStr); + + let accountId: string; + if (session.account?.id) { + if (typeof session.account.id === 'number') { + seenNumberAccountId = true; + } + accountId = `${session.account.id}`; + } else { + accountId = userInfo?.id ?? ''; + } return { id: session.id, account: { label: session.account ? session.account.label ?? session.account.displayName ?? '' : userInfo?.accountName ?? '', - id: session.account?.id ?? userInfo?.id ?? '' + id: accountId }, // we set this to session.scopes to maintain the original order of the scopes requested // by the extension that called getSession() @@ -283,7 +300,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid .filter((p?: T): p is T => Boolean(p)); this._logger.info(`Got ${verifiedSessions.length} verified sessions.`); - if (verifiedSessions.length !== sessionData.length) { + if (seenNumberAccountId || verifiedSessions.length !== sessionData.length) { await this.storeSessions(verifiedSessions); } diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index c9f0a8c07d5..a9f94ccf65d 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -228,9 +228,9 @@ export class GitHubServer implements IGitHubServer { if (result.ok) { try { - const json = await result.json(); + const json = await result.json() as { id: number; login: string }; this._logger.info('Got account info!'); - return { id: json.id, accountName: json.login }; + return { id: `${json.id}`, accountName: json.login }; } catch (e) { this._logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`); throw e; From d0079eb32b7b84ad4c778d28aafae6db1e0ef3ed Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Tue, 10 Sep 2024 13:08:18 +1000 Subject: [PATCH 208/286] Collapse unchanged lines in input/metadata editors of NB Diff view (#228051) * Collapse unchanged lines in input/metadata editors of NB Diff view * add comments --- .../browser/diff/diffCellEditorOptions.ts | 8 +- .../notebook/browser/diff/diffComponents.ts | 86 +++++++---- .../browser/diff/diffElementViewModel.ts | 83 ++++++++--- .../browser/diff/notebookDiffActions.ts | 24 ++++ .../browser/diff/notebookDiffEditor.ts | 10 +- .../browser/diff/notebookDiffViewModel.ts | 40 ++++-- .../browser/diff/notebookMultiDiffEditor.ts | 3 +- .../browser/diff/unchangedEditorRegions.ts | 133 ++++++++++++++++++ .../test/browser/notebookDiff.test.ts | 25 ++-- 9 files changed, 335 insertions(+), 77 deletions(-) create mode 100644 src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts index a14887cabe9..14c76fe0a75 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts @@ -10,15 +10,19 @@ import { IEditorOptions } from '../../../../../editor/common/config/editorOption * Do not leave at 12, when at 12 and we have whitespace and only one line, * then there's not enough space for the button `Show Whitespace Differences` */ -export const fixedEditorPaddingSingleLineCells = { +const fixedEditorPaddingSingleLineCells = { top: 24, bottom: 24 }; -export const fixedEditorPadding = { +const fixedEditorPadding = { top: 12, bottom: 12 }; +export function getEditorPadding(lineCount: number) { + return lineCount === 1 ? fixedEditorPaddingSingleLineCells : fixedEditorPadding; +} + export const fixedEditorOptions: IEditorOptions = { padding: fixedEditorPadding, scrollBeyondLastLine: false, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 664df4fa56a..7018c9f126f 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -38,7 +38,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { fixedDiffEditorOptions, fixedEditorOptions, fixedEditorPadding, fixedEditorPaddingSingleLineCells } from './diffCellEditorOptions.js'; +import { fixedDiffEditorOptions, fixedEditorOptions, getEditorPadding } from './diffCellEditorOptions.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { DiffEditorWidget } from '../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; @@ -48,6 +48,7 @@ import { localize } from '../../../../../nls.js'; import { Emitter } from '../../../../../base/common/event.js'; import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; import { getFormattedMetadataJSON } from '../../common/model/notebookCellTextModel.js'; +import { IDiffEditorOptions } from '../../../../../editor/common/config/editorOptions.js'; export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { return { @@ -516,20 +517,36 @@ abstract class AbstractElementRenderer extends Disposable { modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() }); + if (this.cell.unchangedRegionsService.options.enabled) { + this._metadataEditor.updateOptions({ hideUnchangedRegions: this.cell.unchangedRegionsService.options }); + } + this._metadataEditorDisposeStore.add(this.cell.unchangedRegionsService.options.onDidChangeEnablement(() => { + if (this._metadataEditor) { + this._metadataEditor.updateOptions({ hideUnchangedRegions: this.cell.unchangedRegionsService.options }); + } + })); + + this.layout({ metadataHeight: true }); this._metadataEditorDisposeStore.add(this._metadataEditor); this._metadataEditorContainer?.classList.add('diff'); - const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.originalDocument.uri, this.cell.original.handle, Schemas.vscodeNotebookCellMetadata)); - const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.modifiedDocument.uri, this.cell.modified.handle, Schemas.vscodeNotebookCellMetadata)); - this._metadataEditor.setModel({ + const [originalMetadataModel, modifiedMetadataModel] = await Promise.all([ + this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.originalDocument.uri, this.cell.original.handle, Schemas.vscodeNotebookCellMetadata)), + this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.modifiedDocument.uri, this.cell.modified.handle, Schemas.vscodeNotebookCellMetadata)) + ]); + this._metadataEditorDisposeStore.add(originalMetadataModel); + this._metadataEditorDisposeStore.add(modifiedMetadataModel); + const vm = this._metadataEditor.createViewModel({ original: originalMetadataModel.object.textEditorModel, modified: modifiedMetadataModel.object.textEditorModel }); - - this._metadataEditorDisposeStore.add(originalMetadataModel); - this._metadataEditorDisposeStore.add(modifiedMetadataModel); + // Reduces flicker (compute this before setting the model) + // Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y. + // & that results in flicker. + await vm.waitForDiff(); + this._metadataEditor.setModel(vm); this.cell.metadataHeight = this._metadataEditor.getContentHeight(); @@ -847,10 +864,8 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { this.cell.editorHeight = 0; return; } - - const lineCount = this.nestedCellViewModel.textModel.textBuffer.getLineCount(); const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; + const editorHeight = this.cell.computeInputEditorHeight(lineHeight); this._editorContainer.style.height = `${editorHeight}px`; this._editorContainer.style.display = 'block'; @@ -1585,7 +1600,7 @@ export class ModifiedElement extends AbstractElementRenderer { const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : (lineCount * lineHeight) + fixedEditorPadding.top + fixedEditorPadding.bottom; + const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : this.cell.computeInputEditorHeight(lineHeight); this._editorContainer.style.height = `${editorHeight}px`; this._editorContainer.style.display = 'block'; @@ -1603,11 +1618,17 @@ export class ModifiedElement extends AbstractElementRenderer { // E.g. assume we have a cell with 1 line and we add some whitespace, // Then diff editor displays the button `Show Whitespace Differences`, however with 12 paddings on the top, the // button can get cut off. - if (lineCount === 1) { - this._editor.updateOptions({ - padding: fixedEditorPaddingSingleLineCells - }); + const options: IDiffEditorOptions = { + padding: getEditorPadding(lineCount) + }; + if (this.cell.unchangedRegionsService.options.enabled) { + options.hideUnchangedRegions = this.cell.unchangedRegionsService.options; } + this._editor.updateOptions(options); + this._register(this.cell.unchangedRegionsService.options.onDidChangeEnablement(() => { + options.hideUnchangedRegions = this.cell.unchangedRegionsService.options; + this._editor?.updateOptions(options); + })); this._editor.layout({ width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, height: editorHeight @@ -1683,25 +1704,28 @@ export class ModifiedElement extends AbstractElementRenderer { } private async _initializeSourceDiffEditor() { - const originalCell = this.cell.original; - const modifiedCell = this.cell.modified; - - const originalRef = await this.textModelService.createModelReference(originalCell.uri); - const modifiedRef = await this.textModelService.createModelReference(modifiedCell.uri); - - if (this._isDisposed) { - return; - } - - const textModel = originalRef.object.textEditorModel; - const modifiedTextModel = modifiedRef.object.textEditorModel; + const [originalRef, modifiedRef] = await Promise.all([ + this.textModelService.createModelReference(this.cell.original.uri), + this.textModelService.createModelReference(this.cell.modified.uri)]); this._register(originalRef); this._register(modifiedRef); - this._editor!.setModel({ - original: textModel, - modified: modifiedTextModel, - }); + if (this._isDisposed) { + originalRef.dispose(); + modifiedRef.dispose(); + return; + } + + const vm = this._register(this._editor!.createViewModel({ + original: originalRef.object.textEditorModel, + modified: modifiedRef.object.textEditorModel, + })); + + // Reduces flicker (compute this before setting the model) + // Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y. + // & that results in flicker. + await vm.waitForDiff(); + this._editor!.setModel(vm); const handleViewStateChange = () => { this._editorViewStateChanged = true; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index 16c1b24c0c0..9e82e57bee0 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -10,7 +10,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { DiffEditorWidget } from '../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; import { FontInfo } from '../../../../../editor/common/config/fontInfo.js'; import * as editorCommon from '../../../../../editor/common/editorCommon.js'; -import { fixedEditorPadding } from './diffCellEditorOptions.js'; +import { getEditorPadding } from './diffCellEditorOptions.js'; import { DiffNestedCellViewModel } from './diffNestedCellViewModel.js'; import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from './eventDispatcher.js'; import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, DiffSide, IDiffElementLayoutInfo } from './notebookDiffEditorBrowser.js'; @@ -18,9 +18,16 @@ import { CellLayoutState, IGenericCellViewModel } from '../notebookBrowser.js'; import { NotebookLayoutInfo } from '../notebookViewEvents.js'; import { getFormattedMetadataJSON, NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; -import { ICellOutput, INotebookTextModel, IOutputDto, IOutputItemDto } from '../../common/notebookCommon.js'; +import { CellUri, ICellOutput, INotebookTextModel, IOutputDto, IOutputItemDto } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IUnchangedEditorRegionsService } from './unchangedEditorRegions.js'; +import { Schemas } from '../../../../../base/common/network.js'; + +// From `.monaco-editor .diff-hidden-lines .center` in src/vs/editor/browser/widget/diffEditor/style.css +export const HeightOfHiddenLinesRegionInDiffEditor = 24; + +export const DefaultLineHeight = 17; export enum PropertyFoldingState { Expanded, @@ -204,7 +211,9 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB fontInfo: FontInfo | undefined; }, notebookService: INotebookService, - private readonly configurationService: IConfigurationService + public readonly index: number, + private readonly configurationService: IConfigurationService, + public readonly unchangedRegionsService: IUnchangedEditorRegionsService ) { super(mainDocumentTextModel, editorEventDispatcher, initData); this.original = original ? this._register(new DiffNestedCellViewModel(original, notebookService)) : undefined; @@ -246,14 +255,14 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB case 'insert': { const lineCount = this.modified!.textModel.textBuffer.getLineCount(); - const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; + const editorHeight = lineCount * lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom; return editorHeight; } case 'delete': case 'modified': { const lineCount = this.original!.textModel.textBuffer.getLineCount(); - const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; + const editorHeight = lineCount * lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom; return editorHeight; } } @@ -364,7 +373,7 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB getHeight(lineHeight: number) { if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) { - const editorHeight = this.cellFoldingState === PropertyFoldingState.Collapsed ? 0 : this.estimateEditorHeight(lineHeight); + const editorHeight = this.cellFoldingState === PropertyFoldingState.Collapsed ? 0 : this.computeInputEditorHeight(lineHeight); return this._computeTotalHeight(editorHeight); } else { return this._layoutInfo.totalHeight; @@ -385,15 +394,9 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB return totalHeight; } - private estimateEditorHeight(lineHeight: number | undefined = 20): number { - const hasScrolling = false; - const verticalScrollbarHeight = hasScrolling ? 12 : 0; // take zoom level into account - // const editorPadding = this.viewContext.notebookOptions.computeEditorPadding(this.internalMetadata); + public computeInputEditorHeight(lineHeight: number): number { const lineCount = Math.max(this.original?.textModel.textBuffer.getLineCount() ?? 1, this.modified?.textModel.textBuffer.getLineCount() ?? 1); - return lineCount * lineHeight - + 24 // Top padding - + 12 // Bottom padding - + verticalScrollbarHeight; + return lineCount * lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom; } private _getOutputTotalHeight(rawOutputHeight: number, metadataHeight: number) { @@ -474,6 +477,10 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase override readonly modified!: DiffNestedCellViewModel; override readonly type: 'unchanged' | 'modified'; + /** + * The height of the editor when the unchanged lines are collapsed. + */ + private editorHeightWithUnchangedLinesCollapsed?: number; constructor( mainDocumentTextModel: NotebookTextModel, readonly otherDocumentTextModel: NotebookTextModel, @@ -487,7 +494,9 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase fontInfo: FontInfo | undefined; }, notebookService: INotebookService, - configurationService: IConfigurationService + configurationService: IConfigurationService, + index: number, + unchangedRegionsService: IUnchangedEditorRegionsService ) { super( mainDocumentTextModel, @@ -497,7 +506,9 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase editorEventDispatcher, initData, notebookService, - configurationService); + index, + configurationService, + unchangedRegionsService); this.type = type; @@ -629,6 +640,40 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase return this.modified; } } + + public override computeInputEditorHeight(lineHeight: number): number { + if (this.type === 'modified' && + typeof this.editorHeightWithUnchangedLinesCollapsed === 'number' && + this.unchangedRegionsService.options.enabled && + this.checkIfInputModified()) { + return this.editorHeightWithUnchangedLinesCollapsed; + } + + return super.computeInputEditorHeight(lineHeight); + } + + private async computeInputEditorHeightWithUnchangedLinesHidden() { + if (this.checkIfInputModified()) { + this.editorHeightWithUnchangedLinesCollapsed = this._layoutInfo.editorHeight = await this.unchangedRegionsService.computeEditorHeight(this.original.uri, this.modified.uri); + } + } + + private async computeMetadataEditorHeightWithUnchangedLinesHidden() { + if (this.checkMetadataIfModified()) { + const originalMetadataUri = CellUri.generateCellPropertyUri(this.originalDocument.uri, this.original.handle, Schemas.vscodeNotebookCellMetadata); + const modifiedMetadataUri = CellUri.generateCellPropertyUri(this.modifiedDocument.uri, this.modified.handle, Schemas.vscodeNotebookCellMetadata); + this._layoutInfo.metadataHeight = await this.unchangedRegionsService.computeEditorHeight(originalMetadataUri, modifiedMetadataUri); + } + } + + public async computeEditorHeights() { + if (this.type === 'unchanged' || !this.unchangedRegionsService.options.enabled) { + return; + } + + await Promise.all([this.computeInputEditorHeightWithUnchangedLinesHidden(), this.computeMetadataEditorHeightWithUnchangedLinesHidden()]); + } + } export class SingleSideDiffElementViewModel extends DiffElementCellViewModelBase { @@ -667,9 +712,11 @@ export class SingleSideDiffElementViewModel extends DiffElementCellViewModelBase fontInfo: FontInfo | undefined; }, notebookService: INotebookService, - configurationService: IConfigurationService + configurationService: IConfigurationService, + unchangedRegionsService: IUnchangedEditorRegionsService, + index: number ) { - super(mainDocumentTextModel, original, modified, type, editorEventDispatcher, initData, notebookService, configurationService); + super(mainDocumentTextModel, original, modified, type, editorEventDispatcher, initData, notebookService, index, configurationService, unchangedRegionsService); this.type = type; this._register(this.cellViewModel.onDidChangeOutputLayout(() => { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index fe0f33e5e75..7c776ef9806 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -61,6 +61,30 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.diff.cell.toggleCollapseUnchangedRegions', + title: localize2('notebook.diff.cell.toggleCollapseUnchangedRegions', 'Toggle Collapse Unchanged Regions'), + icon: Codicon.map, + toggled: ContextKeyExpr.has('config.diffEditor.hideUnchangedRegions.enabled'), + precondition: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + menu: { + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + }, + }); + } + + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const configurationService = accessor.get(IConfigurationService); + const newValue = !configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); + configurationService.updateValue('diffEditor.hideUnchangedRegions.enabled', newValue); + } +}); + + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 79710bf55df..dadcdba8537 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -46,6 +46,10 @@ import { NotebookDiffOverviewRuler } from './notebookDiffOverviewRuler.js'; import { registerZIndex, ZIndex } from '../../../../../platform/layout/browser/zIndexRegistry.js'; import { NotebookDiffViewModel } from './notebookDiffViewModel.js'; import { INotebookService } from '../../common/notebookService.js'; +import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; +import { UnchangedEditorRegionsService } from './unchangedEditorRegions.js'; const $ = DOM.$; @@ -150,6 +154,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @INotebookService private readonly notebookService: INotebookService, + @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, + @ITextModelService private readonly textModelResolverService: ITextModelService, + @ITextResourceConfigurationService private readonly textConfigurationService: ITextResourceConfigurationService ) { super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, false, undefined); @@ -511,7 +518,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD })); if (this._model) { - const vm = this.notebookDiffViewModel = this._register(new NotebookDiffViewModel(this._model, this.notebookEditorWorkerService, this.configurationService, this._eventDispatcher!, this.notebookService, this.fontInfo)); + const unchangedEditorRegions = this._localStore.add(new UnchangedEditorRegionsService(this.configurationService, this.editorWorkerService, this.textModelResolverService, this.textConfigurationService, this.fontInfo.lineHeight)); + const vm = this.notebookDiffViewModel = this._register(new NotebookDiffViewModel(this._model, this.notebookEditorWorkerService, this.configurationService, this._eventDispatcher!, this.notebookService, unchangedEditorRegions, this.fontInfo, undefined)); this._localStore.add(this.notebookDiffViewModel.onDidChangeItems(e => { this._list.splice(e.start, e.deleteCount, e.elements); if (this.isOverviewRulerEnabled()) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts index 0d5c1f6fe1d..6c22cefa0ce 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts @@ -20,6 +20,7 @@ import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; import { CellUri, INotebookDiffEditorModel, INotebookDiffResult } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js'; +import { IUnchangedEditorRegionsService } from './unchangedEditorRegions.js'; export class NotebookDiffViewModel extends Disposable implements INotebookDiffViewModel, IValueWithChangeEvent { private readonly placeholderAndRelatedCells = new Map(); @@ -76,6 +77,7 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi private readonly configurationService: IConfigurationService, private readonly eventDispatcher: NotebookDiffEditorEventDispatcher, private readonly notebookService: INotebookService, + private readonly unchangedRegionsService: IUnchangedEditorRegionsService, private readonly fontInfo?: FontInfo, private readonly excludeUnchangedPlaceholder?: boolean, ) { @@ -134,7 +136,7 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi if (isEqual(cellDiffInfo, this.originalCellViewModels, this.model)) { return; } else { - this.updateViewModels(cellDiffInfo); + await this.updateViewModels(cellDiffInfo); this.updateDiffEditorItems(); return { firstChangeIndex }; } @@ -192,8 +194,8 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi this._onDidChange.fire(); } - private updateViewModels(cellDiffInfo: CellDiffInfo[]) { - const cellViewModels = createDiffViewModels(this.configurationService, this.model, this.eventDispatcher, cellDiffInfo, this.fontInfo, this.notebookService); + private async updateViewModels(cellDiffInfo: CellDiffInfo[]) { + const cellViewModels = await createDiffViewModels(this.configurationService, this.model, this.eventDispatcher, cellDiffInfo, this.fontInfo, this.notebookService, this.unchangedRegionsService); const oldLength = this._items.length; this.clear(); this._items.splice(0, oldLength); @@ -237,6 +239,8 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi } }); + // Note, ensure all of the height calculations are done before firing the event. + // This is to ensure that the diff editor is not resized multiple times, thereby avoiding flickering. this._onDidChangeItems.fire({ start: 0, deleteCount: oldLength, elements: this._items }); } } @@ -391,7 +395,7 @@ function isEqual(cellDiffInfo: CellDiffInfo[], viewModels: DiffElementCellViewMo return true; } -function createDiffViewModels(configurationService: IConfigurationService, model: INotebookDiffEditorModel, eventDispatcher: NotebookDiffEditorEventDispatcher, computedCellDiffs: CellDiffInfo[], fontInfo: FontInfo | undefined, notebookService: INotebookService) { +async function createDiffViewModels(configurationService: IConfigurationService, model: INotebookDiffEditorModel, eventDispatcher: NotebookDiffEditorEventDispatcher, computedCellDiffs: CellDiffInfo[], fontInfo: FontInfo | undefined, notebookService: INotebookService, unchangedRegionsService: IUnchangedEditorRegionsService) { const originalModel = model.original.notebook; const modifiedModel = model.modified.notebook; const initData = { @@ -399,8 +403,7 @@ function createDiffViewModels(configurationService: IConfigurationService, model outputStatusHeight: configurationService.getValue('notebook.diff.ignoreOutputs') || !!(modifiedModel.transientOptions.transientOutputs) ? 0 : 25, fontInfo }; - - return computedCellDiffs.map(diff => { + return Promise.all(computedCellDiffs.map(async (diff) => { switch (diff.type) { case 'delete': { return new SingleSideDiffElementViewModel( @@ -412,7 +415,9 @@ function createDiffViewModels(configurationService: IConfigurationService, model eventDispatcher, initData, notebookService, - configurationService + configurationService, + unchangedRegionsService, + diff.originalCellIndex ); } case 'insert': { @@ -425,11 +430,13 @@ function createDiffViewModels(configurationService: IConfigurationService, model eventDispatcher, initData, notebookService, - configurationService + configurationService, + unchangedRegionsService, + diff.modifiedCellIndex ); } case 'modified': { - return new SideBySideDiffElementViewModel( + const viewModel = new SideBySideDiffElementViewModel( model.modified.notebook, model.original.notebook, originalModel.cells[diff.originalCellIndex], @@ -438,8 +445,15 @@ function createDiffViewModels(configurationService: IConfigurationService, model eventDispatcher, initData, notebookService, - configurationService + configurationService, + diff.originalCellIndex, + unchangedRegionsService ); + // Reduces flicker (compute this before setting the model) + // Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y. + // & that results in flicker. + await viewModel.computeEditorHeights(); + return viewModel; } case 'unchanged': { return new SideBySideDiffElementViewModel( @@ -450,11 +464,13 @@ function createDiffViewModels(configurationService: IConfigurationService, model 'unchanged', eventDispatcher, initData, notebookService, - configurationService + configurationService, + diff.originalCellIndex, + unchangedRegionsService ); } } - }); + })); } function computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextModel, modifiedModel: NotebookTextModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts index 7f6f35699d0..3df5bc39b5e 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts @@ -39,6 +39,7 @@ import type { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from '../../ import type { URI } from '../../../../../base/common/uri.js'; import { type IDiffElementViewModelBase } from './diffElementViewModel.js'; import { autorun, transaction } from '../../../../../base/common/observable.js'; +import { UnchangedEditorRegionsService } from './unchangedEditorRegions.js'; export class NotebookMultiTextDiffEditor extends EditorPane { private _multiDiffEditorWidget?: MultiDiffEditorWidget; @@ -111,7 +112,7 @@ export class NotebookMultiTextDiffEditor extends EditorPane { this._model = model; } const eventDispatcher = this.modelSpecificResources.add(new NotebookDiffEditorEventDispatcher()); - this.viewModel = this.modelSpecificResources.add(new NotebookDiffViewModel(model, this.notebookEditorWorkerService, this.configurationService, eventDispatcher, this.notebookService, undefined, true)); + this.viewModel = this.modelSpecificResources.add(new NotebookDiffViewModel(model, this.notebookEditorWorkerService, this.configurationService, eventDispatcher, this.notebookService, UnchangedEditorRegionsService.Empty, undefined, true)); await this.viewModel.computeDiff(this.modelSpecificResources.add(new CancellationTokenSource()).token); this.ctxHasUnchangedCells.set(this.viewModel.hasUnchangedCells); this.ctxHasUnchangedCells.set(this.viewModel.hasUnchangedCells); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts b/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts new file mode 100644 index 00000000000..4d039c03b4e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { UnchangedRegion } from '../../../../../editor/browser/widget/diffEditor/diffEditorViewModel.js'; +import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { getEditorPadding } from './diffCellEditorOptions.js'; +import { HeightOfHiddenLinesRegionInDiffEditor } from './diffElementViewModel.js'; + +export type UnchangedEditorRegionOptions = { + enabled: boolean; + contextLineCount: number; + minimumLineCount: number; + revealLineCount: number; + onDidChangeEnablement: Event; +}; + +export interface IUnchangedEditorRegionsService { + readonly options: Readonly; + + /** + * Given two URIs, compute the height of the editor with unchanged regions collapsed. + * @param originalUri + * @param modifiedUri + */ + computeEditorHeight(originalUri: URI, modifiedUri: URI): Promise; +} + +export class UnchangedEditorRegionsService extends Disposable implements IUnchangedEditorRegionsService { + public readonly options: Readonly; + constructor(configurationService: IConfigurationService, + private readonly editorWorkerService: IEditorWorkerService, + private readonly textModelResolverService: ITextModelService, + private readonly textConfigurationService: ITextResourceConfigurationService, + private readonly lineHeight: number + ) { + super(); + this.options = this._register(createHideUnchangedRegionOptions(configurationService)); + } + + public static Empty: IUnchangedEditorRegionsService = { + options: { + enabled: false, + contextLineCount: 0, + minimumLineCount: 0, + revealLineCount: 0, + onDidChangeEnablement: Event.None, + }, + computeEditorHeight: (_originalUri: URI, _modifiedUri: URI) => Promise.resolve(0) + }; + + public async computeEditorHeight( + originalUri: URI, + modifiedUri: URI) { + const { numberOfUnchangedRegions, numberOfVisibleLines } = await computeInputUnchangedLines(originalUri, modifiedUri, this.options, this.editorWorkerService, this.textModelResolverService, this.textConfigurationService); + const lineCount = numberOfVisibleLines; + const unchangeRegionsHeight = numberOfUnchangedRegions * HeightOfHiddenLinesRegionInDiffEditor; + // TODO: When we have a horizontal scrollbar, we need to add 12 to the height. + // Right now there's no way to determine if a horizontal scrollbar is visible in the editor. + return lineCount * this.lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom + unchangeRegionsHeight; + + } +} + +function createHideUnchangedRegionOptions(configurationService: IConfigurationService): UnchangedEditorRegionOptions & { dispose: () => void } { + const disposables = new DisposableStore(); + const unchangedRegionsEnablementEmitter = disposables.add(new Emitter()); + + const options = { + enabled: configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'), + minimumLineCount: configurationService.getValue('diffEditor.hideUnchangedRegions.minimumLineCount'), + contextLineCount: configurationService.getValue('diffEditor.hideUnchangedRegions.contextLineCount'), + revealLineCount: configurationService.getValue('diffEditor.hideUnchangedRegions.revealLineCount'), + // We only care about enable/disablement. + // If user changes counters when a diff editor is open, we do not care, might as well ask user to reload. + // Simpler and almost never going to happen. + onDidChangeEnablement: unchangedRegionsEnablementEmitter.event.bind(unchangedRegionsEnablementEmitter), + dispose: () => disposables.dispose() + }; + + disposables.add(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.enabled')) { + options.enabled = configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); + unchangedRegionsEnablementEmitter.fire(options.enabled); + } + + })); + + return options; +} + +async function computeInputUnchangedLines(originalUri: URI, + modifiedUri: URI, + unchangedRegionOptions: UnchangedEditorRegionOptions, + editorWorkerService: IEditorWorkerService, + textModelResolverService: ITextModelService, + textConfigurationService: ITextResourceConfigurationService +) { + // Ensure we have resolved the cell text models. + const [originalModel, modifiedModel] = await Promise.all([textModelResolverService.createModelReference(originalUri), textModelResolverService.createModelReference(modifiedUri)]); + + try { + const ignoreTrimWhitespace = textConfigurationService.getValue(originalUri, 'diffEditor.ignoreTrimWhitespace'); + const diff = await editorWorkerService.computeDiff(originalUri, modifiedUri, { + ignoreTrimWhitespace, + maxComputationTimeMs: 0, + computeMoves: false + }, 'advanced'); + const originalLineCount = originalModel.object.textEditorModel.getLineCount(); + const modifiedLineCount = modifiedModel.object.textEditorModel.getLineCount(); + const unchanged = diff ? UnchangedRegion.fromDiffs(diff.changes, + originalLineCount, + modifiedLineCount, + unchangedRegionOptions.minimumLineCount ?? 3, + unchangedRegionOptions.contextLineCount ?? 3) : []; + + const totalLines = Math.max(originalLineCount, modifiedLineCount); + const numberOfUnchangedRegions = unchanged.length; + const numberOfVisibleLines = totalLines - unchanged.reduce((prev, curr) => prev + curr.lineCount, 0); + return { numberOfUnchangedRegions, numberOfVisibleLines }; + } finally { + originalModel.dispose(); + modifiedModel.dispose(); + } +} + diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index e9d86215e56..39a5468f12f 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -20,6 +20,7 @@ import { CellKind, INotebookTextModel } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js'; import { withTestNotebookDiffModel } from './testNotebookEditor.js'; +import { UnchangedEditorRegionsService } from '../../browser/diff/unchangedEditorRegions.js'; class CellSequence implements ISequence { @@ -87,7 +88,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 1); @@ -116,7 +117,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); await verifyChangeEventIsNotFired(diffViewModel); @@ -146,7 +147,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); await diffViewModel.computeDiff(token); @@ -195,7 +196,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 1); @@ -234,7 +235,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 1); @@ -257,7 +258,7 @@ suite('NotebookDiff', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); diffResult = diff.ComputeDiff(false); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); await diffViewModel.computeDiff(token); @@ -287,7 +288,7 @@ suite('NotebookDiff', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); diffResult = diff.ComputeDiff(false); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); await diffViewModel.computeDiff(token); @@ -325,7 +326,7 @@ suite('NotebookDiff', () => { quitEarly: false }; - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); const result = await diffViewModel.computeDiff(token); @@ -379,7 +380,7 @@ suite('NotebookDiff', () => { quitEarly: false }; - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); const result = await diffViewModel.computeDiff(token); @@ -441,7 +442,7 @@ suite('NotebookDiff', () => { quitEarly: false }; - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); await diffViewModel.computeDiff(token); @@ -611,7 +612,7 @@ suite('NotebookDiff', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); diffResult = diff.ComputeDiff(false); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 2); @@ -635,7 +636,7 @@ suite('NotebookDiff', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); diffResult = diff.ComputeDiff(false); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 2); From bffae5151e5d744751187869167958d981872441 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Tue, 10 Sep 2024 06:11:16 +0200 Subject: [PATCH 209/286] fix: memory leak in debug view (#225334) * fix: memory leak in debug view * Organize imports * fix: dispose element disposables * automatically dispose element disposables * simplify code * simplify code * format code --------- Co-authored-by: Rob Lourens --- .../contrib/debug/browser/breakpointsView.ts | 190 ++++++++++++------ 1 file changed, 125 insertions(+), 65 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index a5e9075c983..58dd961da31 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -20,7 +20,7 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { DisposableStore, IDisposable, dispose } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, dispose } from '../../../../base/common/lifecycle.js'; import * as resources from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Constants } from '../../../../base/common/uint.js'; @@ -39,6 +39,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -48,22 +49,21 @@ import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { IEditorPane } from '../../../common/editor.js'; import { IViewDescriptorService } from '../../../common/views.js'; -import * as icons from './debugIcons.js'; -import { DisassemblyView } from './disassemblyView.js'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, DEBUG_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from '../common/debug.js'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from '../common/debugModel.js'; import { DisassemblyViewInput } from '../common/disassemblyViewInput.js'; -import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; +import * as icons from './debugIcons.js'; +import { DisassemblyView } from './disassemblyView.js'; const $ = dom.$; -function createCheckbox(disposables: IDisposable[]): HTMLInputElement { +function createCheckbox(disposables: DisposableStore): HTMLInputElement { const checkbox = $('input'); checkbox.type = 'checkbox'; checkbox.tabIndex = -1; - disposables.push(Gesture.ignoreTarget(checkbox)); + disposables.add(Gesture.ignoreTarget(checkbox)); return checkbox; } @@ -432,7 +432,8 @@ interface IBaseBreakpointTemplateData { checkbox: HTMLInputElement; context: BreakpointItem; actionBar: ActionBar; - toDispose: IDisposable[]; + templateDisposables: DisposableStore; + elementDisposables: DisposableStore; badge: HTMLElement; } @@ -466,7 +467,8 @@ interface IFunctionBreakpointInputTemplateData { checkbox: HTMLInputElement; icon: HTMLElement; breakpoint: IFunctionBreakpoint; - toDispose: IDisposable[]; + templateDisposables: DisposableStore; + elementDisposables: DisposableStore; type: 'hitCount' | 'condition' | 'name'; updating?: boolean; } @@ -476,7 +478,8 @@ interface IDataBreakpointInputTemplateData { checkbox: HTMLInputElement; icon: HTMLElement; breakpoint: IDataBreakpoint; - toDispose: IDisposable[]; + elementDisposables: DisposableStore; + templateDisposables: DisposableStore; type: 'hitCount' | 'condition' | 'name'; updating?: boolean; } @@ -485,7 +488,8 @@ interface IExceptionBreakpointInputTemplateData { inputBox: InputBox; checkbox: HTMLInputElement; breakpoint: IExceptionBreakpoint; - toDispose: IDisposable[]; + templateDisposables: DisposableStore; + elementDisposables: DisposableStore; } const breakpointIdToActionBarDomeNode = new Map(); @@ -511,14 +515,16 @@ class BreakpointsRenderer implements IListRenderer { + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -529,7 +535,7 @@ class BreakpointsRenderer implements IListRenderer { + data.checkbox = createCheckbox(data.templateDisposables); + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -613,7 +627,7 @@ class ExceptionBreakpointsRenderer implements IListRenderer { + data.checkbox = createCheckbox(data.templateDisposables); + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -689,7 +709,7 @@ class FunctionBreakpointsRenderer implements IListRenderer { + data.checkbox = createCheckbox(data.templateDisposables); + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -778,7 +804,7 @@ class DataBreakpointsRenderer implements IListRenderer { + data.checkbox = createCheckbox(data.templateDisposables); + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -870,7 +902,7 @@ class InstructionBreakpointsRenderer implements IListRenderer { template.updating = true; try { @@ -967,7 +1006,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { const isEscape = e.equals(KeyCode.Escape); const isEnter = e.equals(KeyCode.Enter); if (isEscape || isEnter) { @@ -976,14 +1015,16 @@ class FunctionBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addDisposableListener(inputBox.inputElement, 'blur', () => { if (!template.updating) { wrapUp(!!inputBox.value); } })); template.inputBox = inputBox; - template.toDispose = toDispose; + template.elementDisposables = new DisposableStore(); + template.templateDisposables = toDispose; + template.templateDisposables.add(template.elementDisposables); return template; } @@ -993,7 +1034,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { template.updating = true; @@ -1076,7 +1122,7 @@ class DataBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { const isEscape = e.equals(KeyCode.Escape); const isEnter = e.equals(KeyCode.Enter); if (isEscape || isEnter) { @@ -1085,14 +1131,16 @@ class DataBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addDisposableListener(inputBox.inputElement, 'blur', () => { if (!template.updating) { wrapUp(!!inputBox.value); } })); template.inputBox = inputBox; - template.toDispose = toDispose; + template.elementDisposables = new DisposableStore(); + template.templateDisposables = toDispose; + template.templateDisposables.add(template.elementDisposables); return template; } @@ -1102,7 +1150,7 @@ class DataBreakpointInputRenderer implements IListRenderer { this.view.breakpointInputFocused.set(false); let newCondition = template.breakpoint.condition; @@ -1172,7 +1226,7 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { const isEscape = e.equals(KeyCode.Escape); const isEnter = e.equals(KeyCode.Enter); if (isEscape || isEnter) { @@ -1181,7 +1235,7 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addDisposableListener(inputBox.inputElement, 'blur', () => { // Need to react with a timeout on the blur event due to possible concurent splices #56443 setTimeout(() => { wrapUp(true); @@ -1189,7 +1243,9 @@ class ExceptionBreakpointInputRenderer implements IListRenderer Date: Tue, 10 Sep 2024 14:30:10 +1000 Subject: [PATCH 210/286] Use relative imports in cli.ts to import the cliProcessMaiin (#228044) * Use relative imports in CLI * ignore joins * revert to prevent inlining --- src/vs/code/node/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index ffd48670edc..b25377a804d 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -117,7 +117,7 @@ export async function main(argv: string[]): Promise { // Extensions Management else if (shouldSpawnCliProcess(args)) { - const cli = await import(['vs', 'code', 'node', 'cliProcessMain'].join('/') /* TODO@esm workaround to prevent esbuild from inlining this */); + const cli = await import(['./cliProcessMain.js'].join('/') /* TODO@esm workaround to prevent esbuild from inlining this */); await cli.main(args); return; From 789c320a1c67bb0547cdef2b8b82634bf93929a3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 10 Sep 2024 07:43:29 +0200 Subject: [PATCH 211/286] Replace mkdirp with fs.mkdirSync(path, { recursive: true }) (#228017) * Replace mkdirp with fs.mkdirSync(path, { recursive: true }) (fix #227931) * compile --- build/gulpfile.vscode.win32.js | 3 +-- build/lib/builtInExtensions.js | 3 +-- build/lib/builtInExtensions.ts | 4 +--- build/package-lock.json | 23 ------------------ build/package.json | 2 -- cglicenses.json | 27 ---------------------- package-lock.json | 1 - package.json | 1 - test/automation/package-lock.json | 22 ------------------ test/automation/package.json | 2 -- test/automation/src/electron.ts | 4 ++-- test/automation/src/playwrightBrowser.ts | 4 ++-- test/integration/browser/package-lock.json | 10 -------- test/integration/browser/package.json | 1 - test/smoke/package-lock.json | 22 ------------------ test/smoke/package.json | 2 -- test/smoke/src/main.ts | 9 ++++---- 17 files changed, 11 insertions(+), 129 deletions(-) diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 5adfdfbfe18..98175f530dd 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -16,7 +16,6 @@ const pkg = require('../package.json'); const product = require('../product.json'); const vfs = require('vinyl-fs'); const rcedit = require('rcedit'); -const mkdirp = require('mkdirp'); const repoPath = path.dirname(__dirname); const buildPath = (/** @type {string} */ arch) => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); @@ -75,7 +74,7 @@ function buildWin32Setup(arch, target) { const sourcePath = buildPath(arch); const outputPath = setupDir(arch, target); - mkdirp.sync(outputPath); + fs.mkdirSync(outputPath, { recursive: true }); const originalProductJsonPath = path.join(sourcePath, 'resources/app/product.json'); const productJsonPath = path.join(outputPath, 'product.json'); diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 463ce16e18d..ac784c03506 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -16,7 +16,6 @@ const vfs = require("vinyl-fs"); const ext = require("./extensions"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); -const mkdirp = require('mkdirp'); const root = path.dirname(path.dirname(__dirname)); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productjson.builtInExtensions || []; @@ -107,7 +106,7 @@ function readControlFile() { } } function writeControlFile(control) { - mkdirp.sync(path.dirname(controlFilePath)); + fs.mkdirSync(path.dirname(controlFilePath), { recursive: true }); fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } function getBuiltInExtensions() { diff --git a/build/lib/builtInExtensions.ts b/build/lib/builtInExtensions.ts index fefed436bb9..8b831d42d44 100644 --- a/build/lib/builtInExtensions.ts +++ b/build/lib/builtInExtensions.ts @@ -15,8 +15,6 @@ import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; import { Stream } from 'stream'; -const mkdirp = require('mkdirp'); - export interface IExtensionDefinition { name: string; version: string; @@ -147,7 +145,7 @@ function readControlFile(): IControlFile { } function writeControlFile(control: IControlFile): void { - mkdirp.sync(path.dirname(controlFilePath)); + fs.mkdirSync(path.dirname(controlFilePath), { recursive: true }); fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } diff --git a/build/package-lock.json b/build/package-lock.json index 9c423af3291..4499f7cf541 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -31,7 +31,6 @@ "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", - "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/node": "20.x", "@types/pump": "^1.0.1", @@ -54,7 +53,6 @@ "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", - "mkdirp": "^1.0.4", "source-map": "0.6.1", "ternary-stream": "^3.0.0", "through2": "^4.0.2", @@ -1124,15 +1122,6 @@ "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, - "node_modules/@types/mkdirp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", - "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/mocha": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", @@ -3353,18 +3342,6 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "devOptional": true }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", diff --git a/build/package.json b/build/package.json index 5e637eaebf4..7017b9144e0 100644 --- a/build/package.json +++ b/build/package.json @@ -25,7 +25,6 @@ "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", - "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/node": "20.x", "@types/pump": "^1.0.1", @@ -48,7 +47,6 @@ "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", - "mkdirp": "^1.0.4", "source-map": "0.6.1", "ternary-stream": "^3.0.0", "through2": "^4.0.2", diff --git a/cglicenses.json b/cglicenses.json index d75a53bf172..0b4e03e502b 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -241,33 +241,6 @@ "CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." ] }, - { - // Reason: The substack org has been deleted on GH - "name": "mkdirp", - "fullLicenseText": [ - "Copyright 2010 James Halliday (mail@substack.net)", - "", - "This project is free software released under the MIT/X11 license:", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in", - "all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN", - "THE SOFTWARE." - ] - }, { // Reason: repo URI is wrong on crate, pending https://github.com/warp-tech/russh/pull/53 "name": "russh-cryptovec", diff --git a/package-lock.json b/package-lock.json index f4a1e6017c2..de67f93ba3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -131,7 +131,6 @@ "mime": "^1.4.1", "minimatch": "^3.0.4", "minimist": "^1.2.6", - "mkdirp": "^1.0.4", "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", diff --git a/package.json b/package.json index 138ae5f7954..70050be5067 100644 --- a/package.json +++ b/package.json @@ -193,7 +193,6 @@ "mime": "^1.4.1", "minimatch": "^3.0.4", "minimist": "^1.2.6", - "mkdirp": "^1.0.4", "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", diff --git a/test/automation/package-lock.json b/test/automation/package-lock.json index 1a59eaa65f1..0253b826daf 100644 --- a/test/automation/package-lock.json +++ b/test/automation/package-lock.json @@ -9,14 +9,12 @@ "version": "1.71.0", "license": "MIT", "dependencies": { - "mkdirp": "^1.0.4", "ncp": "^2.0.0", "tmp": "0.2.1", "tree-kill": "1.2.2", "vscode-uri": "3.0.2" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/ncp": "2.0.1", "@types/node": "20.x", "@types/tmp": "0.2.2", @@ -25,15 +23,6 @@ "watch": "^1.0.2" } }, - "node_modules/@types/mkdirp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", - "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/ncp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/ncp/-/ncp-2.0.1.tgz", @@ -596,17 +585,6 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/test/automation/package.json b/test/automation/package.json index b9dbbec4bb8..4d5dbd02f63 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -18,14 +18,12 @@ "prepublishOnly": "npm run copy-package-version" }, "dependencies": { - "mkdirp": "^1.0.4", "ncp": "^2.0.0", "tmp": "0.2.1", "tree-kill": "1.2.2", "vscode-uri": "3.0.2" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/ncp": "2.0.1", "@types/node": "20.x", "@types/tmp": "0.2.2", diff --git a/test/automation/src/electron.ts b/test/automation/src/electron.ts index da2087a8faa..8a9a73974f6 100644 --- a/test/automation/src/electron.ts +++ b/test/automation/src/electron.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { join } from 'path'; -import * as mkdirp from 'mkdirp'; +import * as fs from 'fs'; import { copyExtension } from './extensions'; import { URI } from 'vscode-uri'; import { measureAndLog } from './logger'; @@ -70,7 +70,7 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom } args.push('--enable-proposed-api=vscode.vscode-test-resolver'); const remoteDataDir = `${userDataDir}-server`; - mkdirp.sync(remoteDataDir); + fs.mkdirSync(remoteDataDir, { recursive: true }); env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; env['TESTRESOLVER_LOGS_FOLDER'] = join(logsPath, 'server'); diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts index 0a98250767b..e36d070cdc9 100644 --- a/test/automation/src/playwrightBrowser.ts +++ b/test/automation/src/playwrightBrowser.ts @@ -6,7 +6,7 @@ import * as playwright from '@playwright/test'; import { ChildProcess, spawn } from 'child_process'; import { join } from 'path'; -import * as mkdirp from 'mkdirp'; +import * as fs from 'fs'; import { URI } from 'vscode-uri'; import { Logger, measureAndLog } from './logger'; import type { LaunchOptions } from './code'; @@ -35,7 +35,7 @@ async function launchServer(options: LaunchOptions) { const serverLogsPath = join(logsPath, 'server'); const codeServerPath = codePath ?? process.env.VSCODE_REMOTE_SERVER_PATH; const agentFolder = userDataDir; - await measureAndLog(() => mkdirp(agentFolder), `mkdirp(${agentFolder})`, logger); + await measureAndLog(() => fs.promises.mkdir(agentFolder, { recursive: true }), `mkdirp(${agentFolder})`, logger); const env = { VSCODE_REMOTE_SERVER_PATH: codeServerPath, diff --git a/test/integration/browser/package-lock.json b/test/integration/browser/package-lock.json index ec66b5554e9..bdec916fb4b 100644 --- a/test/integration/browser/package-lock.json +++ b/test/integration/browser/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/node": "20.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", @@ -42,15 +41,6 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, - "node_modules/@types/mkdirp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", - "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "20.11.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index e87c8669983..728d02763d1 100644 --- a/test/integration/browser/package.json +++ b/test/integration/browser/package.json @@ -7,7 +7,6 @@ "compile": "node ../../../node_modules/typescript/bin/tsc" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/node": "20.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", diff --git a/test/smoke/package-lock.json b/test/smoke/package-lock.json index 4932002dbe8..af2f72d890a 100644 --- a/test/smoke/package-lock.json +++ b/test/smoke/package-lock.json @@ -9,13 +9,11 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "mkdirp": "^1.0.4", "ncp": "^2.0.0", "node-fetch": "^2.6.7", "rimraf": "3.0.2" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/ncp": "2.0.1", "@types/node": "20.x", @@ -48,15 +46,6 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, - "node_modules/@types/mkdirp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", - "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/mocha": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", @@ -582,17 +571,6 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", diff --git a/test/smoke/package.json b/test/smoke/package.json index 1b4d142a1e7..90da59a0f53 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -11,13 +11,11 @@ "mocha": "node ../node_modules/mocha/bin/mocha" }, "dependencies": { - "mkdirp": "^1.0.4", "ncp": "^2.0.0", "node-fetch": "^2.6.7", "rimraf": "3.0.2" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/ncp": "2.0.1", "@types/node": "20.x", diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 4df38db9d72..d8120ea0675 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -10,7 +10,6 @@ import * as path from 'path'; import * as os from 'os'; import * as minimist from 'minimist'; import * as rimraf from 'rimraf'; -import * as mkdirp from 'mkdirp'; import * as vscodetest from '@vscode/test-electron'; import fetch from 'node-fetch'; import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog, getDevElectronPath, getBuildElectronPath, getBuildVersion } from '../../automation'; @@ -105,7 +104,7 @@ function createLogger(): Logger { // Prepare logs rot path fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 3 }); - mkdirp.sync(logsRootPath); + fs.mkdirSync(logsRootPath, { recursive: true }); // Always log to log file loggers.push(new FileLogger(path.join(logsRootPath, 'smoke-test-runner.log'))); @@ -123,7 +122,7 @@ const testDataPath = path.join(os.tmpdir(), 'vscsmoke'); if (fs.existsSync(testDataPath)) { rimraf.sync(testDataPath); } -mkdirp.sync(testDataPath); +fs.mkdirSync(testDataPath, { recursive: true }); process.once('exit', () => { try { rimraf.sync(testDataPath); @@ -135,7 +134,7 @@ process.once('exit', () => { const testRepoUrl = 'https://github.com/microsoft/vscode-smoketest-express'; const workspacePath = path.join(testDataPath, 'vscode-smoketest-express'); const extensionsPath = path.join(testDataPath, 'extensions-dir'); -mkdirp.sync(extensionsPath); +fs.mkdirSync(extensionsPath, { recursive: true }); function fail(errorMessage): void { logger.log(errorMessage); @@ -179,7 +178,7 @@ function parseQuality(): Quality { // if (!opts.web) { let testCodePath = opts.build; - let electronPath: string; + let electronPath: string | undefined; if (testCodePath) { electronPath = getBuildElectronPath(testCodePath); From fea0aa298b63a72a5fd50b9f647773d13ec0c33d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 08:45:20 +0200 Subject: [PATCH 212/286] Git - only calculate branch merge base on checkout (#228066) --- extensions/git/src/historyProvider.ts | 64 ++++++++++++++++++--------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 872582dae6e..5a48c70686c 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -62,7 +62,10 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this._onDidChangeCurrentHistoryItemGroup.fire(); } + private _HEAD: Branch | undefined; private historyItemRefs: SourceControlHistoryItemRef[] = []; + private historyItemBaseRef: SourceControlHistoryItemRef | undefined; + private historyItemDecorations = new Map(); private disposables: Disposable[] = []; @@ -79,37 +82,56 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return; } - // Get the merge base of the current history item group - const mergeBase = await this.resolveHEADMergeBase(); + let historyItemRefId = ''; + let historyItemRefName = ''; - // Handle tag, and detached commit - const currentHistoryItemGroupId = - this.repository.HEAD.name === undefined ? - this.repository.HEAD.commit : - this.repository.HEAD.type === RefType.Tag ? - `refs/tags/${this.repository.HEAD.name}` : - `refs/heads/${this.repository.HEAD.name}`; + switch (this.repository.HEAD.type) { + case RefType.Head: { + if (this.repository.HEAD.name !== undefined) { + // Branch + historyItemRefId = `refs/heads/${this.repository.HEAD.name}`; + historyItemRefName = this.repository.HEAD.name; - // Detached commit - const currentHistoryItemGroupName = - this.repository.HEAD.name ?? this.repository.HEAD.commit; + // Merge base if the branch has changed + if (this._HEAD?.name !== this.repository.HEAD.name) { + const mergeBase = await this.resolveHEADMergeBase(); + this.historyItemBaseRef = mergeBase && + (mergeBase.remote !== this.repository.HEAD.upstream?.remote || + mergeBase.name !== this.repository.HEAD.upstream?.name) ? { + id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, + name: `${mergeBase.remote}/${mergeBase.name}`, + revision: mergeBase.commit + } : undefined; + } + } else { + // Detached commit + historyItemRefId = this.repository.HEAD.commit ?? ''; + historyItemRefName = this.repository.HEAD.commit ?? ''; + this.historyItemBaseRef = undefined; + } + break; + } + case RefType.Tag: { + // Tag + historyItemRefId = `refs/tags/${this.repository.HEAD.name}`; + historyItemRefName = this.repository.HEAD.name ?? this.repository.HEAD.commit ?? ''; + this.historyItemBaseRef = undefined; + break; + } + } + + this._HEAD = this.repository.HEAD; this.currentHistoryItemGroup = { - id: currentHistoryItemGroupId ?? '', - name: currentHistoryItemGroupName ?? '', + id: historyItemRefId, + name: historyItemRefName, revision: this.repository.HEAD.commit, remote: this.repository.HEAD.upstream ? { id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, revision: this.repository.HEAD.upstream.commit } : undefined, - base: mergeBase && - (mergeBase.remote !== this.repository.HEAD.upstream?.remote || - mergeBase.name !== this.repository.HEAD.upstream?.name) ? { - id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, - name: `${mergeBase.remote}/${mergeBase.name}`, - revision: mergeBase.commit - } : undefined + base: this.historyItemBaseRef }; this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup: ${JSON.stringify(this.currentHistoryItemGroup)}`); From 5921ef8b553ff004493010e8af64f26158b1dd1b Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 10 Sep 2024 16:03:14 +0900 Subject: [PATCH 213/286] chore: increase heap size for client watch commands (#228068) --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 70050be5067..72970d62570 100644 --- a/package.json +++ b/package.json @@ -28,11 +28,11 @@ "kill-watch-webd": "deemon --kill npm run watch-web", "restart-watchd": "deemon --restart npm run watch", "restart-watch-webd": "deemon --restart npm run watch-web", - "watch-client": "node ./node_modules/gulp/bin/gulp.js watch-client", - "watch-client-amd": "node ./node_modules/gulp/bin/gulp.js watch-client-amd", + "watch-client": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-client", + "watch-client-amd": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-client-amd", "watch-clientd": "deemon npm run watch-client", "kill-watch-clientd": "deemon --kill npm run watch-client", - "watch-extensions": "node ./node_modules/gulp/bin/gulp.js watch-extensions watch-extension-media", + "watch-extensions": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-extensions watch-extension-media", "watch-extensionsd": "deemon npm run watch-extensions", "kill-watch-extensionsd": "deemon --kill npm run watch-extensions", "precommit": "node build/hygiene.js", From 4523f644ac7c76fb10bdfeb166566fccfaae8594 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:08:19 +0200 Subject: [PATCH 214/286] SCM - fix badge spacing in the history item hover (#228069) --- src/vs/workbench/contrib/scm/browser/media/scm.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 1e6e2a8d9e2..8c637576613 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -473,7 +473,11 @@ } .monaco-hover.history-item-hover p:last-child { - margin-bottom: 4px; + margin-bottom: 0; +} + +.monaco-hover.history-item-hover p:last-child span:not(.codicon) { + margin-bottom: 2px !important; } .monaco-hover.history-item-hover hr { @@ -485,7 +489,7 @@ margin: 4px 0; } -.monaco-hover.history-item-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span:not(.codicon) { +.monaco-hover.history-item-hover span:not(.codicon) { margin-bottom: 0 !important; } From e96c90306bc1c10ed35fa817b178eba3fed12052 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:15:17 +0200 Subject: [PATCH 215/286] SCM - Add history item hover action (#228072) --- .../contrib/scm/browser/media/scm.css | 10 ++++++ .../contrib/scm/browser/scmHistoryViewPane.ts | 31 ++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 8c637576613..2306e5e0c50 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -493,6 +493,16 @@ margin-bottom: 0 !important; } +.monaco-hover.history-item-hover .hover-row.status-bar .action { + display: flex; + align-items: center; +} + +.monaco-hover.history-item-hover .hover-row.status-bar .action .codicon { + color: inherit; + font-size: 12px; +} + /* Graph */ .pane-header .scm-graph-view-badge-container { diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 828e8d60f33..476e480f9e5 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -60,6 +60,7 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { clamp } from '../../../../base/common/numbers.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { compare } from '../../../../base/common/strings.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; @@ -292,6 +293,7 @@ class HistoryItemRenderer implements ITreeRenderer this._clipboardService.writeText(historyItem.id) + }, + { + commandId: 'workbench.scm.action.copyHistoryItemMessage', + iconClass: 'codicon.codicon-copy', + label: localize('historyItemMessage', "Message"), + run: () => this._clipboardService.writeText(historyItem.message) + } + ]; + } + + private _getHoverContent(element: SCMHistoryItemViewModelTreeElement): IManagedHoverTooltipMarkdownString { const colorTheme = this._themeService.getColorTheme(); const historyItem = element.historyItemViewModel.historyItem; const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); - markdown.appendMarkdown(`$(git-commit) \`${historyItem.displayId ?? historyItem.id}\`\n\n`); + // markdown.appendMarkdown(`$(git-commit) \`${historyItem.displayId ?? historyItem.id}\`\n\n`); if (historyItem.author) { markdown.appendMarkdown(`$(account) **${historyItem.author}**`); @@ -421,7 +442,7 @@ class HistoryItemRenderer implements ITreeRenderer Date: Tue, 10 Sep 2024 11:17:57 +0200 Subject: [PATCH 216/286] SCM Graph - more improvements to refresh (#228079) --- .../contrib/scm/browser/scmHistoryViewPane.ts | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 476e480f9e5..a3c6bea513b 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -32,7 +32,7 @@ import { asCssVariable, ColorIdentifier, foreground } from '../../../../platform import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; -import { renderSCMHistoryItemGraph, historyItemGroupLocal, historyItemGroupRemote, historyItemGroupBase, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverDeletionsForeground, historyItemHoverLabelForeground, historyItemHoverAdditionsForeground, historyItemHoverDefaultLabelForeground, historyItemHoverDefaultLabelBackground } from './scmHistory.js'; +import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverDeletionsForeground, historyItemHoverLabelForeground, historyItemHoverAdditionsForeground, historyItemHoverDefaultLabelForeground, historyItemHoverDefaultLabelBackground } from './scmHistory.js'; import { isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js'; import { ISCMHistoryItem, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js'; @@ -801,16 +801,20 @@ class SCMHistoryViewModel extends Disposable { private _getGraphColorMap(historyItemRefs: ISCMHistoryItemRef[]): Map { const repository = this.repository.get(); const historyProvider = repository?.provider.historyProvider.get(); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get(); + const historyItemRef = historyProvider?.currentHistoryItemRef.get(); + const historyItemRemoteRef = historyProvider?.currentHistoryItemRemoteRef.get(); + const historyItemBaseRef = historyProvider?.currentHistoryItemBaseRef.get(); const colorMap = new Map(); - if (currentHistoryItemGroup) { - colorMap.set(currentHistoryItemGroup.id, historyItemGroupLocal); - if (currentHistoryItemGroup.remote) { - colorMap.set(currentHistoryItemGroup.remote.id, historyItemGroupRemote); + + if (historyItemRef) { + colorMap.set(historyItemRef.id, historyItemRef.color); + + if (historyItemRemoteRef) { + colorMap.set(historyItemRemoteRef.id, historyItemRemoteRef.color); } - if (currentHistoryItemGroup.base) { - colorMap.set(currentHistoryItemGroup.base.id, historyItemGroupBase); + if (historyItemBaseRef) { + colorMap.set(historyItemBaseRef.id, historyItemBaseRef.color); } } @@ -1101,18 +1105,15 @@ export class SCMHistoryViewPane extends ViewPane { // Update context this._scmProviderCtx.set(repository.provider.contextValue); - // Commit, Checkout - const historyItemRefSignal = signalFromObservable(this, historyProvider.currentHistoryItemRef); - // Publish - const historyItemRefRemoteIdSignal = signalFromObservable(this, derived(reader => { + const historyItemRemoteRefIdSignal = signalFromObservable(this, derived(reader => { return historyProvider.currentHistoryItemRemoteRef.read(reader)?.id; })); // Fetch, Push - const historyItemRefRemoteRevisionSignal = signalFromObservable(this, derived(reader => { + const historyItemRemoteRefRevision = derived(reader => { return historyProvider.currentHistoryItemRemoteRef.read(reader)?.revision; - })); + }); // HistoryItemRefs changed store.add( @@ -1120,23 +1121,33 @@ export class SCMHistoryViewPane extends ViewPane { owner: this, createEmptyChangeSummary: () => ({ refresh: false }), handleChange(context, changeSummary) { - changeSummary.refresh = context.didChange(historyItemRefRemoteRevisionSignal) ? 'ifScrollTop' : true; + changeSummary.refresh = context.didChange(historyItemRemoteRefRevision) ? 'ifScrollTop' : true; return true; }, }, (reader, changeSummary) => { - historyItemRefSignal.read(reader); - historyItemRefRemoteIdSignal.read(reader); - historyItemRefRemoteRevisionSignal.read(reader); + historyItemRemoteRefIdSignal.read(reader); + const historyItemRefValue = historyProvider.currentHistoryItemRef.read(reader); + const historyItemRemoteRefRevisionValue = historyItemRemoteRefRevision.read(reader); + // Commit, Checkout, Publish, Pull if (changeSummary.refresh === true) { this.refresh(); return; } if (changeSummary.refresh === 'ifScrollTop') { - // Remote revision changes can occur as a result of a user action (Fetch, Push) but - // it can also occur as a result of background action (Auto Fetch). If the tree is - // scrolled to the top, we can safely refresh the tree. + // If the history item remote revision has changed, but it matches the history + // item revision, then it means that a Push operation was performed and it is + // safe to refresh the graph. + if (historyItemRefValue?.revision === historyItemRemoteRefRevisionValue) { + this.refresh(); + return; + } + + // If the history item remote revision has changed, but it does not matches the + // history item revision, then a Fetch operation was performed. This can be the + // result of a user action (Fetch) or a background action (Auto Fetch). If the + // tree is scrolled to the top, we can safely refresh the tree. if (this._tree.scrollTop === 0) { this.refresh(); return; From 62f8880800eb82f642cd7b0d7b5122e5e6574160 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:07:37 +0200 Subject: [PATCH 217/286] SCM Graph - add progress indicator for initial rendering (#228084) --- .../contrib/scm/browser/scmHistoryViewPane.ts | 155 +++++++++--------- 1 file changed, 80 insertions(+), 75 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index a3c6bea513b..52a30c7f6ce 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -326,8 +326,8 @@ class HistoryItemRenderer implements ITreeRenderer { - if (visible) { - this._treeViewModel = this.instantiationService.createInstance(SCMHistoryViewModel); - this._visibilityDisposables.add(this._treeViewModel); + if (!visible) { + this._visibilityDisposables.clear(); + return; + } + // Create view model + this._treeViewModel = this.instantiationService.createInstance(SCMHistoryViewModel); + this._visibilityDisposables.add(this._treeViewModel); + + // Initial rendering + await this._progressService.withProgress({ location: this.id }, async () => { const firstRepositoryInitialized = derived(this, reader => { const repository = this._treeViewModel.repository.read(reader); const historyProvider = repository?.provider.historyProvider.read(reader); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.read(reader); + const historyItemRef = historyProvider?.currentHistoryItemRef.read(reader); - return currentHistoryItemGroup !== undefined ? repository : undefined; + return historyItemRef !== undefined ? true : undefined; }); // Wait for first repository to be initialized await waitForState(firstRepositoryInitialized); // Set tree input - this._treeOperationSequencer.queue(async () => { + await this._treeOperationSequencer.queue(async () => { await this._tree.setInput(this._treeViewModel); this._tree.scrollTop = 0; }); + }); - // Repository change - let isFirstRun = true; - this._visibilityDisposables.add(autorunWithStore((reader, store) => { - const repository = this._treeViewModel.repository.read(reader); - const historyProvider = repository?.provider.historyProvider.read(reader); - if (!repository || !historyProvider) { - return; - } + // Repository change + let isFirstRun = true; + this._visibilityDisposables.add(autorunWithStore((reader, store) => { + const repository = this._treeViewModel.repository.read(reader); + const historyProvider = repository?.provider.historyProvider.read(reader); + if (!repository || !historyProvider) { + return; + } - // Update context - this._scmProviderCtx.set(repository.provider.contextValue); + // Update context + this._scmProviderCtx.set(repository.provider.contextValue); - // Publish - const historyItemRemoteRefIdSignal = signalFromObservable(this, derived(reader => { - return historyProvider.currentHistoryItemRemoteRef.read(reader)?.id; - })); + // Publish + const historyItemRemoteRefIdSignal = signalFromObservable(this, derived(reader => { + return historyProvider.currentHistoryItemRemoteRef.read(reader)?.id; + })); - // Fetch, Push - const historyItemRemoteRefRevision = derived(reader => { - return historyProvider.currentHistoryItemRemoteRef.read(reader)?.revision; - }); + // Fetch, Push + const historyItemRemoteRefRevision = derived(reader => { + return historyProvider.currentHistoryItemRemoteRef.read(reader)?.revision; + }); - // HistoryItemRefs changed - store.add( - autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({ - owner: this, - createEmptyChangeSummary: () => ({ refresh: false }), - handleChange(context, changeSummary) { - changeSummary.refresh = context.didChange(historyItemRemoteRefRevision) ? 'ifScrollTop' : true; - return true; - }, - }, (reader, changeSummary) => { - historyItemRemoteRefIdSignal.read(reader); - const historyItemRefValue = historyProvider.currentHistoryItemRef.read(reader); - const historyItemRemoteRefRevisionValue = historyItemRemoteRefRevision.read(reader); + // HistoryItemRefs changed + store.add( + autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({ + owner: this, + createEmptyChangeSummary: () => ({ refresh: false }), + handleChange(context, changeSummary) { + changeSummary.refresh = context.didChange(historyItemRemoteRefRevision) ? 'ifScrollTop' : true; + return true; + }, + }, (reader, changeSummary) => { + historyItemRemoteRefIdSignal.read(reader); + const historyItemRefValue = historyProvider.currentHistoryItemRef.read(reader); + const historyItemRemoteRefRevisionValue = historyItemRemoteRefRevision.read(reader); - // Commit, Checkout, Publish, Pull - if (changeSummary.refresh === true) { + // Commit, Checkout, Publish, Pull + if (changeSummary.refresh === true) { + this.refresh(); + return; + } + + if (changeSummary.refresh === 'ifScrollTop') { + // If the history item remote revision has changed, but it matches the history + // item revision, then it means that a Push operation was performed and it is + // safe to refresh the graph. + if (historyItemRefValue?.revision === historyItemRemoteRefRevisionValue) { this.refresh(); return; } - if (changeSummary.refresh === 'ifScrollTop') { - // If the history item remote revision has changed, but it matches the history - // item revision, then it means that a Push operation was performed and it is - // safe to refresh the graph. - if (historyItemRefValue?.revision === historyItemRemoteRefRevisionValue) { - this.refresh(); - return; - } - - // If the history item remote revision has changed, but it does not matches the - // history item revision, then a Fetch operation was performed. This can be the - // result of a user action (Fetch) or a background action (Auto Fetch). If the - // tree is scrolled to the top, we can safely refresh the tree. - if (this._tree.scrollTop === 0) { - this.refresh(); - return; - } - - // Show the "Outdated" badge on the view - this._repositoryOutdated.set(true, undefined); + // If the history item remote revision has changed, but it does not matches the + // history item revision, then a Fetch operation was performed. This can be the + // result of a user action (Fetch) or a background action (Auto Fetch). If the + // tree is scrolled to the top, we can safely refresh the tree. + if (this._tree.scrollTop === 0) { + this.refresh(); + return; } - })); - // HistoryItemRefs filter changed - store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => { - this.refresh(); + // Show the "Outdated" badge on the view + this._repositoryOutdated.set(true, undefined); + } })); - // We skip refreshing the graph on the first execution of the autorun - // since the graph for the first repository is rendered when the tree - // input is set. - if (!isFirstRun) { - this.refresh(); - } - isFirstRun = false; + // HistoryItemRefs filter changed + store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => { + this.refresh(); })); - } else { - this._visibilityDisposables.clear(); - } + + // We skip refreshing the graph on the first execution of the autorun + // since the graph for the first repository is rendered when the tree + // input is set. + if (!isFirstRun) { + this.refresh(); + } + isFirstRun = false; + })); }); } From c2f7ee51b3c2fc25d94a909533d72b5f5eb3b1ff Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 10 Sep 2024 12:11:22 +0200 Subject: [PATCH 218/286] chat input completions must use more honest filterText (#228086) --- .../contrib/chat/browser/contrib/chatInputCompletions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 60a91948272..231e5d2bff3 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -383,7 +383,7 @@ class BuiltinDynamicCompletions extends Disposable { return { label: { label: basename, description: this.labelService.getUriLabel(resource) }, - filterText: info.varWord?.word, + filterText: `${chatVariableLeader}${basename}`, insertText, range: info, kind: CompletionItemKind.File, From b1d3ce462b164ffe0115b616fbcf702dddd16744 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:33:18 +0200 Subject: [PATCH 219/286] SCM Graph - fix action ids to be consistent with other scm workbench actions (#228091) --- .../contrib/scm/browser/scmHistoryViewPane.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 52a30c7f6ce..0b9c0b6bcbf 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -62,6 +62,9 @@ import { observableConfigValue } from '../../../../platform/observable/common/pl import { compare } from '../../../../base/common/strings.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +const PICK_REPOSITORY_ACTION_ID = 'workbench.scm.action.graph.pickRepository'; +const PICK_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs'; + type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; class SCMRepositoryActionViewItem extends ActionViewItem { @@ -144,7 +147,7 @@ class SCMHistoryItemRefsActionViewItem extends ActionViewItem { registerAction2(class extends ViewAction { constructor() { super({ - id: 'workbench.scm.graph.action.pickRepository', + id: PICK_REPOSITORY_ACTION_ID, title: '', viewId: HISTORY_VIEW_PANE_ID, f1: false, @@ -165,7 +168,7 @@ registerAction2(class extends ViewAction { registerAction2(class extends ViewAction { constructor() { super({ - id: 'workbench.scm.graph.action.pickHistoryItemRefs', + id: PICK_HISTORY_ITEM_REFS_ACTION_ID, title: '', icon: Codicon.gitBranch, viewId: HISTORY_VIEW_PANE_ID, @@ -187,7 +190,7 @@ registerAction2(class extends ViewAction { registerAction2(class extends ViewAction { constructor() { super({ - id: 'workbench.scm.action.refreshGraph', + id: 'workbench.scm.action.graph.refresh', title: localize('refreshGraph', "Refresh"), viewId: HISTORY_VIEW_PANE_ID, f1: false, @@ -208,7 +211,7 @@ registerAction2(class extends ViewAction { registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.scm.action.scm.viewChanges', + id: 'workbench.scm.action.graph.viewChanges', title: localize('viewChanges', "View Changes"), f1: false, menu: [ @@ -371,13 +374,13 @@ class HistoryItemRenderer implements ITreeRenderer this._clipboardService.writeText(historyItem.id) }, { - commandId: 'workbench.scm.action.copyHistoryItemMessage', + commandId: 'workbench.scm.action.graph.copyHistoryItemMessage', iconClass: 'codicon.codicon-copy', label: localize('historyItemMessage', "Message"), run: () => this._clipboardService.writeText(historyItem.message) @@ -1196,12 +1199,12 @@ export class SCMHistoryViewPane extends ViewPane { } override getActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined { - if (action.id === 'workbench.scm.graph.action.pickRepository') { + if (action.id === PICK_REPOSITORY_ACTION_ID) { const repository = this._treeViewModel?.repository.get(); if (repository) { return new SCMRepositoryActionViewItem(repository, action, options); } - } else if (action.id === 'workbench.scm.graph.action.pickHistoryItemRefs') { + } else if (action.id === PICK_HISTORY_ITEM_REFS_ACTION_ID) { const repository = this._treeViewModel?.repository.get(); const historyItemsFilter = this._treeViewModel?.historyItemsFilter.get(); if (repository && historyItemsFilter) { From 3f0e1da18456e116e612649854d626741a03962e Mon Sep 17 00:00:00 2001 From: BABA <38986298+BABA983@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:25:11 +0800 Subject: [PATCH 220/286] Register fold import action (#227216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Register fold import action * Get regions from foldingModel * Rename toggle import fold action * 💄 --------- Co-authored-by: Martin Aeschlimann --- .../editor/contrib/folding/browser/folding.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 2380c725389..ee657d44e14 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -1231,6 +1231,35 @@ class RemoveFoldRangeFromSelectionAction extends FoldingAction { } +class ToggleImportFoldAction extends FoldingAction { + + constructor() { + super({ + id: 'editor.toggleImportFold', + label: nls.localize('toggleImportFold.label', "Toggle Import Fold"), + alias: 'Toggle Import Fold', + precondition: CONTEXT_FOLDING_ENABLED, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + weight: KeybindingWeight.EditorContrib + } + }); + } + + async invoke(foldingController: FoldingController, foldingModel: FoldingModel): Promise { + const regionsToToggle: FoldingRegion[] = []; + const regions = foldingModel.regions; + for (let i = regions.length - 1; i >= 0; i--) { + if (regions.getType(i) === FoldingRangeKind.Imports.value) { + regionsToToggle.push(regions.toRegion(i)); + } + } + foldingModel.toggleCollapseState(regionsToToggle); + foldingController.triggerFoldingModelChanged(); + } +} + + registerEditorContribution(FoldingController.ID, FoldingController, EditorContributionInstantiation.Eager); // eager because it uses `saveViewState`/`restoreViewState` registerEditorAction(UnfoldAction); registerEditorAction(UnFoldRecursivelyAction); @@ -1250,6 +1279,7 @@ registerEditorAction(GotoPreviousFoldAction); registerEditorAction(GotoNextFoldAction); registerEditorAction(FoldRangeFromSelectionAction); registerEditorAction(RemoveFoldRangeFromSelectionAction); +registerEditorAction(ToggleImportFoldAction); for (let i = 1; i <= 7; i++) { registerInstantiatedEditorAction( From c2c032e0da150de8222a0e7f53e3bb6f2891eb0f Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:46:12 +0200 Subject: [PATCH 221/286] =?UTF-8?q?SCM=20-=20=F0=9F=92=84=20remove=20Sourc?= =?UTF-8?q?eControlHistoryItemGroup=20from=20the=20API=20proposal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/git/src/commands.ts | 4 +- extensions/git/src/decorationProvider.ts | 13 +-- extensions/git/src/historyProvider.ts | 68 ++++++++------ src/vs/workbench/api/browser/mainThreadSCM.ts | 88 +++++++------------ .../workbench/api/common/extHost.protocol.ts | 10 +-- src/vs/workbench/api/common/extHostSCM.ts | 14 ++- .../workbench/contrib/scm/browser/activity.ts | 4 +- .../contrib/scm/browser/scmHistory.ts | 12 +-- .../contrib/scm/browser/scmHistoryViewPane.ts | 28 +++--- .../contrib/scm/browser/workingSet.ts | 18 ++-- .../workbench/contrib/scm/common/history.ts | 16 +--- .../scm/test/browser/scmHistory.test.ts | 36 ++++---- .../vscode.proposed.scmHistoryProvider.d.ts | 18 ++-- 13 files changed, 149 insertions(+), 180 deletions(-) diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index d80a203b5c3..2b4e7d2dfaa 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3059,7 +3059,7 @@ export class CommandCenter { @command('git.fetchRef', { repository: true }) async fetchRef(repository: Repository, ref?: string): Promise { - ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; + ref = ref ?? repository?.historyProvider.currentHistoryItemRemoteRef?.id; if (!repository || !ref) { return; } @@ -3132,7 +3132,7 @@ export class CommandCenter { @command('git.pullRef', { repository: true }) async pullRef(repository: Repository, ref?: string): Promise { - ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; + ref = ref ?? repository?.historyProvider.currentHistoryItemRemoteRef?.id; if (!repository || !ref) { return; } diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 6de2cfae31d..f1c675ceff1 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -164,11 +164,11 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider constructor(private readonly repository: Repository) { this.disposables.push( window.registerFileDecorationProvider(this), - runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemGroup, () => this.onDidChangeCurrentHistoryItemGroup()) + runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemRefs, () => this.onDidChangeCurrentHistoryItemRefs()) ); } - private async onDidChangeCurrentHistoryItemGroup(): Promise { + private async onDidChangeCurrentHistoryItemRefs(): Promise { const newDecorations = new Map(); await this.collectIncomingChangesFileDecorations(newDecorations); const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); @@ -218,18 +218,19 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider private async getIncomingChanges(): Promise { try { const historyProvider = this.repository.historyProvider; - const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; + const currentHistoryItemRef = historyProvider.currentHistoryItemRef; + const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef; - if (!currentHistoryItemGroup?.remote) { + if (!currentHistoryItemRef || !currentHistoryItemRemoteRef) { return []; } - const ancestor = await historyProvider.resolveHistoryItemRefsCommonAncestor([currentHistoryItemGroup.id, currentHistoryItemGroup.remote.id]); + const ancestor = await historyProvider.resolveHistoryItemRefsCommonAncestor([currentHistoryItemRef.id, currentHistoryItemRemoteRef.id]); if (!ancestor) { return []; } - const changes = await this.repository.diffBetween(ancestor, currentHistoryItemGroup.remote.id); + const changes = await this.repository.diffBetween(ancestor, currentHistoryItemRemoteRef.id); return changes; } catch (err) { return []; diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 5a48c70686c..3d115d8c116 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; import { IDisposable, deltaHistoryItemRefs, dispose } from './util'; import { toGitUri } from './uri'; @@ -45,22 +45,23 @@ function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { } export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { - - private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter(); - readonly onDidChangeCurrentHistoryItemGroup: Event = this._onDidChangeCurrentHistoryItemGroup.event; - - private readonly _onDidChangeHistoryItemRefs = new EventEmitter(); - readonly onDidChangeHistoryItemRefs: Event = this._onDidChangeHistoryItemRefs.event; - private readonly _onDidChangeDecorations = new EventEmitter(); readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; - private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined; - get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } - set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) { - this._currentHistoryItemGroup = value; - this._onDidChangeCurrentHistoryItemGroup.fire(); - } + private _currentHistoryItemRef: SourceControlHistoryItemRef | undefined; + get currentHistoryItemRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemRef; } + + private _currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined; + get currentHistoryItemRemoteRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemRemoteRef; } + + private _currentHistoryItemBaseRef: SourceControlHistoryItemRef | undefined; + get currentHistoryItemBaseRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemBaseRef; } + + private readonly _onDidChangeCurrentHistoryItemRefs = new EventEmitter(); + readonly onDidChangeCurrentHistoryItemRefs: Event = this._onDidChangeCurrentHistoryItemRefs.event; + + private readonly _onDidChangeHistoryItemRefs = new EventEmitter(); + readonly onDidChangeHistoryItemRefs: Event = this._onDidChangeHistoryItemRefs.event; private _HEAD: Branch | undefined; private historyItemRefs: SourceControlHistoryItemRef[] = []; @@ -78,7 +79,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private async onDidRunGitStatus(): Promise { if (!this.repository.HEAD) { this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] repository.HEAD is undefined'); - this.currentHistoryItemGroup = undefined; + this._currentHistoryItemRef = this._currentHistoryItemRemoteRef = this._currentHistoryItemBaseRef = undefined; + this._onDidChangeCurrentHistoryItemRefs.fire(); + return; } @@ -122,19 +125,26 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this._HEAD = this.repository.HEAD; - this.currentHistoryItemGroup = { + this._currentHistoryItemRef = { id: historyItemRefId, name: historyItemRefName, revision: this.repository.HEAD.commit, - remote: this.repository.HEAD.upstream ? { - id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - revision: this.repository.HEAD.upstream.commit - } : undefined, - base: this.historyItemBaseRef + icon: new ThemeIcon('target'), }; - this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup: ${JSON.stringify(this.currentHistoryItemGroup)}`); + this._currentHistoryItemRemoteRef = this.repository.HEAD.upstream ? { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + revision: this.repository.HEAD.upstream.commit, + icon: new ThemeIcon('cloud') + } : undefined; + + this._currentHistoryItemBaseRef = this.historyItemBaseRef; + + this._onDidChangeCurrentHistoryItemRefs.fire(); + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemRef: ${JSON.stringify(this._currentHistoryItemRef)}`); + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemRemoteRef: ${JSON.stringify(this._currentHistoryItemRemoteRef)}`); + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemBaseRef: ${JSON.stringify(this._currentHistoryItemBaseRef)}`); // Refs (alphabetically) const refs = await this.repository.getRefs({ sort: 'alphabetically' }); @@ -177,7 +187,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec } async provideHistoryItems(options: SourceControlHistoryOptions): Promise { - if (!this.currentHistoryItemGroup || !options.historyItemRefs) { + if (!this.currentHistoryItemRef || !options.historyItemRefs) { return []; } @@ -265,16 +275,16 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec if (historyItemRefs.length === 0) { // TODO@lszomoru - log return undefined; - } else if (historyItemRefs.length === 1 && historyItemRefs[0] === this.currentHistoryItemGroup?.id) { + } else if (historyItemRefs.length === 1 && historyItemRefs[0] === this.currentHistoryItemRemoteRef?.id) { // Remote - if (this.currentHistoryItemGroup.remote) { - const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemGroup.remote.id); + if (this.currentHistoryItemRemoteRef) { + const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemRemoteRef.id); return ancestor; } // Base - if (this.currentHistoryItemGroup.base) { - const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemGroup.base.id); + if (this.currentHistoryItemBaseRef) { + const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemBaseRef.id); return ancestor; } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index d713066de80..bbb27f6f9b5 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -6,10 +6,10 @@ import { Barrier } from '../../../base/common/async.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { Event, Emitter } from '../../../base/common/event.js'; -import { derivedOpts, IObservable, observableValue, observableValueOpts } from '../../../base/common/observable.js'; +import { IObservable, observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from '../../contrib/scm/common/scm.js'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto, SCMHistoryItemRefsChangeEventDto } from '../common/extHost.protocol.js'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMHistoryItemRefsChangeEventDto, SCMHistoryItemRefDto } from '../common/extHost.protocol.js'; import { Command } from '../../../editor/common/languages.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; @@ -17,7 +17,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryItemRef, ISCMHistoryItemRefsChangeEvent, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemRef, ISCMHistoryItemRefsChangeEvent, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; import { ResourceTree } from '../../../base/common/resourceTree.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js'; @@ -28,8 +28,8 @@ import { ITextModelContentProvider, ITextModelService } from '../../../editor/co import { Schemas } from '../../../base/common/network.js'; import { ITextModel } from '../../../editor/common/model.js'; import { structuralEquals } from '../../../base/common/equals.js'; -import { Codicon } from '../../../base/common/codicons.js'; -import { historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote } from '../../contrib/scm/browser/scmHistory.js'; +import { historyItemBaseRefColor, historyItemRefColor, historyItemRemoteRefColor } from '../../contrib/scm/browser/scmHistory.js'; +import { ColorIdentifier } from '../../../platform/theme/common/colorUtils.js'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -56,6 +56,10 @@ function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { return { ...historyItemDto, subject, references }; } +function toISCMHistoryItemRef(historyItemRefDto?: SCMHistoryItemRefDto, color?: ColorIdentifier): ISCMHistoryItemRef | undefined { + return historyItemRefDto ? { ...historyItemRefDto, icon: getIconFromIconDto(historyItemRefDto.icon), color: color } : undefined; +} + class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { constructor( textModelService: ITextModelService, @@ -168,55 +172,23 @@ class MainThreadSCMResource implements ISCMResource { } class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { - private readonly _currentHistoryItemGroup = observableValueOpts({ - owner: this, equalsFn: structuralEquals + private readonly _historyItemRef = observableValueOpts({ + owner: this, + equalsFn: structuralEquals }, undefined); - get currentHistoryItemGroup() { return this._currentHistoryItemGroup; } + get historyItemRef(): IObservable { return this._historyItemRef; } - readonly currentHistoryItemRef = derivedOpts({ + private readonly _historyItemRemoteRef = observableValueOpts({ owner: this, equalsFn: structuralEquals - }, reader => { - const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader); + }, undefined); + get historyItemRemoteRef(): IObservable { return this._historyItemRemoteRef; } - return currentHistoryItemGroup ? { - id: currentHistoryItemGroup.id ?? '', - name: currentHistoryItemGroup.name, - revision: currentHistoryItemGroup.revision, - color: historyItemGroupLocal, - icon: Codicon.target, - } : undefined; - }); - - readonly currentHistoryItemRemoteRef = derivedOpts({ + private readonly _historyItemBaseRef = observableValueOpts({ owner: this, equalsFn: structuralEquals - }, reader => { - const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader); - - return currentHistoryItemGroup?.remote ? { - id: currentHistoryItemGroup.remote.id ?? '', - name: currentHistoryItemGroup.remote.name, - revision: currentHistoryItemGroup.remote.revision, - color: historyItemGroupRemote, - icon: Codicon.cloud, - } : undefined; - }); - - readonly currentHistoryItemBaseRef = derivedOpts({ - owner: this, - equalsFn: structuralEquals - }, reader => { - const currentHistoryItemGroup = this._currentHistoryItemGroup.read(reader); - - return currentHistoryItemGroup?.base ? { - id: currentHistoryItemGroup.base.id ?? '', - name: currentHistoryItemGroup.base.name, - revision: currentHistoryItemGroup.base.revision, - color: historyItemGroupBase, - icon: Codicon.cloud, - } : undefined; - }); + }, undefined); + get historyItemBaseRef(): IObservable { return this._historyItemBaseRef; } private readonly _historyItemRefChanges = observableValue(this, { added: [], modified: [], removed: [] }); get historyItemRefChanges(): IObservable { return this._historyItemRefChanges; } @@ -247,14 +219,18 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { })); } - $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { - this._currentHistoryItemGroup.set(historyItemGroup, undefined); + $onDidChangeCurrentHistoryItemRefs(historyItemRef?: SCMHistoryItemRefDto, historyItemRemoteRef?: SCMHistoryItemRefDto, historyItemBaseRef?: SCMHistoryItemRefDto): void { + transaction(tx => { + this._historyItemRef.set(toISCMHistoryItemRef(historyItemRef, historyItemRefColor), tx); + this._historyItemRemoteRef.set(toISCMHistoryItemRef(historyItemRemoteRef, historyItemRemoteRefColor), tx); + this._historyItemBaseRef.set(toISCMHistoryItemRef(historyItemBaseRef, historyItemBaseRefColor), tx); + }); } $onDidChangeHistoryItemRefs(historyItemRefs: SCMHistoryItemRefsChangeEventDto): void { - const added = historyItemRefs.added.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); - const modified = historyItemRefs.modified.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); - const removed = historyItemRefs.removed.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); + const added = historyItemRefs.added.map(ref => toISCMHistoryItemRef(ref)!); + const modified = historyItemRefs.modified.map(ref => toISCMHistoryItemRef(ref)!); + const removed = historyItemRefs.removed.map(ref => toISCMHistoryItemRef(ref)!); this._historyItemRefChanges.set({ added, modified, removed }, undefined); } @@ -487,12 +463,12 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { return result && URI.revive(result); } - $onDidChangeHistoryProviderCurrentHistoryItemGroup(currentHistoryItemGroup?: SCMHistoryItemGroupDto): void { + $onDidChangeHistoryProviderCurrentHistoryItemRefs(historyItemRef?: SCMHistoryItemRefDto, historyItemRemoteRef?: SCMHistoryItemRefDto, historyItemBaseRef?: SCMHistoryItemRefDto): void { if (!this.historyProvider.get()) { return; } - this._historyProvider.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); + this._historyProvider.get()?.$onDidChangeCurrentHistoryItemRefs(historyItemRef, historyItemRemoteRef, historyItemBaseRef); } $onDidChangeHistoryProviderHistoryItemRefs(historyItemRefs: SCMHistoryItemRefsChangeEventDto): void { @@ -736,7 +712,7 @@ export class MainThreadSCM implements MainThreadSCMShape { } } - async $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise { + async $onDidChangeHistoryProviderCurrentHistoryItemRefs(sourceControlHandle: number, historyItemRef?: SCMHistoryItemRefDto, historyItemRemoteRef?: SCMHistoryItemRefDto, historyItemBaseRef?: SCMHistoryItemRefDto): Promise { await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); @@ -745,7 +721,7 @@ export class MainThreadSCM implements MainThreadSCMShape { } const provider = repository.provider as MainThreadSCMProvider; - provider.$onDidChangeHistoryProviderCurrentHistoryItemGroup(historyItemGroup); + provider.$onDidChangeHistoryProviderCurrentHistoryItemRefs(historyItemRef, historyItemRemoteRef, historyItemBaseRef); } async $onDidChangeHistoryProviderHistoryItemRefs(sourceControlHandle: number, historyItemRefs: SCMHistoryItemRefsChangeEventDto): Promise { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2055d997aa0..274e29330d6 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1542,14 +1542,6 @@ export type SCMRawResourceSplices = [ SCMRawResourceSplice[] ]; -export interface SCMHistoryItemGroupDto { - readonly id: string; - readonly name: string; - readonly revision?: string; - readonly base?: Omit, 'remote'>; - readonly remote?: Omit, 'remote'>; -} - export interface SCMHistoryItemRefDto { readonly id: string; readonly name: string; @@ -1606,7 +1598,7 @@ export interface MainThreadSCMShape extends IDisposable { $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): Promise; $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise; - $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise; + $onDidChangeHistoryProviderCurrentHistoryItemRefs(sourceControlHandle: number, historyItemRef?: SCMHistoryItemRefDto, historyItemRemoteRef?: SCMHistoryItemRefDto, historyItemBaseRef?: SCMHistoryItemRefDto): Promise; $onDidChangeHistoryProviderHistoryItemRefs(sourceControlHandle: number, historyItemRefs: SCMHistoryItemRefsChangeEventDto): Promise; } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 1f15f83d60a..766f5cc9d5f 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -79,6 +79,10 @@ function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMH return { ...historyItem, references }; } +function toSCMHistoryItemRefDto(historyItemRef?: vscode.SourceControlHistoryItemRef): SCMHistoryItemRefDto | undefined { + return historyItemRef ? { ...historyItemRef, icon: getHistoryItemIconDto(historyItemRef.icon) } : undefined; +} + function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number { if (!a.iconPath && !b.iconPath) { return 0; @@ -577,7 +581,6 @@ class ExtHostSourceControl implements vscode.SourceControl { private _historyProvider: vscode.SourceControlHistoryProvider | undefined; private readonly _historyProviderDisposable = new MutableDisposable(); - private _historyProviderCurrentHistoryItemGroup: vscode.SourceControlHistoryItemGroup | undefined; get historyProvider(): vscode.SourceControlHistoryProvider | undefined { checkProposedApiEnabled(this._extension, 'scmHistoryProvider'); @@ -593,9 +596,12 @@ class ExtHostSourceControl implements vscode.SourceControl { this.#proxy.$updateSourceControl(this.handle, { hasHistoryProvider: !!historyProvider }); if (historyProvider) { - this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemGroup(() => { - this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup); + this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemRefs(() => { + const historyItemRef = toSCMHistoryItemRefDto(historyProvider?.currentHistoryItemRef); + const historyItemRemoteRef = toSCMHistoryItemRefDto(historyProvider?.currentHistoryItemRemoteRef); + const historyItemBaseRef = toSCMHistoryItemRefDto(historyProvider?.currentHistoryItemBaseRef); + + this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemRefs(this.handle, historyItemRef, historyItemRemoteRef, historyItemBaseRef); })); this._historyProviderDisposable.value.add(historyProvider.onDidChangeHistoryItemRefs((e) => { if (e.added.length === 0 && e.modified.length === 0 && e.removed.length === 0) { diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index faca874aa54..181ccffb1eb 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -39,9 +39,9 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe private readonly _activeRepositoryCurrentHistoryItemGroupName = derived(reader => { const repository = this.scmViewService.activeRepository.read(reader); const historyProvider = repository?.provider.historyProvider.read(reader); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.read(reader); + const historyItemRef = historyProvider?.historyItemRef.read(reader); - return currentHistoryItemGroup?.name; + return historyItemRef?.name; }); private readonly _countBadgeRepositories = derived(this, reader => { diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index 08e24fe1870..0343b271fc5 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -19,11 +19,11 @@ const CIRCLE_RADIUS = 4; const SWIMLANE_CURVE_RADIUS = 5; /** - * History graph colors (local, remote, base) + * History item reference colors (local, remote, base) */ -export const historyItemGroupLocal = registerColor('scmGraph.historyItemGroupLocal', chartsBlue, localize('scmGraphHistoryItemGroupLocal', "Local history item group color.")); -export const historyItemGroupRemote = registerColor('scmGraph.historyItemGroupRemote', chartsPurple, localize('scmGraphHistoryItemGroupRemote', "Remote history item group color.")); -export const historyItemGroupBase = registerColor('scmGraph.historyItemGroupBase', chartsOrange, localize('scmGraphHistoryItemGroupBase', "Base history item group color.")); +export const historyItemRefColor = registerColor('scmGraph.historyItemRefColor', chartsBlue, localize('scmGraphHistoryItemRefColor', "History item reference color.")); +export const historyItemRemoteRefColor = registerColor('scmGraph.historyItemRemoteRefColor', chartsPurple, localize('scmGraphHistoryItemRemoteRefColor', "History item remote reference color.")); +export const historyItemBaseRefColor = registerColor('scmGraph.historyItemBaseRefColor', chartsOrange, localize('scmGraphHistoryItemBaseRefColor', "History item base reference color.")); /** * History item hover color @@ -107,7 +107,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV // Circle color - use the output swimlane color if present, otherwise the input swimlane color const circleColor = circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : - circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemGroupLocal; + circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemRefColor; let outputSwimlaneIndex = 0; for (let index = 0; index < inputSwimlanes.length; index++) { @@ -315,7 +315,7 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], // Circle color - use the output swimlane color if present, otherwise the input swimlane color color = circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : - circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemGroupLocal; + circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemRefColor; } return { ...ref, color }; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 52a30c7f6ce..6ef3cfb17a5 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -129,9 +129,9 @@ class SCMHistoryItemRefsActionViewItem extends ActionViewItem { const historyProvider = this._repository.provider.historyProvider.get(); return [ - historyProvider?.currentHistoryItemRef.get()?.name, - historyProvider?.currentHistoryItemRemoteRef.get()?.name, - historyProvider?.currentHistoryItemBaseRef.get()?.name + historyProvider?.historyItemRef.get()?.name, + historyProvider?.historyItemRemoteRef.get()?.name, + historyProvider?.historyItemBaseRef.get()?.name ].filter(ref => !!ref).join(', '); } else if (this._historyItemsFilter.length === 1) { return this._historyItemsFilter[0].name; @@ -326,7 +326,7 @@ class HistoryItemRenderer implements ITreeRenderer !!ref); break; default: @@ -801,9 +801,9 @@ class SCMHistoryViewModel extends Disposable { private _getGraphColorMap(historyItemRefs: ISCMHistoryItemRef[]): Map { const repository = this.repository.get(); const historyProvider = repository?.provider.historyProvider.get(); - const historyItemRef = historyProvider?.currentHistoryItemRef.get(); - const historyItemRemoteRef = historyProvider?.currentHistoryItemRemoteRef.get(); - const historyItemBaseRef = historyProvider?.currentHistoryItemBaseRef.get(); + const historyItemRef = historyProvider?.historyItemRef.get(); + const historyItemRemoteRef = historyProvider?.historyItemRemoteRef.get(); + const historyItemBaseRef = historyProvider?.historyItemBaseRef.get(); const colorMap = new Map(); @@ -1086,7 +1086,7 @@ export class SCMHistoryViewPane extends ViewPane { const firstRepositoryInitialized = derived(this, reader => { const repository = this._treeViewModel.repository.read(reader); const historyProvider = repository?.provider.historyProvider.read(reader); - const historyItemRef = historyProvider?.currentHistoryItemRef.read(reader); + const historyItemRef = historyProvider?.historyItemRef.read(reader); return historyItemRef !== undefined ? true : undefined; }); @@ -1115,12 +1115,12 @@ export class SCMHistoryViewPane extends ViewPane { // Publish const historyItemRemoteRefIdSignal = signalFromObservable(this, derived(reader => { - return historyProvider.currentHistoryItemRemoteRef.read(reader)?.id; + return historyProvider.historyItemRemoteRef.read(reader)?.id; })); // Fetch, Push const historyItemRemoteRefRevision = derived(reader => { - return historyProvider.currentHistoryItemRemoteRef.read(reader)?.revision; + return historyProvider.historyItemRemoteRef.read(reader)?.revision; }); // HistoryItemRefs changed @@ -1134,7 +1134,7 @@ export class SCMHistoryViewPane extends ViewPane { }, }, (reader, changeSummary) => { historyItemRemoteRefIdSignal.read(reader); - const historyItemRefValue = historyProvider.currentHistoryItemRef.read(reader); + const historyItemRefValue = historyProvider.historyItemRef.read(reader); const historyItemRemoteRefRevisionValue = historyItemRemoteRefRevision.read(reader); // Commit, Checkout, Publish, Pull diff --git a/src/vs/workbench/contrib/scm/browser/workingSet.ts b/src/vs/workbench/contrib/scm/browser/workingSet.ts index 7dfd2c00bd5..1185f8de20a 100644 --- a/src/vs/workbench/contrib/scm/browser/workingSet.ts +++ b/src/vs/workbench/contrib/scm/browser/workingSet.ts @@ -63,17 +63,17 @@ export class SCMWorkingSetController extends Disposable implements IWorkbenchCon private _onDidAddRepository(repository: ISCMRepository): void { const disposables = new DisposableStore(); - const currentHistoryItemGroupId = derived(reader => { + const historyItemRefId = derived(reader => { const historyProvider = repository.provider.historyProvider.read(reader); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.read(reader); + const historyItemRef = historyProvider?.historyItemRef.read(reader); - return currentHistoryItemGroup?.id; + return historyItemRef?.id; }); disposables.add(autorun(async reader => { - const historyItemGroupId = currentHistoryItemGroupId.read(reader); + const historyItemRefIdValue = historyItemRefId.read(reader); - if (!historyItemGroupId) { + if (!historyItemRefIdValue) { return; } @@ -81,20 +81,20 @@ export class SCMWorkingSetController extends Disposable implements IWorkbenchCon const repositoryWorkingSets = this._workingSets.get(providerKey); if (!repositoryWorkingSets) { - this._workingSets.set(providerKey, { currentHistoryItemGroupId: historyItemGroupId, editorWorkingSets: new Map() }); + this._workingSets.set(providerKey, { currentHistoryItemGroupId: historyItemRefIdValue, editorWorkingSets: new Map() }); return; } // Editors for the current working set are automatically restored - if (repositoryWorkingSets.currentHistoryItemGroupId === historyItemGroupId) { + if (repositoryWorkingSets.currentHistoryItemGroupId === historyItemRefIdValue) { return; } // Save the working set - this._saveWorkingSet(providerKey, historyItemGroupId, repositoryWorkingSets); + this._saveWorkingSet(providerKey, historyItemRefIdValue, repositoryWorkingSets); // Restore the working set - await this._restoreWorkingSet(providerKey, historyItemGroupId); + await this._restoreWorkingSet(providerKey, historyItemRefIdValue); })); this._repositoryDisposables.set(repository, disposables); diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 6a388fcb460..c40e5b214f2 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -15,11 +15,9 @@ export interface ISCMHistoryProviderMenus { } export interface ISCMHistoryProvider { - readonly currentHistoryItemGroup: IObservable; - - readonly currentHistoryItemRef: IObservable; - readonly currentHistoryItemRemoteRef: IObservable; - readonly currentHistoryItemBaseRef: IObservable; + readonly historyItemRef: IObservable; + readonly historyItemRemoteRef: IObservable; + readonly historyItemBaseRef: IObservable; readonly historyItemRefChanges: IObservable; @@ -35,14 +33,6 @@ export interface ISCMHistoryOptions { readonly historyItemRefs?: readonly string[]; } -export interface ISCMHistoryItemGroup { - readonly id: string; - readonly name: string; - readonly revision?: string; - readonly base?: Omit, 'remote'>; - readonly remote?: Omit, 'remote'>; -} - export interface ISCMHistoryItemStatistics { readonly files: number; readonly insertions: number; diff --git a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts index 0cb64cfdda0..d322704bd79 100644 --- a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ColorIdentifier } from '../../../../../platform/theme/common/colorUtils.js'; -import { colorRegistry, historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote, toISCMHistoryItemViewModelArray } from '../../browser/scmHistory.js'; +import { colorRegistry, historyItemBaseRefColor, historyItemRefColor, historyItemRemoteRefColor, toISCMHistoryItemViewModelArray } from '../../browser/scmHistory.js'; import { ISCMHistoryItem, ISCMHistoryItemRef } from '../../common/history.js'; function toSCMHistoryItem(id: string, parentIds: string[], references?: ISCMHistoryItemRef[]): ISCMHistoryItem { @@ -526,9 +526,9 @@ suite('toISCMHistoryItemViewModelArray', () => { ]; const colorMap = new Map([ - ['topic', historyItemGroupLocal], - ['origin/topic', historyItemGroupRemote], - ['origin/main', historyItemGroupBase], + ['topic', historyItemRefColor], + ['origin/topic', historyItemRemoteRefColor], + ['origin/main', historyItemBaseRefColor], ]); const viewModels = toISCMHistoryItemViewModelArray(models, colorMap); @@ -540,57 +540,57 @@ suite('toISCMHistoryItemViewModelArray', () => { assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[0].outputSwimlanes[0].color, historyItemGroupLocal); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, historyItemRefColor); // node b assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].inputSwimlanes[0].color, historyItemGroupLocal); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, historyItemRefColor); assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[1].outputSwimlanes[0].color, historyItemGroupLocal); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, historyItemRefColor); // node c assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[2].inputSwimlanes[0].color, historyItemGroupLocal); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, historyItemRefColor); assert.strictEqual(viewModels[2].outputSwimlanes.length, 1); assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[2].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, historyItemRemoteRefColor); // node d assert.strictEqual(viewModels[3].inputSwimlanes.length, 1); assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[3].inputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[3].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, historyItemRemoteRefColor); // node e assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[4].inputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[4].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[4].outputSwimlanes[1].color, historyItemGroupBase); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, historyItemBaseRefColor); // node g assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[5].inputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[5].inputSwimlanes[1].color, historyItemGroupBase); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, historyItemBaseRefColor); assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[5].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'h'); - assert.strictEqual(viewModels[5].outputSwimlanes[1].color, historyItemGroupBase); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, historyItemBaseRefColor); }); }); diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index b6a4893dd54..cc1e6b44635 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -11,13 +11,15 @@ declare module 'vscode' { } export interface SourceControlHistoryProvider { - currentHistoryItemGroup?: SourceControlHistoryItemGroup; + readonly currentHistoryItemRef: SourceControlHistoryItemRef | undefined; + readonly currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined; + readonly currentHistoryItemBaseRef: SourceControlHistoryItemRef | undefined; /** - * Fires when the current history item group changes after - * a user action (ex: commit, checkout, fetch, pull, push) + * Fires when the current history item refs (local, remote, base) + * change after a user action (ex: commit, checkout, fetch, pull, push) */ - onDidChangeCurrentHistoryItemGroup: Event; + onDidChangeCurrentHistoryItemRefs: Event; /** * Fires when history item refs change @@ -37,14 +39,6 @@ declare module 'vscode' { readonly historyItemRefs?: readonly string[]; } - export interface SourceControlHistoryItemGroup { - readonly id: string; - readonly name: string; - readonly revision?: string; - readonly base?: Omit, 'remote'>; - readonly remote?: Omit, 'remote'>; - } - export interface SourceControlHistoryItemStatistics { readonly files: number; readonly insertions: number; From 04846aa8da4cb169d414d09f0220ce4d6aecbcae Mon Sep 17 00:00:00 2001 From: henricryden Date: Tue, 10 Sep 2024 14:50:30 +0200 Subject: [PATCH 222/286] additional search path for libc.so.6 in check-requirements-linux.sh (#227713) extra path for libc.so.6 in check-requirements-linux.sh --- resources/server/bin/helpers/check-requirements-linux.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/server/bin/helpers/check-requirements-linux.sh b/resources/server/bin/helpers/check-requirements-linux.sh index 31a618fbd85..8ef07a2fb1f 100644 --- a/resources/server/bin/helpers/check-requirements-linux.sh +++ b/resources/server/bin/helpers/check-requirements-linux.sh @@ -125,6 +125,9 @@ elif [ -z "$(ldd --version 2>&1 | grep 'musl libc')" ]; then elif [ -f /usr/lib/libc.so.6 ]; then # Typical path libc_path='/usr/lib/libc.so.6' + elif [ -f /lib64/libc.so.6 ]; then + # Typical path (OpenSUSE) + libc_path='/lib64/libc.so.6' elif [ -f /usr/lib64/libc.so.6 ]; then # Typical path libc_path='/usr/lib64/libc.so.6' From f6846e124eceeaad6ce5c8b3b7eb875e3f0b4eb0 Mon Sep 17 00:00:00 2001 From: Marcus Revaj Date: Tue, 10 Sep 2024 14:50:59 +0200 Subject: [PATCH 223/286] # Render file creation in the refactor preview (#226950) * # Render out the file edits --- .../browser/preview/bulkEditPreview.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts index 8b9a0c4eb65..ad2d7ebb97e 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts @@ -307,18 +307,26 @@ export class BulkFileOperations { return result; } - getFileEdits(uri: URI): ISingleEditOperation[] { + private async getFileEditOperation(edit: ResourceFileEdit): Promise { + const content = await edit.options.contents; + if (!content) { return undefined; } + return EditOperation.replaceMove(Range.lift({ startLineNumber: 0, startColumn: 0, endLineNumber: Number.MAX_VALUE, endColumn: 0 }), content.toString()); + } + + async getFileEdits(uri: URI): Promise { for (const file of this.fileOperations) { if (file.uri.toString() === uri.toString()) { - const result: ISingleEditOperation[] = []; + const result: Promise[] = []; let ignoreAll = false; for (const edit of file.originalEdits.values()) { - if (edit instanceof ResourceTextEdit) { + if (edit instanceof ResourceFileEdit) { + result.push(this.getFileEditOperation(edit)); + } else if (edit instanceof ResourceTextEdit) { if (this.checked.isChecked(edit)) { - result.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), !edit.textEdit.insertAsSnippet ? edit.textEdit.text : SnippetParser.asInsertText(edit.textEdit.text))); + result.push(Promise.resolve(EditOperation.replaceMove(Range.lift(edit.textEdit.range), !edit.textEdit.insertAsSnippet ? edit.textEdit.text : SnippetParser.asInsertText(edit.textEdit.text)))); } } else if (!this.checked.isChecked(edit)) { @@ -331,7 +339,7 @@ export class BulkFileOperations { return []; } - return result.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + return (await Promise.all(result)).filter(r => r !== undefined).sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); } } return []; @@ -402,7 +410,7 @@ export class BulkEditPreviewProvider implements ITextModelContentProvider { model.applyEdits(undoEdits); } // apply new edits and keep (future) undo edits - const newEdits = this._operations.getFileEdits(uri); + const newEdits = await this._operations.getFileEdits(uri); const newUndoEdits = model.applyEdits(newEdits, true); this._modelPreviewEdits.set(model.id, newUndoEdits); } From a7eeac01a84820f1de41fb9f1fddbcc29b7e2cb3 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 10 Sep 2024 15:01:16 +0200 Subject: [PATCH 224/286] Hediet/b/slight-mink (#227793) * observable code restructuring * only import observables from the facade * introduces observableInternal/commonFacade to make extraction easier. * Fixes CI --- src/vs/base/common/observable.ts | 70 +------------ src/vs/base/common/observableInternal/api.ts | 7 +- .../base/common/observableInternal/autorun.ts | 4 +- src/vs/base/common/observableInternal/base.ts | 5 +- .../commonFacade/cancellation.ts | 7 ++ .../observableInternal/commonFacade/deps.ts | 10 ++ .../base/common/observableInternal/derived.ts | 7 +- .../base/common/observableInternal/index.ts | 29 ++++++ .../observableInternal/lazyObservableValue.ts | 5 +- .../base/common/observableInternal/promise.ts | 96 +----------------- .../base/common/observableInternal/utils.ts | 7 +- .../observableInternal/utilsCancellation.ts | 98 +++++++++++++++++++ src/vs/base/test/common/observable.test.ts | 7 +- src/vs/editor/browser/observableCodeEditor.ts | 6 +- .../treeSitter/treeSitterParserService.ts | 2 +- .../diffEditor/components/diffEditorSash.ts | 3 +- .../widget/diffEditor/diffEditorOptions.ts | 9 +- .../widget/diffEditor/diffEditorWidget.ts | 49 +++++----- .../diffEditor/features/gutterFeature.ts | 27 +++-- .../features/hideUnchangedRegionsFeature.ts | 19 ++-- .../multiDiffEditor/diffEditorItemTemplate.ts | 19 ++-- .../multiDiffEditorViewModel.ts | 15 ++- .../multiDiffEditor/multiDiffEditorWidget.ts | 20 ++-- .../multiDiffEditorWidgetImpl.ts | 27 +++-- .../browser/controller/commands.ts | 19 ++-- .../controller/inlineCompletionsController.ts | 5 +- .../inlineCompletionsHintsWidget.ts | 19 ++-- .../browser/inlineEditController.ts | 29 +++--- .../browser/inlineEditSideBySideWidget.ts | 7 +- .../contrib/inlineEdits/browser/commands.ts | 9 +- .../browser/inlineEditsController.ts | 13 ++- .../inlineEdits/browser/inlineEditsModel.ts | 3 +- .../inlineEdits/browser/inlineEditsWidget.ts | 11 +-- .../browser/placeholderTextContribution.ts | 4 +- .../browser/accessibilitySignalService.ts | 3 +- .../common/platformObservableUtils.ts | 3 +- .../editorTextPropertySignalsContribution.ts | 7 +- .../contrib/chat/common/chatAgents.ts | 3 +- .../browser/multiDiffEditorInput.ts | 7 +- .../browser/scmMultiDiffSourceResolver.ts | 9 +- .../contrib/scm/browser/scmHistoryViewPane.ts | 3 +- .../contrib/scm/browser/scmViewService.ts | 2 +- 42 files changed, 323 insertions(+), 381 deletions(-) create mode 100644 src/vs/base/common/observableInternal/commonFacade/cancellation.ts create mode 100644 src/vs/base/common/observableInternal/commonFacade/deps.ts create mode 100644 src/vs/base/common/observableInternal/index.ts create mode 100644 src/vs/base/common/observableInternal/utilsCancellation.ts diff --git a/src/vs/base/common/observable.ts b/src/vs/base/common/observable.ts index 1b8cecf7e69..86ac5971a27 100644 --- a/src/vs/base/common/observable.ts +++ b/src/vs/base/common/observable.ts @@ -5,72 +5,4 @@ // This is a facade for the observable implementation. Only import from here! -export type { - IObservable, - IObserver, - IReader, - ISettable, - ISettableObservable, - ITransaction, - IChangeContext, - IChangeTracker, -} from './observableInternal/base.js'; - -export { - observableValue, - disposableObservableValue, - transaction, - subtransaction, -} from './observableInternal/base.js'; -export { - derived, - derivedOpts, - derivedHandleChanges, - derivedWithStore, -} from './observableInternal/derived.js'; -export { - autorun, - autorunDelta, - autorunHandleChanges, - autorunWithStore, - autorunOpts, - autorunWithStoreHandleChanges, -} from './observableInternal/autorun.js'; -export type { - IObservableSignal, -} from './observableInternal/utils.js'; -export { - constObservable, - debouncedObservable, - derivedObservableWithCache, - derivedObservableWithWritableCache, - keepObserved, - recomputeInitiallyAndOnChange, - observableFromEvent, - observableFromPromise, - observableSignal, - observableSignalFromEvent, - wasEventTriggeredRecently, -} from './observableInternal/utils.js'; -export { - ObservableLazy, - ObservableLazyPromise, - ObservablePromise, - PromiseResult, - waitForState, - derivedWithCancellationToken, -} from './observableInternal/promise.js'; -export { - observableValueOpts -} from './observableInternal/api.js'; - -import { ConsoleObservableLogger, setLogger } from './observableInternal/logging.js'; - -// Remove "//" in the next line to enable logging -const enableLogging = false - // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this - ; - -if (enableLogging) { - setLogger(new ConsoleObservableLogger()); -} +export * from './observableInternal/index.js'; diff --git a/src/vs/base/common/observableInternal/api.ts b/src/vs/base/common/observableInternal/api.ts index 57cb2a5994d..0f7e63c3fa9 100644 --- a/src/vs/base/common/observableInternal/api.ts +++ b/src/vs/base/common/observableInternal/api.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EqualityComparer, strictEquals } from '../equals.js'; -import { ISettableObservable } from '../observable.js'; -import { ObservableValue } from './base.js'; -import { IDebugNameData, DebugNameData } from './debugName.js'; +import { ISettableObservable, ObservableValue } from './base.js'; +import { DebugNameData, IDebugNameData } from './debugName.js'; +import { EqualityComparer, strictEquals } from './commonFacade/deps.js'; import { LazyObservableValue } from './lazyObservableValue.js'; export function observableValueOpts( diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index dba2eb7edf3..f2011ef433d 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assertFn } from '../assert.js'; -import { onBugIndicatingError } from '../errors.js'; -import { DisposableStore, IDisposable, markAsDisposed, toDisposable, trackDisposable } from '../lifecycle.js'; import { IChangeContext, IObservable, IObserver, IReader } from './base.js'; import { DebugNameData, IDebugNameData } from './debugName.js'; +import { assertFn, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js'; import { getLogger } from './logging.js'; /** diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 6821b4f3934..3ce43ead2ae 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { strictEquals, EqualityComparer } from '../equals.js'; -import { DisposableStore, IDisposable } from '../lifecycle.js'; -import { keepObserved, recomputeInitiallyAndOnChange } from '../observable.js'; import { DebugNameData, DebugOwner, getFunctionName } from './debugName.js'; +import { DisposableStore, EqualityComparer, IDisposable, strictEquals } from './commonFacade/deps.js'; import type { derivedOpts } from './derived.js'; import { getLogger } from './logging.js'; +import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js'; /** * Represents an observable value. diff --git a/src/vs/base/common/observableInternal/commonFacade/cancellation.ts b/src/vs/base/common/observableInternal/commonFacade/cancellation.ts new file mode 100644 index 00000000000..af1686a3206 --- /dev/null +++ b/src/vs/base/common/observableInternal/commonFacade/cancellation.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { CancellationError } from '../../errors.js'; +export { CancellationToken, CancellationTokenSource } from '../../cancellation.js'; diff --git a/src/vs/base/common/observableInternal/commonFacade/deps.ts b/src/vs/base/common/observableInternal/commonFacade/deps.ts new file mode 100644 index 00000000000..1ed8b1cc594 --- /dev/null +++ b/src/vs/base/common/observableInternal/commonFacade/deps.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { assertFn } from '../../assert.js'; +export { type EqualityComparer, strictEquals } from '../../equals.js'; +export { BugIndicatingError, onBugIndicatingError } from '../../errors.js'; +export { Event, type IValueWithChangeEvent } from '../../event.js'; +export { DisposableStore, type IDisposable, markAsDisposed, toDisposable, trackDisposable } from '../../lifecycle.js'; diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 88a060eb694..ed32875f8ba 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -3,12 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assertFn } from '../assert.js'; -import { EqualityComparer, strictEquals } from '../equals.js'; -import { onBugIndicatingError } from '../errors.js'; -import { DisposableStore, IDisposable } from '../lifecycle.js'; import { BaseObservable, IChangeContext, IObservable, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js'; -import { DebugNameData, IDebugNameData, DebugOwner } from './debugName.js'; +import { DebugNameData, DebugOwner, IDebugNameData } from './debugName.js'; +import { DisposableStore, EqualityComparer, IDisposable, assertFn, onBugIndicatingError, strictEquals } from './commonFacade/deps.js'; import { getLogger } from './logging.js'; /** diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts new file mode 100644 index 00000000000..9295f2697d5 --- /dev/null +++ b/src/vs/base/common/observableInternal/index.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This is a facade for the observable implementation. Only import from here! + +export { observableValueOpts } from './api.js'; +export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges } from './autorun.js'; +export { asyncTransaction, disposableObservableValue, globalTransaction, observableValue, subtransaction, transaction, TransactionImpl, type IChangeContext, type IChangeTracker, type IObservable, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction, } from './base.js'; +export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './derived.js'; +export { ObservableLazy, ObservableLazyPromise, ObservablePromise, PromiseResult, } from './promise.js'; +export { derivedWithCancellationToken, waitForState } from './utilsCancellation.js'; +export { constObservable, debouncedObservable, derivedConstOnceDefined, derivedObservableWithCache, derivedObservableWithWritableCache, keepObserved, latestChangedValue, mapObservableArrayCached, observableFromEvent, observableFromEventOpts, observableFromPromise, observableFromValueWithChangeEvent, observableSignal, observableSignalFromEvent, recomputeInitiallyAndOnChange, runOnChange, runOnChangeWithStore, signalFromObservable, ValueWithChangeEventFromObservable, wasEventTriggeredRecently, type IObservableSignal, } from './utils.js'; +export { type DebugOwner } from './debugName.js'; + +import { + ConsoleObservableLogger, + setLogger +} from './logging.js'; + +// Remove "//" in the next line to enable logging +const enableLogging = false + // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this + ; + +if (enableLogging) { + setLogger(new ConsoleObservableLogger()); +} diff --git a/src/vs/base/common/observableInternal/lazyObservableValue.ts b/src/vs/base/common/observableInternal/lazyObservableValue.ts index 363fd7f8c5a..8a3f63c05d7 100644 --- a/src/vs/base/common/observableInternal/lazyObservableValue.ts +++ b/src/vs/base/common/observableInternal/lazyObservableValue.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EqualityComparer } from '../equals.js'; -import { ISettableObservable, ITransaction } from '../observable.js'; -import { BaseObservable, IObserver, TransactionImpl } from './base.js'; +import { EqualityComparer } from './commonFacade/deps.js'; +import { BaseObservable, IObserver, ISettableObservable, ITransaction, TransactionImpl } from './base.js'; import { DebugNameData } from './debugName.js'; /** diff --git a/src/vs/base/common/observableInternal/promise.ts b/src/vs/base/common/observableInternal/promise.ts index 6989a58731f..6551c21664c 100644 --- a/src/vs/base/common/observableInternal/promise.ts +++ b/src/vs/base/common/observableInternal/promise.ts @@ -2,13 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { autorun } from './autorun.js'; -import { IObservable, IReader, observableValue, transaction } from './base.js'; -import { Derived, derived } from './derived.js'; -import { CancellationToken, CancellationTokenSource } from '../cancellation.js'; -import { DebugNameData, DebugOwner } from './debugName.js'; -import { strictEquals } from '../equals.js'; -import { CancellationError } from '../errors.js'; +import { IObservable, observableValue, transaction } from './base.js'; +import { derived } from './derived.js'; export class ObservableLazy { private readonly _value = observableValue(this, undefined); @@ -120,90 +115,3 @@ export class ObservableLazyPromise { return this._lazyValue.getValue().promise; } } - -/** - * Resolves the promise when the observables state matches the predicate. - */ -export function waitForState(observable: IObservable): Promise; -export function waitForState(observable: IObservable, predicate: (state: T) => state is TState, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise; -export function waitForState(observable: IObservable, predicate: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise; -export function waitForState(observable: IObservable, predicate?: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise { - if (!predicate) { - predicate = state => state !== null && state !== undefined; - } - return new Promise((resolve, reject) => { - let isImmediateRun = true; - let shouldDispose = false; - const stateObs = observable.map(state => { - /** @description waitForState.state */ - return { - isFinished: predicate(state), - error: isError ? isError(state) : false, - state - }; - }); - const d = autorun(reader => { - /** @description waitForState */ - const { isFinished, error, state } = stateObs.read(reader); - if (isFinished || error) { - if (isImmediateRun) { - // The variable `d` is not initialized yet - shouldDispose = true; - } else { - d.dispose(); - } - if (error) { - reject(error === true ? state : error); - } else { - resolve(state); - } - } - }); - if (cancellationToken) { - const dc = cancellationToken.onCancellationRequested(() => { - d.dispose(); - dc.dispose(); - reject(new CancellationError()); - }); - if (cancellationToken.isCancellationRequested) { - d.dispose(); - dc.dispose(); - reject(new CancellationError()); - return; - } - } - isImmediateRun = false; - if (shouldDispose) { - d.dispose(); - } - }); -} - -export function derivedWithCancellationToken(computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; -export function derivedWithCancellationToken(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; -export function derivedWithCancellationToken(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable { - let computeFn: (reader: IReader, store: CancellationToken) => T; - let owner: DebugOwner; - if (computeFnOrUndefined === undefined) { - computeFn = computeFnOrOwner as any; - owner = undefined; - } else { - owner = computeFnOrOwner; - computeFn = computeFnOrUndefined as any; - } - - let cancellationTokenSource: CancellationTokenSource | undefined = undefined; - return new Derived( - new DebugNameData(owner, undefined, computeFn), - r => { - if (cancellationTokenSource) { - cancellationTokenSource.dispose(true); - } - cancellationTokenSource = new CancellationTokenSource(); - return computeFn(r, cancellationTokenSource.token); - }, undefined, - undefined, - () => cancellationTokenSource?.dispose(), - strictEquals, - ); -} diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 3ce3805792e..47478220dae 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -3,15 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, IValueWithChangeEvent } from '../event.js'; -import { DisposableStore, IDisposable, toDisposable } from '../lifecycle.js'; import { autorun, autorunOpts, autorunWithStoreHandleChanges } from './autorun.js'; import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from './base.js'; -import { DebugNameData, IDebugNameData, DebugOwner, getDebugName, } from './debugName.js'; +import { DebugNameData, DebugOwner, IDebugNameData, getDebugName, } from './debugName.js'; +import { BugIndicatingError, DisposableStore, EqualityComparer, Event, IDisposable, IValueWithChangeEvent, strictEquals, toDisposable } from './commonFacade/deps.js'; import { derived, derivedOpts } from './derived.js'; import { getLogger } from './logging.js'; -import { BugIndicatingError } from '../errors.js'; -import { EqualityComparer, strictEquals } from '../equals.js'; /** * Represents an efficient observable whose value never changes. diff --git a/src/vs/base/common/observableInternal/utilsCancellation.ts b/src/vs/base/common/observableInternal/utilsCancellation.ts new file mode 100644 index 00000000000..17e4ba9e308 --- /dev/null +++ b/src/vs/base/common/observableInternal/utilsCancellation.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IReader, IObservable } from './base.js'; +import { DebugOwner, DebugNameData } from './debugName.js'; +import { CancellationError, CancellationToken, CancellationTokenSource } from './commonFacade/cancellation.js'; +import { Derived } from './derived.js'; +import { strictEquals } from './commonFacade/deps.js'; +import { autorun } from './autorun.js'; + +/** + * Resolves the promise when the observables state matches the predicate. + */ +export function waitForState(observable: IObservable): Promise; +export function waitForState(observable: IObservable, predicate: (state: T) => state is TState, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise; +export function waitForState(observable: IObservable, predicate: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise; +export function waitForState(observable: IObservable, predicate?: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise { + if (!predicate) { + predicate = state => state !== null && state !== undefined; + } + return new Promise((resolve, reject) => { + let isImmediateRun = true; + let shouldDispose = false; + const stateObs = observable.map(state => { + /** @description waitForState.state */ + return { + isFinished: predicate(state), + error: isError ? isError(state) : false, + state + }; + }); + const d = autorun(reader => { + /** @description waitForState */ + const { isFinished, error, state } = stateObs.read(reader); + if (isFinished || error) { + if (isImmediateRun) { + // The variable `d` is not initialized yet + shouldDispose = true; + } else { + d.dispose(); + } + if (error) { + reject(error === true ? state : error); + } else { + resolve(state); + } + } + }); + if (cancellationToken) { + const dc = cancellationToken.onCancellationRequested(() => { + d.dispose(); + dc.dispose(); + reject(new CancellationError()); + }); + if (cancellationToken.isCancellationRequested) { + d.dispose(); + dc.dispose(); + reject(new CancellationError()); + return; + } + } + isImmediateRun = false; + if (shouldDispose) { + d.dispose(); + } + }); +} + +export function derivedWithCancellationToken(computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; +export function derivedWithCancellationToken(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; +export function derivedWithCancellationToken(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable { + let computeFn: (reader: IReader, store: CancellationToken) => T; + let owner: DebugOwner; + if (computeFnOrUndefined === undefined) { + computeFn = computeFnOrOwner as any; + owner = undefined; + } else { + owner = computeFnOrOwner; + computeFn = computeFnOrUndefined as any; + } + + let cancellationTokenSource: CancellationTokenSource | undefined = undefined; + return new Derived( + new DebugNameData(owner, undefined, computeFn), + r => { + if (cancellationTokenSource) { + cancellationTokenSource.dispose(true); + } + cancellationTokenSource = new CancellationTokenSource(); + return computeFn(r, cancellationTokenSource.token); + }, undefined, + undefined, + () => cancellationTokenSource?.dispose(), + strictEquals + ); +} diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index 306f2a43a79..91d09493c15 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { setUnexpectedErrorHandler } from '../../common/errors.js'; import { Emitter, Event } from '../../common/event.js'; import { DisposableStore } from '../../common/lifecycle.js'; -import { ISettableObservable, autorun, derived, ITransaction, observableFromEvent, observableValue, transaction, keepObserved, waitForState, autorunHandleChanges, observableSignal } from '../../common/observable.js'; -import { BaseObservable, IObservable, IObserver } from '../../common/observableInternal/base.js'; -import { derivedDisposable } from '../../common/observableInternal/derived.js'; +import { autorun, autorunHandleChanges, derived, derivedDisposable, IObservable, IObserver, ISettableObservable, ITransaction, keepObserved, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from '../../common/observable.js'; +import { BaseObservable } from '../../common/observableInternal/base.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; -import { setUnexpectedErrorHandler } from '../../common/errors.js'; suite('observables', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 868f6bca9aa..9641c87228b 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -5,16 +5,14 @@ import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; -import { IObservable, ITransaction, autorun, autorunOpts, derived, derivedOpts, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; -import { TransactionImpl } from '../../base/common/observableInternal/base.js'; -import { derivedWithSetter } from '../../base/common/observableInternal/derived.js'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from './editorBrowser.js'; +import { IObservable, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js'; import { Position } from '../common/core/position.js'; import { Selection } from '../common/core/selection.js'; import { ICursorSelectionChangedEvent } from '../common/cursorEvents.js'; import { IModelDeltaDecoration, ITextModel } from '../common/model.js'; import { IModelContentChangedEvent } from '../common/textModelEvents.js'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from './editorBrowser.js'; /** * Returns a facade for the code editor that provides observables for various states/events. diff --git a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts index e6d3695efac..26a61c41c1a 100644 --- a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts @@ -21,7 +21,7 @@ import { CancellationToken, cancelOnDispose } from '../../../../base/common/canc import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { canASAR } from '../../../../base/common/amd.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; -import { PromiseResult } from '../../../../base/common/observableInternal/promise.js'; +import { PromiseResult } from '../../../../base/common/observable.js'; const EDITOR_TREESITTER_TELEMETRY = 'editor.experimental.treeSitterTelemetry'; const MODULE_LOCATION_SUBPATH = `@vscode/tree-sitter-wasm/wasm`; diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts index beb8bcc0761..8abaddcceb9 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts @@ -5,9 +5,8 @@ import { IBoundarySashes, ISashEvent, Orientation, Sash, SashState } from '../../../../../base/browser/ui/sash/sash.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, ISettableObservable, autorun, observableValue } from '../../../../../base/common/observable.js'; +import { IObservable, IReader, ISettableObservable, autorun, derivedWithSetter, observableValue } from '../../../../../base/common/observable.js'; import { DiffEditorOptions } from '../diffEditorOptions.js'; -import { derivedWithSetter } from '../../../../../base/common/observableInternal/derived.js'; export class SashLayout { public readonly sashLeft = derivedWithSetter(this, reader => { diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index 97260fa8c83..0c1a533681f 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IObservable, ISettableObservable, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { derivedConstOnceDefined } from '../../../../base/common/observableInternal/utils.js'; +import { IObservable, ISettableObservable, derived, derivedConstOnceDefined, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { Constants } from '../../../../base/common/uint.js'; -import { allowsTrueInlineDiffRendering } from './components/diffEditorViewZones/diffEditorViewZones.js'; -import { DiffEditorViewModel, DiffState } from './diffEditorViewModel.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { diffEditorDefaultOptions } from '../../../common/config/diffEditor.js'; import { IDiffEditorBaseOptions, IDiffEditorOptions, IEditorOptions, ValidDiffEditorBaseOptions, clampedFloat, clampedInt, boolean as validateBooleanOption, stringSet as validateStringSetOption } from '../../../common/config/editorOptions.js'; import { LineRangeMapping } from '../../../common/diff/rangeMapping.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { allowsTrueInlineDiffRendering } from './components/diffEditorViewZones/diffEditorViewZones.js'; +import { DiffEditorViewModel, DiffState } from './diffEditorViewModel.js'; export class DiffEditorOptions { private readonly _options: ISettableObservable, { changedOptions: IDiffEditorOptions }>; diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 6b69885c757..da2e28f1c44 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -7,28 +7,15 @@ import { IBoundarySashes } from '../../../../base/browser/ui/sash/sash.js'; import { findLast } from '../../../../base/common/arraysFind.js'; import { BugIndicatingError, onUnexpectedError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; -import { toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ITransaction, autorun, autorunWithStore, derived, disposableObservableValue, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../base/common/observable.js'; -import { derivedDisposable } from '../../../../base/common/observableInternal/derived.js'; -import './style.css'; -import { IEditorConstructionOptions } from '../../config/editorConfiguration.js'; -import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from '../../editorBrowser.js'; -import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from '../../editorExtensions.js'; -import { ICodeEditorService } from '../../services/codeEditorService.js'; -import { StableEditorScrollState } from '../../stableEditorScroll.js'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../codeEditor/codeEditorWidget.js'; -import { AccessibleDiffViewer, AccessibleDiffViewerModelFromEditors } from './components/accessibleDiffViewer.js'; -import { DiffEditorDecorations } from './components/diffEditorDecorations.js'; -import { DiffEditorSash, SashLayout } from './components/diffEditorSash.js'; -import { DiffEditorViewZones } from './components/diffEditorViewZones/diffEditorViewZones.js'; -import { DiffEditorGutter } from './features/gutterFeature.js'; -import { HideUnchangedRegionsFeature } from './features/hideUnchangedRegionsFeature.js'; -import { MovedBlocksLinesFeature } from './features/movedBlocksLinesFeature.js'; -import { OverviewRulerFeature } from './features/overviewRulerFeature.js'; -import { RevertButtonsFeature } from './features/revertButtonsFeature.js'; -import { CSSStyle, ObservableElementSizeObserver, RefCounted, applyStyle, applyViewZones, translatePosition } from './utils.js'; import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; +import { toDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable, ITransaction, autorun, autorunWithStore, derived, derivedDisposable, disposableObservableValue, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../base/common/observable.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; import { IDimension } from '../../../common/core/dimension.js'; import { Position } from '../../../common/core/position.js'; @@ -39,15 +26,27 @@ import { LineRangeMapping, RangeMapping } from '../../../common/diff/rangeMappin import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from '../../../common/editorCommon.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { IIdentifiedSingleEditOperation } from '../../../common/model.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; +import { IEditorConstructionOptions } from '../../config/editorConfiguration.js'; +import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from '../../editorBrowser.js'; +import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from '../../editorExtensions.js'; +import { ICodeEditorService } from '../../services/codeEditorService.js'; +import { StableEditorScrollState } from '../../stableEditorScroll.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../codeEditor/codeEditorWidget.js'; +import { AccessibleDiffViewer, AccessibleDiffViewerModelFromEditors } from './components/accessibleDiffViewer.js'; +import { DiffEditorDecorations } from './components/diffEditorDecorations.js'; import { DiffEditorEditors } from './components/diffEditorEditors.js'; +import { DiffEditorSash, SashLayout } from './components/diffEditorSash.js'; +import { DiffEditorViewZones } from './components/diffEditorViewZones/diffEditorViewZones.js'; import { DelegatingEditor } from './delegatingEditorImpl.js'; import { DiffEditorOptions } from './diffEditorOptions.js'; import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel.js'; +import { DiffEditorGutter } from './features/gutterFeature.js'; +import { HideUnchangedRegionsFeature } from './features/hideUnchangedRegionsFeature.js'; +import { MovedBlocksLinesFeature } from './features/movedBlocksLinesFeature.js'; +import { OverviewRulerFeature } from './features/overviewRulerFeature.js'; +import { RevertButtonsFeature } from './features/revertButtonsFeature.js'; +import './style.css'; +import { CSSStyle, ObservableElementSizeObserver, RefCounted, applyStyle, applyViewZones, translatePosition } from './utils.js'; export interface IDiffCodeEditorWidgetOptions { originalEditor?: ICodeEditorWidgetOptions; diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index a2a4eb15f27..556378aebf3 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -9,16 +9,13 @@ import { ActionsOrientation } from '../../../../../base/browser/ui/actionbar/act import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; import { IBoundarySashes } from '../../../../../base/browser/ui/sash/sash.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, autorunWithStore, derived, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; -import { derivedDisposable, derivedWithSetter } from '../../../../../base/common/observableInternal/derived.js'; +import { IObservable, autorun, autorunWithStore, derived, derivedDisposable, derivedWithSetter, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; -import { DiffEditorEditors } from '../components/diffEditorEditors.js'; -import { DiffEditorSash, SashLayout } from '../components/diffEditorSash.js'; -import { DiffEditorOptions } from '../diffEditorOptions.js'; -import { DiffEditorViewModel } from '../diffEditorViewModel.js'; -import { appendRemoveOnDispose, applyStyle, prependRemoveOnDispose } from '../utils.js'; -import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../utils/editorGutter.js'; -import { ActionRunnerWithContext } from '../../multiDiffEditor/utils.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; +import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { WorkbenchHoverDelegate } from '../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { EditorOption } from '../../../../common/config/editorOptions.js'; import { LineRange, LineRangeSet } from '../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../common/core/offsetRange.js'; @@ -26,11 +23,13 @@ import { Range } from '../../../../common/core/range.js'; import { TextEdit } from '../../../../common/core/textEdit.js'; import { DetailedLineRangeMapping } from '../../../../common/diff/rangeMapping.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; -import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { WorkbenchHoverDelegate } from '../../../../../platform/hover/browser/hover.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ActionRunnerWithContext } from '../../multiDiffEditor/utils.js'; +import { DiffEditorEditors } from '../components/diffEditorEditors.js'; +import { DiffEditorSash, SashLayout } from '../components/diffEditorSash.js'; +import { DiffEditorOptions } from '../diffEditorOptions.js'; +import { DiffEditorViewModel } from '../diffEditorViewModel.js'; +import { appendRemoveOnDispose, applyStyle, prependRemoveOnDispose } from '../utils.js'; +import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../utils/editorGutter.js'; const emptyArr: never[] = []; const width = 35; diff --git a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts index 559c40ec8eb..e87fe47e055 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts @@ -8,16 +8,11 @@ import { renderIcon, renderLabelWithIcons } from '../../../../../base/browser/ui import { Codicon } from '../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, autorun, derived, derivedWithStore, observableValue, transaction } from '../../../../../base/common/observable.js'; -import { derivedDisposable } from '../../../../../base/common/observableInternal/derived.js'; +import { IObservable, IReader, autorun, derived, derivedDisposable, derivedWithStore, observableValue, transaction } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isDefined } from '../../../../../base/common/types.js'; -import { ICodeEditor } from '../../../editorBrowser.js'; -import { observableCodeEditor } from '../../../observableCodeEditor.js'; -import { DiffEditorEditors } from '../components/diffEditorEditors.js'; -import { DiffEditorOptions } from '../diffEditorOptions.js'; -import { DiffEditorViewModel, RevealPreference, UnchangedRegion } from '../diffEditorViewModel.js'; -import { IObservableViewZone, PlaceholderViewZone, ViewZoneOverlayWidget, applyObservableDecorations, applyStyle } from '../utils.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { EditorOption } from '../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../common/core/lineRange.js'; import { Position } from '../../../../common/core/position.js'; @@ -25,8 +20,12 @@ import { Range } from '../../../../common/core/range.js'; import { CursorChangeReason } from '../../../../common/cursorEvents.js'; import { SymbolKind, SymbolKinds } from '../../../../common/languages.js'; import { IModelDecorationOptions, IModelDeltaDecoration, ITextModel } from '../../../../common/model.js'; -import { localize } from '../../../../../nls.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor } from '../../../editorBrowser.js'; +import { observableCodeEditor } from '../../../observableCodeEditor.js'; +import { DiffEditorEditors } from '../components/diffEditorEditors.js'; +import { DiffEditorOptions } from '../diffEditorOptions.js'; +import { DiffEditorViewModel, RevealPreference, UnchangedRegion } from '../diffEditorViewModel.js'; +import { IObservableViewZone, PlaceholderViewZone, ViewZoneOverlayWidget, applyObservableDecorations, applyStyle } from '../utils.js'; /** * Make sure to add the view zones to the editor! diff --git a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index 6e35b12dbb0..48ac35815d4 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -6,22 +6,21 @@ import { h } from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, derived } from '../../../../base/common/observable.js'; -import { globalTransaction, observableValue } from '../../../../base/common/observableInternal/base.js'; -import { observableCodeEditor } from '../../observableCodeEditor.js'; -import { DiffEditorWidget } from '../diffEditor/diffEditorWidget.js'; -import { DocumentDiffItemViewModel } from './multiDiffEditorViewModel.js'; -import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; -import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { autorun, derived, globalTransaction, observableValue } from '../../../../base/common/observable.js'; import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService, type IScopedContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; +import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; +import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { observableCodeEditor } from '../../observableCodeEditor.js'; +import { DiffEditorWidget } from '../diffEditor/diffEditorWidget.js'; +import { DocumentDiffItemViewModel } from './multiDiffEditorViewModel.js'; import { IObjectData, IPooledObject } from './objectPool.js'; import { ActionRunnerWithContext } from './utils.js'; -import { IContextKeyService, type IScopedContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; +import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; export class TemplateData implements IObjectData { constructor( diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 518b4720698..49c674d0477 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ITransaction, derived, observableValue, transaction } from '../../../../base/common/observable.js'; -import { constObservable, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent } from '../../../../base/common/observableInternal/utils.js'; +import { IObservable, ITransaction, constObservable, derived, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; -import { DiffEditorOptions } from '../diffEditor/diffEditorOptions.js'; -import { DiffEditorViewModel } from '../diffEditor/diffEditorViewModel.js'; -import { RefCounted } from '../diffEditor/utils.js'; -import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; +import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; import { Selection } from '../../../common/core/selection.js'; import { IDiffEditorViewModel } from '../../../common/editorCommon.js'; import { IModelService } from '../../../common/services/model.js'; -import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { DiffEditorOptions } from '../diffEditor/diffEditorOptions.js'; +import { DiffEditorViewModel } from '../diffEditor/diffEditorViewModel.js'; +import { RefCounted } from '../diffEditor/utils.js'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; export class MultiDiffEditorViewModel extends Disposable { private readonly _documents: IObservable[] | 'loading'> = observableFromValueWithChangeEvent(this.model, this.model.documents); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts index 6236c75a2f8..d23ad8c87f6 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts @@ -4,22 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { Dimension } from '../../../../base/browser/dom.js'; +import { Event } from '../../../../base/common/event.js'; +import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { derived, derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from '../../../../base/common/observable.js'; -import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; -import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; -import { IMultiDiffEditorViewState, IMultiDiffResourceId, MultiDiffEditorWidgetImpl } from './multiDiffEditorWidgetImpl.js'; -import { MultiDiffEditorViewModel } from './multiDiffEditorViewModel.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import './colors.js'; -import { DiffEditorItemTemplate } from './diffEditorItemTemplate.js'; -import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; -import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { Range } from '../../../common/core/range.js'; import { IDiffEditor } from '../../../common/editorCommon.js'; import { ICodeEditor } from '../../editorBrowser.js'; import { DiffEditorWidget } from '../diffEditor/diffEditorWidget.js'; -import { Range } from '../../../common/core/range.js'; +import './colors.js'; +import { DiffEditorItemTemplate } from './diffEditorItemTemplate.js'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; +import { MultiDiffEditorViewModel } from './multiDiffEditorViewModel.js'; +import { IMultiDiffEditorViewState, IMultiDiffResourceId, MultiDiffEditorWidgetImpl } from './multiDiffEditorWidgetImpl.js'; +import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; export class MultiDiffEditorWidget extends Disposable { private readonly _dimension = observableValue(this, undefined); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index 4069feb23d1..ec0bf3cf64c 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -9,29 +9,28 @@ import { compareBy, numberComparator } from '../../../../base/common/arrays.js'; import { findFirstMax } from '../../../../base/common/arraysFind.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, IReader, autorun, autorunWithStore, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { ITransaction, disposableObservableValue, globalTransaction, transaction } from '../../../../base/common/observableInternal/base.js'; +import { IObservable, IReader, ITransaction, autorun, autorunWithStore, derived, derivedWithStore, disposableObservableValue, globalTransaction, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { Scrollable, ScrollbarVisibility } from '../../../../base/common/scrollable.js'; import { URI } from '../../../../base/common/uri.js'; -import './style.css'; -import { ICodeEditor } from '../../editorBrowser.js'; -import { ObservableElementSizeObserver } from '../diffEditor/utils.js'; -import { RevealOptions } from './multiDiffEditorWidget.js'; -import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; +import { localize } from '../../../../nls.js'; +import { ContextKeyValue, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { OffsetRange } from '../../../common/core/offsetRange.js'; import { IRange } from '../../../common/core/range.js'; import { ISelection, Selection } from '../../../common/core/selection.js'; import { IDiffEditor } from '../../../common/editorCommon.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; -import { ContextKeyValue, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; +import { ICodeEditor } from '../../editorBrowser.js'; +import { ObservableElementSizeObserver } from '../diffEditor/utils.js'; import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate.js'; -import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel.js'; -import { ObjectPool } from './objectPool.js'; -import { localize } from '../../../../nls.js'; import { IDocumentDiffItem } from './model.js'; +import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel.js'; +import { RevealOptions } from './multiDiffEditorWidget.js'; +import { ObjectPool } from './objectPool.js'; +import './style.css'; +import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; export class MultiDiffEditorWidgetImpl extends Disposable { private readonly _scrollableElements = h('div.scrollContent', [ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index d41aea21727..595ab703f28 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -4,20 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { transaction } from '../../../../../base/common/observable.js'; -import { asyncTransaction } from '../../../../../base/common/observableInternal/base.js'; -import { ICodeEditor } from '../../../../browser/editorBrowser.js'; -import { EditorAction, ServicesAccessor } from '../../../../browser/editorExtensions.js'; -import { EditorContextKeys } from '../../../../common/editorContextKeys.js'; -import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId, inlineSuggestCommitId } from './commandIds.js'; -import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; -import { InlineCompletionsController } from './inlineCompletionsController.js'; -import { Context as SuggestContext } from '../../../suggest/browser/suggest.js'; +import { asyncTransaction, transaction } from '../../../../../base/common/observable.js'; import * as nls from '../../../../../nls.js'; -import { MenuId, Action2 } from '../../../../../platform/actions/common/actions.js'; +import { Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ICodeEditor } from '../../../../browser/editorBrowser.js'; +import { EditorAction, ServicesAccessor } from '../../../../browser/editorExtensions.js'; +import { EditorContextKeys } from '../../../../common/editorContextKeys.js'; +import { Context as SuggestContext } from '../../../suggest/browser/suggest.js'; +import { inlineSuggestCommitId, showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from './commandIds.js'; +import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; +import { InlineCompletionsController } from './inlineCompletionsController.js'; export class ShowNextInlineSuggestionAction extends EditorAction { public static ID = showNextInlineSuggestionActionId; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 97064c64434..424715a2aab 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -9,10 +9,7 @@ import { timeout } from '../../../../../base/common/async.js'; import { cancelOnDispose } from '../../../../../base/common/cancellation.js'; import { readHotReloadableExport } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, ITransaction, autorun, constObservable, derived, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from '../../../../../base/common/observable.js'; -import { ISettableObservable } from '../../../../../base/common/observableInternal/base.js'; -import { derivedDisposable } from '../../../../../base/common/observableInternal/derived.js'; -import { derivedObservableWithCache, mapObservableArrayCached, runOnChange, runOnChangeWithStore } from '../../../../../base/common/observableInternal/utils.js'; +import { IObservable, ISettableObservable, ITransaction, autorun, constObservable, derived, derivedDisposable, derivedObservableWithCache, mapObservableArrayCached, observableFromEvent, observableSignal, observableValue, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; import { isUndefined } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts index 7d613257c56..1625099889f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts @@ -11,18 +11,9 @@ import { equals } from '../../../../../base/common/arrays.js'; import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, observableFromEvent } from '../../../../../base/common/observable.js'; -import { derivedWithStore } from '../../../../../base/common/observableInternal/derived.js'; +import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, derivedWithStore, observableFromEvent } from '../../../../../base/common/observable.js'; import { OS } from '../../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import './inlineCompletionsHintsWidget.css'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../browser/editorBrowser.js'; -import { EditorOption } from '../../../../common/config/editorOptions.js'; -import { Position } from '../../../../common/core/position.js'; -import { Command, InlineCompletionTriggerKind } from '../../../../common/languages.js'; -import { PositionAffinity } from '../../../../common/model.js'; -import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from '../controller/commandIds.js'; -import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; import { localize } from '../../../../../nls.js'; import { MenuEntryActionViewItem, createAndFillInActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; @@ -34,6 +25,14 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../browser/editorBrowser.js'; +import { EditorOption } from '../../../../common/config/editorOptions.js'; +import { Position } from '../../../../common/core/position.js'; +import { Command, InlineCompletionTriggerKind } from '../../../../common/languages.js'; +import { PositionAffinity } from '../../../../common/model.js'; +import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from '../controller/commandIds.js'; +import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; +import './inlineCompletionsHintsWidget.css'; export class InlineCompletionsHintsWidget extends Disposable { private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 362fc07a365..b0cf6e51db8 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -3,29 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { createStyleSheet2 } from '../../../../base/browser/dom.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ISettableObservable, autorun, constObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { ISettableObservable, autorun, constObservable, derivedDisposable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; +import { IDiffProviderFactoryService } from '../../../browser/widget/diffEditor/diffProviderFactoryService.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; import { EditOperation } from '../../../common/core/editOperation.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; -import { GhostTextWidget } from './ghostTextWidget.js'; -import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IInlineEdit, InlineEditTriggerKind } from '../../../common/languages.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { GhostText, GhostTextPart } from '../../inlineCompletions/browser/model/ghostText.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { InlineEditHintsWidget } from './inlineEditHintsWidget.js'; -import { EditorOption } from '../../../common/config/editorOptions.js'; -import { createStyleSheet2 } from '../../../../base/browser/dom.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { onUnexpectedExternalError } from '../../../../base/common/errors.js'; -import { derivedDisposable } from '../../../../base/common/observableInternal/derived.js'; -import { InlineEditSideBySideWidget } from './inlineEditSideBySideWidget.js'; -import { IDiffProviderFactoryService } from '../../../browser/widget/diffEditor/diffProviderFactoryService.js'; import { IModelService } from '../../../common/services/model.js'; +import { GhostText, GhostTextPart } from '../../inlineCompletions/browser/model/ghostText.js'; +import { GhostTextWidget } from './ghostTextWidget.js'; +import { InlineEditHintsWidget } from './inlineEditHintsWidget.js'; +import { InlineEditSideBySideWidget } from './inlineEditSideBySideWidget.js'; export class InlineEditController extends Disposable { static ID = 'editor.contrib.inlineEditController'; diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts index a4daba0c44e..4655e81186d 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts @@ -6,10 +6,9 @@ import { $ } from '../../../../base/browser/dom.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ObservablePromise, autorun, autorunWithStore, derived, observableSignalFromEvent } from '../../../../base/common/observable.js'; -import { derivedDisposable } from '../../../../base/common/observableInternal/derived.js'; +import { IObservable, ObservablePromise, autorun, autorunWithStore, derived, derivedDisposable, observableSignalFromEvent } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; -import './inlineEditSideBySideWidget.css'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; import { EmbeddedCodeEditorWidget } from '../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; @@ -24,7 +23,7 @@ import { PLAINTEXT_LANGUAGE_ID } from '../../../common/languages/modesRegistry.j import { IModelDeltaDecoration } from '../../../common/model.js'; import { TextModel } from '../../../common/model/textModel.js'; import { IModelService } from '../../../common/services/model.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import './inlineEditSideBySideWidget.css'; function* range(start: number, end: number, step = 1) { if (end === undefined) { [end, start] = [start, 0]; } diff --git a/src/vs/editor/contrib/inlineEdits/browser/commands.ts b/src/vs/editor/contrib/inlineEdits/browser/commands.ts index 4314aad0e5f..36ebdbf1aa1 100644 --- a/src/vs/editor/contrib/inlineEdits/browser/commands.ts +++ b/src/vs/editor/contrib/inlineEdits/browser/commands.ts @@ -5,17 +5,16 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { transaction } from '../../../../base/common/observable.js'; -import { asyncTransaction } from '../../../../base/common/observableInternal/base.js'; +import { asyncTransaction, transaction } from '../../../../base/common/observable.js'; +import * as nls from '../../../../nls.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorAction, ServicesAccessor } from '../../../browser/editorExtensions.js'; import { EmbeddedCodeEditorWidget } from '../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { inlineEditAcceptId, inlineEditVisible, showNextInlineEditActionId, showPreviousInlineEditActionId } from './consts.js'; import { InlineEditsController } from './inlineEditsController.js'; -import * as nls from '../../../../nls.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; function labelAndAlias(str: nls.ILocalizedString): { label: string; alias: string } { diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts index 784c3d57231..893cb11d931 100644 --- a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts @@ -3,22 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { derived, derivedObservableWithCache, IReader, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; -import { derivedDisposable, derivedWithSetter } from '../../../../base/common/observableInternal/derived.js'; +import { derived, derivedDisposable, derivedObservableWithCache, derivedWithSetter, IReader, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { bindContextKey, observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; -import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; import { Selection } from '../../../common/core/selection.js'; import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { inlineEditVisible, isPinnedContextKey } from './consts.js'; import { InlineEditsModel } from './inlineEditsModel.js'; import { InlineEditsWidget } from './inlineEditsWidget.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { bindContextKey, observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; export class InlineEditsController extends Disposable { static ID = 'editor.contrib.inlineEditsController'; diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts index 98e8b9909be..5b226750016 100644 --- a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts @@ -8,8 +8,7 @@ import { CancellationToken, cancelOnDispose } from '../../../../base/common/canc import { itemsEquals, structuralEquals } from '../../../../base/common/equals.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ISettableObservable, ITransaction, ObservablePromise, derived, derivedHandleChanges, derivedOpts, disposableObservableValue, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction } from '../../../../base/common/observable.js'; -import { derivedDisposable } from '../../../../base/common/observableInternal/derived.js'; +import { IObservable, ISettableObservable, ITransaction, ObservablePromise, derived, derivedDisposable, derivedHandleChanges, derivedOpts, disposableObservableValue, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { IDiffProviderFactoryService } from '../../../browser/widget/diffEditor/diffProviderFactoryService.js'; diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts index f974af7800c..3da0aedd94a 100644 --- a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts @@ -6,9 +6,10 @@ import { h, svgElem } from '../../../../base/browser/dom.js'; import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, derived, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; -import { derivedWithSetter } from '../../../../base/common/observableInternal/derived.js'; -import './inlineEditsWidget.css'; +import { autorun, constObservable, derived, derivedWithSetter, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; +import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorExtensionsRegistry } from '../../../browser/editorExtensions.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; @@ -24,9 +25,7 @@ import { TextModel } from '../../../common/model/textModel.js'; import { ContextMenuController } from '../../contextmenu/browser/contextmenu.js'; import { PlaceholderTextContribution } from '../../placeholderText/browser/placeholderTextContribution.js'; import { SuggestController } from '../../suggest/browser/suggestController.js'; -import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import './inlineEditsWidget.css'; export class InlineEdit { constructor( diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts index 492d439ed23..b06224c3e9d 100644 --- a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts @@ -6,9 +6,7 @@ import { h } from '../../../../base/browser/dom.js'; import { structuralEquals } from '../../../../base/common/equals.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, derivedObservableWithCache, derivedOpts, IObservable, IReader } from '../../../../base/common/observable.js'; -import { DebugOwner } from '../../../../base/common/observableInternal/debugName.js'; -import { derivedWithStore } from '../../../../base/common/observableInternal/derived.js'; +import { autorun, constObservable, DebugOwner, derivedObservableWithCache, derivedOpts, derivedWithStore, IObservable, IReader } from '../../../../base/common/observable.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 6cf0e711027..ed312b7113e 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -8,8 +8,7 @@ import { getStructuralKey } from '../../../base/common/equals.js'; import { Event, IValueWithChangeEvent } from '../../../base/common/event.js'; import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { FileAccess } from '../../../base/common/network.js'; -import { derived, observableFromEvent } from '../../../base/common/observable.js'; -import { ValueWithChangeEventFromObservable } from '../../../base/common/observableInternal/utils.js'; +import { derived, observableFromEvent, ValueWithChangeEventFromObservable } from '../../../base/common/observable.js'; import { localize } from '../../../nls.js'; import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts index a75b0fcc3fb..225357badc9 100644 --- a/src/vs/platform/observable/common/platformObservableUtils.ts +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from '../../../base/common/lifecycle.js'; -import { autorunOpts, IObservable, IReader } from '../../../base/common/observable.js'; -import { observableFromEventOpts } from '../../../base/common/observableInternal/utils.js'; +import { autorunOpts, IObservable, IReader, observableFromEventOpts } from '../../../base/common/observable.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ContextKeyValue, IContextKeyService, RawContextKey } from '../../contextkey/common/contextkey.js'; diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts index 8f2e6afed40..28d6fe3edef 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts @@ -5,20 +5,19 @@ import { disposableTimeout } from '../../../../base/common/async.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { IReader, autorun, autorunWithStore, derived, observableFromEvent, observableFromPromise } from '../../../../base/common/observable.js'; -import { observableFromValueWithChangeEvent, observableSignalFromEvent, wasEventTriggeredRecently } from '../../../../base/common/observableInternal/utils.js'; +import { IReader, autorun, autorunWithStore, derived, observableFromEvent, observableFromPromise, observableFromValueWithChangeEvent, observableSignalFromEvent, wasEventTriggeredRecently } from '../../../../base/common/observable.js'; import { isDefined } from '../../../../base/common/types.js'; import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../editor/common/core/position.js'; import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { FoldingController } from '../../../../editor/contrib/folding/browser/folding.js'; -import { AccessibilitySignal, AccessibilityModality, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { AccessibilityModality, AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IDebugService } from '../../debug/common/debug.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IDebugService } from '../../debug/common/debug.js'; export class EditorTextPropertySignalsContribution extends Disposable implements IWorkbenchContribution { private readonly _textProperties: TextProperty[] = [ diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 6d882fdfca3..5a5b1a36679 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -11,8 +11,7 @@ import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; -import { IObservable } from '../../../../base/common/observable.js'; -import { observableValue } from '../../../../base/common/observableInternal/base.js'; +import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts index 25632f11e6b..225089a5908 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts @@ -11,8 +11,7 @@ import { Disposable, DisposableStore, IDisposable, IReference } from '../../../. import { parse } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { deepClone } from '../../../../base/common/objects.js'; -import { ObservableLazyPromise, autorun, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { ValueWithChangeEventFromObservable, constObservable, mapObservableArrayCached, observableFromValueWithChangeEvent, recomputeInitiallyAndOnChange } from '../../../../base/common/observableInternal/utils.js'; +import { ObservableLazyPromise, ValueWithChangeEventFromObservable, autorun, constObservable, derived, mapObservableArrayCached, observableFromEvent, observableFromValueWithChangeEvent, observableValue, recomputeInitiallyAndOnChange } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { isDefined, isObject } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -28,10 +27,10 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IEditorConfiguration } from '../../../browser/parts/editor/textEditor.js'; import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, EditorInputWithOptions, GroupIdentifier, IEditorSerializer, IResourceMultiDiffEditorInput, IRevertOptions, ISaveOptions, IUntypedEditorInput } from '../../../common/editor.js'; import { EditorInput, IEditorCloseHandler } from '../../../common/editor/editorInput.js'; -import { MultiDiffEditorIcon } from './icons.contribution.js'; -import { IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; import { IEditorResolverService, RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js'; import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { MultiDiffEditorIcon } from './icons.contribution.js'; +import { IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; export class MultiDiffEditorInput extends EditorInput implements ILanguageSupport { public static fromResourceMultiDiffEditorInput(input: IResourceMultiDiffEditorInput, instantiationService: IInstantiationService): MultiDiffEditorInput { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts index c907958ab68..cdd3aac6b29 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts @@ -4,18 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; -import { observableFromEvent, waitForState } from '../../../../base/common/observable.js'; -import { ValueWithChangeEventFromObservable } from '../../../../base/common/observableInternal/utils.js'; +import { observableFromEvent, ValueWithChangeEventFromObservable, waitForState } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { IMultiDiffEditorOptions } from '../../../../editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.js'; import { localize2 } from '../../../../nls.js'; import { Action2 } from '../../../../platform/actions/common/actions.js'; import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; -import { ISCMRepository, ISCMResourceGroup, ISCMService } from '../../scm/common/scm.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ISCMRepository, ISCMResourceGroup, ISCMService } from '../../scm/common/scm.js'; +import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; export class ScmMultiDiffSourceResolver implements IMultiDiffSourceResolver { private static readonly _scheme = 'scm-multi-diff-source'; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 0b9c0b6bcbf..19e3594791b 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -16,7 +16,7 @@ import { fromNow } from '../../../../base/common/date.js'; import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, autorunWithStoreHandleChanges, derived, IObservable, observableValue, waitForState } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, autorunWithStoreHandleChanges, derived, IObservable, observableValue, waitForState, constObservable, latestChangedValue, observableFromEvent, runOnChange, signalFromObservable } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -48,7 +48,6 @@ import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/ac import { delta, groupBy, tail } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; -import { constObservable, latestChangedValue, observableFromEvent, runOnChange, signalFromObservable } from '../../../../base/common/observableInternal/utils.js'; import { ContextKeys } from './scmViewPane.js'; import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IDropdownMenuActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 7952dfbce0a..38c0119b7de 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -18,7 +18,7 @@ import { binarySearch } from '../../../../base/common/arrays.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { derivedObservableWithCache, latestChangedValue, observableFromEventOpts } from '../../../../base/common/observableInternal/utils.js'; +import { derivedObservableWithCache, latestChangedValue, observableFromEventOpts } from '../../../../base/common/observable.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; From 2a27ab1e48952c89ed66e537309ded7abc8132d4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:07:54 +0200 Subject: [PATCH 225/286] Fix typo --- extensions/git/src/historyProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 3d115d8c116..02e3da449d8 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -275,7 +275,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec if (historyItemRefs.length === 0) { // TODO@lszomoru - log return undefined; - } else if (historyItemRefs.length === 1 && historyItemRefs[0] === this.currentHistoryItemRemoteRef?.id) { + } else if (historyItemRefs.length === 1 && historyItemRefs[0] === this.currentHistoryItemRef?.id) { // Remote if (this.currentHistoryItemRemoteRef) { const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemRemoteRef.id); From 0cb3ffae0f0855772c1cecf25cab92ffaa688889 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:14:26 +0200 Subject: [PATCH 226/286] Observables - add previousValue to runOnChange* (#227725) --- src/vs/base/common/observableInternal/utils.ts | 13 ++++++++----- .../controller/inlineCompletionsController.ts | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index 47478220dae..2fb57d1a42a 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -625,7 +625,8 @@ export function derivedConstOnceDefined(owner: DebugOwner, fn: (reader: IRead type RemoveUndefined = T extends undefined ? never : T; -export function runOnChange(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[]) => void): IDisposable { +export function runOnChange(observable: IObservable, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[]) => void): IDisposable { + let _previousValue: T | undefined; return autorunWithStoreHandleChanges({ createEmptyChangeSummary: () => ({ deltas: [] as RemoveUndefined[], didChange: false }), handleChange: (context, changeSummary) => { @@ -640,17 +641,19 @@ export function runOnChange(observable: IObservable, cb: }, }, (reader, changeSummary) => { const value = observable.read(reader); + const previousValue = _previousValue; if (changeSummary.didChange) { - cb(value, changeSummary.deltas); + _previousValue = value; + cb(value, previousValue, changeSummary.deltas); } }); } -export function runOnChangeWithStore(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { +export function runOnChangeWithStore(observable: IObservable, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { const store = new DisposableStore(); - const disposable = runOnChange(observable, (value, deltas) => { + const disposable = runOnChange(observable, (value, previousValue: undefined | T, deltas) => { store.clear(); - cb(value, deltas, store); + cb(value, previousValue, deltas, store); }); return { dispose() { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 424715a2aab..dcd8e176d5b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -155,7 +155,7 @@ export class InlineCompletionsController extends Disposable { } })); - this._register(runOnChange(this._editorObs.selections, (_value, changes) => { + this._register(runOnChange(this._editorObs.selections, (_value, _, changes) => { if (changes.some(e => e.reason === CursorChangeReason.Explicit || e.source === 'api')) { this.model.get()?.stop(); } @@ -203,7 +203,7 @@ export class InlineCompletionsController extends Disposable { this._playAccessibilitySignal.read(reader); currentInlineCompletionBySemanticId.read(reader); return {}; - }), async (_value, _deltas, store) => { + }), async (_value, _, _deltas, store) => { /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ const model = this.model.get(); const state = model?.state.get(); From eb80343e111cd8c01650e4830707c6af4143ccf5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 10 Sep 2024 16:17:58 +0200 Subject: [PATCH 227/286] Improve extensions UX for incompatible extensions (#228106) #227047 - Improve extensions UX for incompatible extensions - show a warning badge on the extensions activity icon - show a warning in the extensions view --- .../common/extensionsScannerService.ts | 4 +- .../extensions/common/extensionValidator.ts | 2 +- .../activitybar/media/activityaction.css | 17 --- .../browser/parts/compositeBarActions.ts | 37 ++++-- .../browser/parts/globalCompositeBar.ts | 3 +- .../browser/parts/media/paneCompositePart.css | 4 + .../extensions/browser/extensionsActions.ts | 8 +- .../extensions/browser/extensionsViewlet.ts | 109 ++++++++++++++---- .../extensions/browser/extensionsViews.ts | 28 ++++- .../extensions/browser/extensionsWidgets.ts | 9 +- .../browser/extensionsWorkbenchService.ts | 76 +++++++++++- .../browser/media/extensionsViewlet.css | 63 ++++++++-- .../contrib/extensions/common/extensions.ts | 15 ++- .../services/activity/common/activity.ts | 74 +++++++++++- 14 files changed, 361 insertions(+), 88 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 8f238a84029..aaa82639b30 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -689,8 +689,8 @@ class ExtensionsScanner extends Disposable { validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension { let isValid = true; - const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); - const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, validateApiVersion); + // const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); + const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, true); for (const [severity, message] of validations) { if (severity === Severity.Error) { isValid = false; diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index d66cf72af3b..4a9d3295696 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -369,7 +369,7 @@ export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): b continue; } if (existingProposal.version !== version) { - incompatibleNotices.push(nls.localize('apiProposalMismatch', "Extension is using an API proposal '{0}' that is not compatible with the current version of VS Code.", proposalName)); + incompatibleNotices.push(nls.localize('apiProposalMismatch', "This extension is using the API proposal '{0}' that is not compatible with the current version of VS Code.", proposalName)); } } notices?.push(...incompatibleNotices); diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index b40341d217c..22cd4083df3 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -169,23 +169,6 @@ text-align: center; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge .profile-icon-overlay { - position: absolute; - top: 27px; - right: 6px; - background-color: var(--vscode-activityBar-background); -} - -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge .profile-icon-overlay .codicon { - color: var(--vscode-activityBar-inactiveForeground); -} - -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active .profile-badge .profile-icon-overlay .codicon, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .profile-badge .profile-icon-overlay .codicon, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover .profile-badge .profile-icon-overlay .codicon { - color: var(--vscode-activityBar-foreground) !important; -} - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge .profile-text-overlay { position: absolute; font-weight: 600; diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 61ddfcd61fb..15f6b06331c 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -10,7 +10,7 @@ import { ICommandService } from '../../../platform/commands/common/commands.js'; import { toDisposable, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js'; import { IThemeService, IColorTheme } from '../../../platform/theme/common/themeService.js'; -import { NumberBadge, IBadge, IActivity, ProgressBadge } from '../../services/activity/common/activity.js'; +import { NumberBadge, IBadge, IActivity, ProgressBadge, IconBadge } from '../../services/activity/common/activity.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { DelayedDragHandler } from '../../../base/browser/dnd.js'; import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; @@ -154,7 +154,7 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { protected override readonly options: ICompositeBarActionViewItemOptions; private badgeContent: HTMLElement | undefined; - private readonly badgeDisposable = this._register(new MutableDisposable()); + private readonly badgeDisposable = this._register(new MutableDisposable()); private mouseUpTimeout: any; private keybindingLabel: string | undefined | null; @@ -214,9 +214,10 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { // Badge if (this.badgeContent) { - const badgeFg = colors.badgeForeground ?? theme.getColor(badgeForeground); - const badgeBg = colors.badgeBackground ?? theme.getColor(badgeBackground); - const contrastBorderColor = theme.getColor(contrastBorder); + const badgeStyles = this.getActivity()?.badge.getColors(theme); + const badgeFg = badgeStyles?.badgeForeground ?? colors.badgeForeground ?? theme.getColor(badgeForeground); + const badgeBg = badgeStyles?.badgeBackground ?? colors.badgeBackground ?? theme.getColor(badgeBackground); + const contrastBorderColor = badgeStyles?.badgeBorder ?? theme.getColor(contrastBorder); this.badgeContent.style.color = badgeFg ? badgeFg.toString() : ''; this.badgeContent.style.backgroundColor = badgeBg ? badgeBg.toString() : ''; @@ -285,15 +286,21 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { this.updateStyles(); } + private getActivity(): IActivity | undefined { + if (this._action instanceof CompositeBarAction) { + return this._action.activity; + } + return undefined; + } + protected updateActivity(): void { - const action = this.action; - if (!this.badge || !this.badgeContent || !(action instanceof CompositeBarAction)) { + if (!this.badge || !this.badgeContent || !(this._action instanceof CompositeBarAction)) { return; } - const activity = action.activity; + const activity = this.getActivity(); - this.badgeDisposable.clear(); + this.badgeDisposable.value = new DisposableStore(); clearNode(this.badgeContent); hide(this.badge); @@ -336,14 +343,24 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { } } + // Icon + else if (badge instanceof IconBadge) { + classes.push('icon-badge'); + const badgeContentClassess = ['icon-overlay', ...ThemeIcon.asClassNameArray(badge.icon)]; + this.badgeContent.classList.add(...badgeContentClassess); + this.badgeDisposable.value.add(toDisposable(() => this.badgeContent?.classList.remove(...badgeContentClassess))); + show(this.badge); + } + if (classes.length) { this.badge.classList.add(...classes); - this.badgeDisposable.value = toDisposable(() => this.badge.classList.remove(...classes)); + this.badgeDisposable.value.add(toDisposable(() => this.badge.classList.remove(...classes))); } } this.updateTitle(); + this.updateStyles(); } protected override updateLabel(): void { diff --git a/src/vs/workbench/browser/parts/globalCompositeBar.ts b/src/vs/workbench/browser/parts/globalCompositeBar.ts index 34f790d6107..8c96ab3e461 100644 --- a/src/vs/workbench/browser/parts/globalCompositeBar.ts +++ b/src/vs/workbench/browser/parts/globalCompositeBar.ts @@ -602,8 +602,7 @@ export class GlobalActivityActionViewItem extends AbstractGlobalActivityActionVi } show(this.profileBadge); - this.profileBadgeContent.classList.toggle('profile-text-overlay', true); - this.profileBadgeContent.classList.toggle('profile-icon-overlay', false); + this.profileBadgeContent.classList.add('profile-text-overlay'); this.profileBadgeContent.textContent = this.userDataProfileService.currentProfile.name.substring(0, 2).toUpperCase(); } diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 52f116455cf..1f2bd9ee9d5 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -205,6 +205,10 @@ position: relative; } +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .icon-badge .badge-content { + padding: 3px; +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { position: absolute; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 974c01deb12..0390170863a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2653,13 +2653,17 @@ export class ExtensionStatusAction extends ExtensionAction { // Extension is disabled by its dependency if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency) { - this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('extension disabled because of dependency', "This extension has been disabled because it depends on an extension that is disabled.")) }, true); + this.updateStatus({ + icon: warningIcon, + message: new MarkdownString(localize('extension disabled because of dependency', "This extension depends on an extension that is disabled.")) + .appendMarkdown(` [${localize('dependencies', "Show Dependencies")}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Dependencies]))}`)})`) + }, true); return; } if (!this.extension.local.isValid) { const errors = this.extension.local.validations.filter(([severity]) => severity === Severity.Error).map(([, message]) => message); - this.updateStatus({ icon: errorIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); + this.updateStatus({ icon: warningIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); return; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index e99f31a59ec..f6f9270f85a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -9,10 +9,10 @@ import { timeout, Delayer, Promises } from '../../../../base/common/async.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { createErrorWithActions } from '../../../../base/common/errorMessage.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { Event } from '../../../../base/common/event.js'; import { Action } from '../../../../base/common/actions.js'; -import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus } from '../../../../base/browser/dom.js'; +import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus, addDisposableListener, EventType, clearNode } from '../../../../base/browser/dom.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; @@ -25,7 +25,7 @@ import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, Reco import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import Severity from '../../../../base/common/severity.js'; -import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js'; +import { IActivityService, IBadge, NumberBadge, WarningBadge } from '../../../services/activity/common/activity.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewDescriptorService, IAddedViewDescriptorRef, ViewContainerLocation } from '../../../common/views.js'; @@ -64,6 +64,9 @@ import { ILocalizedString } from '../../../../platform/action/common/action.js'; import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); @@ -494,7 +497,9 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private searchDelayer: Delayer; private root: HTMLElement | undefined; + private header: HTMLElement | undefined; private searchBox: SuggestEnabledInput | undefined; + private notificationContainer: HTMLElement | undefined; private readonly searchViewletState: MementoObject; constructor( @@ -557,12 +562,12 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE overlay.style.backgroundColor = overlayBackgroundColor; hide(overlay); - const header = append(this.root, $('.header')); + this.header = append(this.root, $('.header')); const placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); const searchValue = this.searchViewletState['query.value'] ? this.searchViewletState['query.value'] : ''; - const searchContainer = append(header, $('.extensions-search-container')); + const searchContainer = append(this.header, $('.extensions-search-container')); this.searchBox = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${VIEWLET_ID}.searchbox`, searchContainer, { triggerCharacters: ['@'], @@ -575,6 +580,10 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE provideResults: (query: string) => Query.suggestions(query) }, placeholder, 'extensions:searchinput', { placeholderText: placeholder, value: searchValue })); + this.notificationContainer = append(this.header, $('.notification-container.hidden', { 'tabindex': '0' })); + this.renderNotificaiton(); + this._register(this.extensionsWorkbenchService.onDidChangeExtensionsNotification(() => this.renderNotificaiton())); + this.updateInstalledExtensionsContexts(); if (this.searchBox.getValue()) { this.triggerSearch(); @@ -657,13 +666,18 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchBox?.focus(); } + private _dimension: Dimension | undefined; override layout(dimension: Dimension): void { + this._dimension = dimension; if (this.root) { this.root.classList.toggle('narrow', dimension.width <= 250); this.root.classList.toggle('mini', dimension.width <= 200); } this.searchBox?.layout(new Dimension(dimension.width - 34 - /*padding*/8 - (24 * 2), 20)); - super.layout(new Dimension(dimension.width, dimension.height - 41)); + const searchBoxHeight = 20 + 21 /*margin*/; + const headerHeight = this.header && !!this.notificationContainer?.childNodes.length ? this.notificationContainer.clientHeight + searchBoxHeight + 10 /*margin*/ : searchBoxHeight; + this.header!.style.height = `${headerHeight}px`; + super.layout(new Dimension(dimension.width, dimension.height - headerHeight)); } override getOptimalWidth(): number { @@ -684,6 +698,46 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } } + private readonly notificationDisposables = this._register(new MutableDisposable()); + private renderNotificaiton(): void { + if (!this.notificationContainer) { + return; + } + + clearNode(this.notificationContainer); + this.notificationDisposables.value = new DisposableStore(); + const status = this.extensionsWorkbenchService.getExtensionsNotification(); + if (status && !this.searchMarketplaceExtensionsContextKey.get()) { + this.notificationContainer.setAttribute('aria-label', status.message); + this.notificationContainer.classList.remove('hidden'); + const messageContainer = append(this.notificationContainer, $('.message-container')); + append(messageContainer, $('span')).className = SeverityIcon.className(status.severity); + append(messageContainer, $('span.message', undefined, status.message)); + const showAction = append(messageContainer, + $('span.message-text-action', { + 'tabindex': '0', + 'role': 'button', + 'aria-label': `${status.message}. ${localize('click show', "Click to Show")}` + }, localize('show', "Show"))); + const showExtensions = () => this.search(status.extensions.map(extension => `@id:${extension.identifier.id}`).join(' ')); + this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.CLICK, () => showExtensions())); + this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.KEY_DOWN, (e: KeyboardEvent) => { + const standardKeyboardEvent = new StandardKeyboardEvent(e); + if (standardKeyboardEvent.keyCode === KeyCode.Enter || standardKeyboardEvent.keyCode === KeyCode.Space) { + showExtensions(); + } + standardKeyboardEvent.stopPropagation(); + })); + } else { + this.notificationContainer.removeAttribute('aria-label'); + this.notificationContainer.classList.add('hidden'); + } + + if (this._dimension) { + this.layout(this._dimension); + } + } + private async updateInstalledExtensionsContexts(): Promise { const result = await this.extensionsWorkbenchService.queryLocal(); this.hasInstalledExtensionsContextKey.set(result.some(r => !r.isBuiltin)); @@ -737,6 +791,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.defaultViewsContextKey.set(!value || ExtensionsListView.isSortInstalledExtensionsQuery(value)); }); + this.renderNotificaiton(); + return this.progress(Promise.all(this.panes.map(view => (view).show(this.normalizedQuery(), refresh) .then(model => this.alertSearchResult(model.length, view.id)) @@ -851,27 +907,40 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution ) { super(); this.onServiceChange(); - this._register(Event.debounce(extensionsWorkbenchService.onChange, () => undefined, 100, undefined, undefined, undefined, this._store)(this.onServiceChange, this)); + this._register(Event.any(Event.debounce(extensionsWorkbenchService.onChange, () => undefined, 100, undefined, undefined, undefined, this._store), extensionsWorkbenchService.onDidChangeExtensionsNotification)(this.onServiceChange, this)); } private onServiceChange(): void { this.badgeHandle.clear(); + let badge: IBadge | undefined; - const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); - const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); - const newBadgeNumber = outdated + actionRequired.length; - if (newBadgeNumber > 0) { - let msg = ''; - if (outdated) { - msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated); + const extensionsNotification = this.extensionsWorkbenchService.getExtensionsNotification(); + if (extensionsNotification) { + if (extensionsNotification.severity === Severity.Warning) { + badge = new WarningBadge(() => extensionsNotification.message); } - if (outdated > 0 && actionRequired.length > 0) { - msg += ', '; + } + + else { + const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); + const newBadgeNumber = outdated + actionRequired.length; + if (newBadgeNumber > 0) { + let msg = ''; + if (outdated) { + msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated); + } + if (outdated > 0 && actionRequired.length > 0) { + msg += ', '; + } + if (actionRequired.length) { + msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length); + } + badge = new NumberBadge(newBadgeNumber, () => msg); } - if (actionRequired.length) { - msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length); - } - const badge = new NumberBadge(newBadgeNumber, () => msg); + } + + if (badge) { this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge }); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index a36f9be88bb..32bb10a91bc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -552,11 +552,24 @@ export class ExtensionsListView extends ViewPane { return isE1Running ? -1 : 1; }; + const incompatible: IExtension[] = []; + const missingDeps: IExtension[] = []; + const deprecated: IExtension[] = []; const outdated: IExtension[] = []; const actionRequired: IExtension[] = []; const noActionRequired: IExtension[] = []; - result.forEach(e => { - if (e.outdated) { + + for (const e of result) { + if (e.enablementState === EnablementState.DisabledByInvalidExtension) { + incompatible.push(e); + } + else if (e.enablementState === EnablementState.DisabledByExtensionDependency) { + missingDeps.push(e); + } + else if (e.deprecationInfo) { + deprecated.push(e); + } + else if (e.outdated) { outdated.push(e); } else if (e.runtimeState) { @@ -565,9 +578,16 @@ export class ExtensionsListView extends ViewPane { else { noActionRequired.push(e); } - }); + } - result = [...outdated.sort(defaultSort), ...actionRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; + result = [ + ...incompatible.sort(defaultSort), + ...missingDeps.sort(defaultSort), + ...deprecated.sort(defaultSort), + ...outdated.sort(defaultSort), + ...actionRequired.sort(defaultSort), + ...noActionRequired.sort(defaultSort) + ]; } return result; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 564e080f583..417c7bba2a7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -10,7 +10,7 @@ import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, Extension import { append, $, reset, addDisposableListener, EventType, finalHandler } from '../../../../base/browser/dom.js'; import * as platform from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; -import { EnablementState, IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { extensionButtonProminentBackground, ExtensionStatusAction } from './extensionsActions.js'; @@ -501,7 +501,7 @@ export class ExtensionActivationStatusWidget extends ExtensionWidget { return; } - const extensionStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension); + const extensionStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension); if (!extensionStatus || !extensionStatus.activationTimes) { return; } @@ -647,7 +647,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { } const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension); - const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension); + const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension); const extensionStatus = this.extensionStatusAction.status; const runtimeState = this.extension.runtimeState; const recommendationMessage = this.getRecommendationMessage(this.extension); @@ -683,9 +683,6 @@ export class ExtensionHoverWidget extends ExtensionWidget { markdown.appendMarkdown(`$(${status.icon.id}) `); } markdown.appendMarkdown(status.message.value); - if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.extension.local) { - markdown.appendMarkdown(` [${localize('dependencies', "Show Dependencies")}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Dependencies]))}`)})`); - } markdown.appendText(`\n`); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index d3deb35f309..6e966272405 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -25,7 +25,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { URI } from '../../../../base/common/uri.js'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey, VIEWLET_ID, IExtensionsViewPaneContainer } from '../common/extensions.js'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionsNotification } from '../common/extensions.js'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js'; import { IURLService, IURLHandler, IOpenURLOptions } from '../../../../platform/url/common/url.js'; import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js'; @@ -45,7 +45,7 @@ import { IUserDataAutoSyncService, IUserDataSyncEnablementService, SyncResource import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { isBoolean, isDefined, isString, isUndefined } from '../../../../base/common/types.js'; import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; -import { IExtensionService, IExtensionsStatus, toExtension, toExtensionDescription } from '../../../services/extensions/common/extensions.js'; +import { IExtensionService, IExtensionsStatus as IExtensionRuntimeStatus, toExtension, toExtensionDescription } from '../../../services/extensions/common/extensions.js'; import { isWeb, language } from '../../../../base/common/platform.js'; import { getLocale } from '../../../../platform/languagePacks/common/languagePacks.js'; import { ILocaleService } from '../../../services/localization/common/locale.js'; @@ -55,7 +55,7 @@ import { IUserDataProfileService } from '../../../services/userDataProfile/commo import { mainWindow } from '../../../../base/browser/window.js'; import { IDialogService, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js'; import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; -import { isEngineValid } from '../../../../platform/extensions/common/extensionValidator.js'; +import { areApiProposalsCompatible, isEngineValid } from '../../../../platform/extensions/common/extensionValidator.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js'; @@ -904,9 +904,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private updatesCheckDelayer: ThrottledDelayer; private autoUpdateDelayer: ThrottledDelayer; - private readonly _onChange: Emitter = new Emitter(); + private readonly _onChange = this._register(new Emitter()); get onChange(): Event { return this._onChange.event; } + private extensionsNotification: IExtensionsNotification | undefined; + private dismissedNotifications: string[] = []; + private readonly _onDidChangeExtensionsNotification = new Emitter(); + readonly onDidChangeExtensionsNotification = this._onDidChangeExtensionsNotification.event; + private readonly _onReset = new Emitter(); get onReset() { return this._onReset.event; } @@ -1033,11 +1038,16 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (this._store.isDisposed) { return; } + this.initializeAutoUpdate(); + this.updateExtensionsNotificaiton(); this.reportInstalledExtensionsTelemetry(); - this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.reportProgressFromOtherSources())); this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EXTENSIONS_AUTO_UPDATE_KEY, this._store)(e => this.onDidSelectedExtensionToAutoUpdateValueChange())); this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EXTENSIONS_DONOT_AUTO_UPDATE_KEY, this._store)(e => this.onDidSelectedExtensionToAutoUpdateValueChange())); + this._register(Event.debounce(this.onChange, () => undefined, 100)(() => { + this.updateExtensionsNotificaiton(); + this.reportProgressFromOtherSources(); + })); } private initializeAutoUpdate(): void { @@ -1329,6 +1339,60 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension ?? this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, undefined, { resourceExtension, isWorkspaceScoped })); } + private updateExtensionsNotificaiton(): void { + let extensionsNotification: IExtensionsNotification | undefined; + + let message = ''; + const severity = Severity.Warning; + const extensions: IExtension[] = []; + + extensions.push(...this.local.filter(e => e.enablementState === EnablementState.DisabledByInvalidExtension)); + if (extensions.length) { + if (extensions.some(e => e.local && + (!isEngineValid(e.local.manifest.engines.vscode, this.productService.version, this.productService.date) || areApiProposalsCompatible([...e.local.manifest.enabledApiProposals ?? []])) + )) { + message = nls.localize('incompatibleExtensions', "Some extensions are disabled due to version incompatibility. Review and update them."); + } else { + message = nls.localize('invalidExtensions', "You have invalid extensions installed. Review them."); + } + } + + else { + extensions.push(...this.local.filter(e => e.enablementState === EnablementState.DisabledByExtensionDependency)); + if (extensions.length) { + message = nls.localize('missingDependencies', "Some extensions are disabled due to missing dependencies. Review them."); + } + + else { + extensions.push(...this.local.filter(e => !!e.deprecationInfo)); + if (extensions.length) { + message = nls.localize('deprecated extensions', "You have deprecated extensions installed. Review them and migrate to alternatives."); + } + } + } + + if (extensions.length && !this.dismissedNotifications.includes(message)) { + extensionsNotification = { + message, + severity, + extensions, + dismiss: () => { + this.dismissedNotifications.push(message); + this.updateExtensionsNotificaiton(); + }, + }; + } + + if (this.extensionsNotification?.message !== extensionsNotification?.message) { + this.extensionsNotification = extensionsNotification; + this._onDidChangeExtensionsNotification.fire(this.extensionsNotification); + } + } + + getExtensionsNotification(): IExtensionsNotification | undefined { + return this.extensionsNotification; + } + private resolveQueryText(text: string): string { text = text.replace(/@web/g, `tag:"${WEB_EXTENSION_TAG}"`); @@ -1399,7 +1463,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } - getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined { + getExtensionRuntimeStatus(extension: IExtension): IExtensionRuntimeStatus | undefined { const extensionsStatus = this.extensionService.getExtensionsStatus(); for (const id of Object.keys(extensionsStatus)) { if (areSameExtensions({ id }, extension.identifier)) { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index a4514bd07c0..b0141e1c4fa 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -8,6 +8,11 @@ height: 100%; } +.extensions-viewlet .hidden { + display: none; + visibility: hidden; +} + .extensions-viewlet > .overlay { position: absolute; top: 0; @@ -47,6 +52,44 @@ height: 100%; } +.extensions-viewlet > .header > .notification-container { + margin-top: 10px; + display: flex; + justify-content: space-between; +} + +.extensions-viewlet > .header .notification-container .message-container { + padding-left: 4px; +} + +.extensions-viewlet > .header .notification-container .message-container .codicon { + vertical-align: text-top; + padding-right: 5px; +} + +.extensions-viewlet .notification-container .message-text-action { + cursor: pointer; + margin-left: 5px; + color: var(--vscode-textLink-foreground); + text-decoration: underline; +} + +.extensions-viewlet .notification-container .message-text-action:hover, +.extensions-viewlet .notification-container .message-text-action:active { + color: var(--vscode-textLink-activeForeground); +} + +.extensions-viewlet .notification-container .message-action { + cursor: pointer; + padding: 2px; + border-radius: 5px; + height: 16px; +} + +.extensions-viewlet .notification-container .message-action:hover { + background-color: var(--vscode-toolbar-hoverBackground); + outline: 1px dashed var(--vscode-toolbar-hoverOutline); +} .extensions-viewlet > .extensions { height: calc(100% - 41px); @@ -70,12 +113,6 @@ display: none; } -.extensions-viewlet > .extensions .extensions-list.hidden, -.extensions-viewlet > .extensions .message-container.hidden { - display: none; - visibility: hidden; -} - .extensions-viewlet > .extensions .panel-header { padding-right: 12px; } @@ -151,8 +188,14 @@ opacity: 0.5; } -.extensions-badge.progress-badge > .badge-content { - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4="); - background-position: center center; - background-repeat: no-repeat; +.extensions-viewlet .codicon-error::before { + color: var(--vscode-editorError-foreground); +} + +.extensions-viewlet .codicon-warning::before { + color: var(--vscode-editorWarning-foreground); +} + +.extensions-viewlet .codicon-info::before { + color: var(--vscode-editorInfo-foreground); } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 4c5e1efa44c..9cae86e743c 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -15,10 +15,11 @@ import { IExtensionManifest, ExtensionType } from '../../../../platform/extensio import { URI } from '../../../../base/common/uri.js'; import { IView, IViewPaneContainer } from '../../../common/views.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IExtensionsStatus } from '../../../services/extensions/common/extensions.js'; +import { IExtensionsStatus as IExtensionRuntimeStatus } from '../../../services/extensions/common/extensions.js'; import { IExtensionEditorOptions } from './extensionsInput.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { Severity } from '../../../../platform/notification/common/notification.js'; export const VIEWLET_ID = 'workbench.view.extensions'; @@ -110,6 +111,13 @@ export interface InstallExtensionOptions extends InstallOptions { enable?: boolean; } +export interface IExtensionsNotification { + readonly message: string; + readonly severity: Severity; + readonly extensions: IExtension[]; + dismiss(): void; +} + export interface IExtensionsWorkbenchService { readonly _serviceBrand: undefined; readonly onChange: Event; @@ -144,10 +152,13 @@ export interface IExtensionsWorkbenchService { openSearch(searchValue: string, focus?: boolean): Promise; getAutoUpdateValue(): AutoUpdateConfigurationValue; checkForUpdates(): Promise; - getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; + getExtensionRuntimeStatus(extension: IExtension): IExtensionRuntimeStatus | undefined; updateAll(): Promise; updateRunningExtensions(): Promise; + onDidChangeExtensionsNotification: Event; + getExtensionsNotification(): IExtensionsNotification | undefined; + // Sync APIs isExtensionIgnoredToSync(extension: IExtension): boolean; toggleExtensionIgnoredToSync(extension: IExtension): Promise; diff --git a/src/vs/workbench/services/activity/common/activity.ts b/src/vs/workbench/services/activity/common/activity.ts index 7599f94db2f..481a4648a4f 100644 --- a/src/vs/workbench/services/activity/common/activity.ts +++ b/src/vs/workbench/services/activity/common/activity.ts @@ -8,6 +8,11 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { ThemeIcon } from '../../../../base/common/themables.js'; import { Event } from '../../../../base/common/event.js'; import { ViewContainer } from '../../../common/views.js'; +import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; +import { Color } from '../../../../base/common/color.js'; +import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; +import { localize } from '../../../../nls.js'; +import { Codicon } from '../../../../base/common/codicons.js'; export interface IActivity { readonly badge: IBadge; @@ -58,23 +63,36 @@ export interface IActivityService { export interface IBadge { getDescription(): string; + getColors(theme: IColorTheme): IBadgeStyles | undefined; +} + +export interface IBadgeStyles { + readonly badgeBackground: Color | undefined; + readonly badgeForeground: Color | undefined; + readonly badgeBorder: Color | undefined; } class BaseBadge implements IBadge { - constructor(readonly descriptorFn: (arg: any) => string) { - this.descriptorFn = descriptorFn; + constructor( + protected readonly descriptorFn: (arg: any) => string, + private readonly stylesFn: ((theme: IColorTheme) => IBadgeStyles | undefined) | undefined, + ) { } getDescription(): string { return this.descriptorFn(null); } + + getColors(theme: IColorTheme): IBadgeStyles | undefined { + return this.stylesFn?.(theme); + } } export class NumberBadge extends BaseBadge { constructor(readonly number: number, descriptorFn: (num: number) => string) { - super(descriptorFn); + super(descriptorFn, undefined); this.number = number; } @@ -85,9 +103,53 @@ export class NumberBadge extends BaseBadge { } export class IconBadge extends BaseBadge { - constructor(readonly icon: ThemeIcon, descriptorFn: () => string) { - super(descriptorFn); + constructor( + readonly icon: ThemeIcon, + descriptorFn: () => string, + stylesFn?: (theme: IColorTheme) => IBadgeStyles | undefined, + ) { + super(descriptorFn, stylesFn); } } -export class ProgressBadge extends BaseBadge { } +export class ProgressBadge extends BaseBadge { + constructor(descriptorFn: () => string) { + super(descriptorFn, undefined); + } +} + +export class WarningBadge extends IconBadge { + constructor(descriptorFn: () => string) { + super(Codicon.warning, descriptorFn, (theme: IColorTheme) => ({ + badgeBackground: theme.getColor(activityWarningBadgeBackground), + badgeForeground: theme.getColor(activityWarningBadgeForeground), + badgeBorder: undefined, + })); + } +} + +export class ErrorBadge extends IconBadge { + constructor(descriptorFn: () => string) { + super(Codicon.error, descriptorFn, (theme: IColorTheme) => ({ + badgeBackground: theme.getColor(activityErrorBadgeBackground), + badgeForeground: theme.getColor(activityErrorBadgeForeground), + badgeBorder: undefined, + })); + } +} + +const activityWarningBadgeForeground = registerColor('activityWarningBadge.foreground', + { dark: Color.black.lighten(0.2), light: Color.white, hcDark: null, hcLight: null }, + localize('activityWarningBadge.foreground', 'Foreground color of the warning activity badge')); + +const activityWarningBadgeBackground = registerColor('activityWarningBadge.background', + { dark: '#CCA700', light: '#BF8803', hcDark: null, hcLight: null }, + localize('activityWarningBadge.background', 'Background color of the warning activity badge')); + +const activityErrorBadgeForeground = registerColor('activityErrorBadge.foreground', + { dark: Color.black.lighten(0.2), light: Color.white, hcDark: null, hcLight: null }, + localize('activityErrorBadge.foreground', 'Foreground color of the error activity badge')); + +const activityErrorBadgeBackground = registerColor('activityErrorBadge.background', + { dark: '#F14C4C', light: '#E51400', hcDark: null, hcLight: null }, + localize('activityErrorBadge.background', 'Background color of the error activity badge')); From f12bee2c50b7f3e6de3145ff13919cff66bf32d8 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 10 Sep 2024 17:46:33 +0200 Subject: [PATCH 228/286] apply editor: do not fallback to insert block (#228116) --- .../browser/actions/chatCodeblockActions.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index e990dadc0ef..fd64f264501 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -216,11 +216,14 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { this.notifyUserAction(chatService, context); } - protected async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + protected async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { const activeModel = codeEditor.getModel(); const range = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); const text = reindent(codeBlockActionContext.code, activeModel, range.startLineNumber); - return { edits: [new ResourceTextEdit(activeModel.uri, { range, text })] }; + if (text !== undefined) { + return { edits: [new ResourceTextEdit(activeModel.uri, { range, text })] }; + } + return undefined; } protected get showPreview() { @@ -234,6 +237,9 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { const result = await this.computeEdits(accessor, codeEditor, codeBlockActionContext); this.notifyUserAction(chatService, codeBlockActionContext, result); + if (!result) { + return; + } if (this.showPreview) { const showWithPreview = await this.applyWithInlinePreview(codeEditorService, result.edits, codeEditor); @@ -295,10 +301,10 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { } -function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number) { +function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number): string | undefined { const newContent = strings.splitLines(codeBlockContent); if (newContent.length === 0) { - return codeBlockContent; + return undefined; } const formattingOptions = model.getFormattingOptions(); @@ -316,7 +322,7 @@ function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine if (newContentIndentLevel === Number.MAX_VALUE || newContentIndentLevel === codeIndentLevel) { // all lines are empty or the indent is already correct - return codeBlockContent; + return undefined; } const newLines = []; for (let i = 0; i < newContent.length; i++) { @@ -483,7 +489,7 @@ export function registerChatCodeBlockActions() { }); } - protected override async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + protected override async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { const progressService = accessor.get(IProgressService); const notificationService = accessor.get(INotificationService); @@ -543,15 +549,14 @@ export function registerChatCodeBlockActions() { } } catch (e) { notificationService.notify({ severity: Severity.Error, message: localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message) }); - } finally { cancellationTokenSource.dispose(); } } - // fall back to inserting the code block as is - return super.computeEdits(accessor, codeEditor, codeBlockActionContext); + return undefined; } + protected override get showPreview() { return true; } From 39684940e825a198c828064ab625650b96bb25d0 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 10 Sep 2024 18:40:10 +0200 Subject: [PATCH 229/286] Cleanup debug model. (#223673) --- src/vs/base/common/collections.ts | 2 +- src/vs/base/common/event.ts | 29 ++++++++++++++++++- .../contrib/debug/common/debugModel.ts | 21 +++++--------- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index d0df190c75b..a47e629160e 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -32,7 +32,7 @@ export function groupBy(data: V[], groupF return result; } -export function diffSets(before: Set, after: Set): { removed: T[]; added: T[] } { +export function diffSets(before: ReadonlySet, after: ReadonlySet): { removed: T[]; added: T[] } { const removed: T[] = []; const added: T[] = []; for (const element of before) { diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index e40c0caace9..915f481fba6 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from './cancellation.js'; +import { diffSets } from './collections.js'; import { onUnexpectedError } from './errors.js'; import { createSingleCallFunction } from './functional.js'; import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from './lifecycle.js'; @@ -12,7 +13,6 @@ import { IObservable, IObserver } from './observable.js'; import { StopWatch } from './stopwatch.js'; import { MicrotaskDelay } from './symbols.js'; - // ----------------------------------------------------------------------------------------------------------------------- // Uncomment the next line to print warnings whenever a listener is GC'ed without having been disposed. This is a LEAK. // ----------------------------------------------------------------------------------------------------------------------- @@ -1791,3 +1791,30 @@ class ConstValueWithChangeEvent implements IValueWithChangeEvent { constructor(readonly value: T) { } } + +/** + * @param handleItem Is called for each item in the set (but only the first time the item is seen in the set). + * The returned disposable is disposed if the item is no longer in the set. + */ +export function trackSetChanges(getData: () => ReadonlySet, onDidChangeData: Event, handleItem: (d: T) => IDisposable): IDisposable { + const map = new DisposableMap(); + let oldData = new Set(getData()); + for (const d of oldData) { + map.set(d, handleItem(d)); + } + + const store = new DisposableStore(); + store.add(onDidChangeData(() => { + const newData = getData(); + const diff = diffSets(oldData, newData); + for (const r of diff.removed) { + map.deleteAndDispose(r); + } + for (const a of diff.added) { + map.set(a, handleItem(a)); + } + oldData = new Set(newData); + })); + store.add(map); + return store; +} diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index e6dba4a911e..9ae7c7333a5 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -8,9 +8,9 @@ import { findLastIdx } from '../../../../base/common/arraysFind.js'; import { DeferredPromise, RunOnceScheduler } from '../../../../base/common/async.js'; import { VSBuffer, decodeBase64, encodeBase64 } from '../../../../base/common/buffer.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; +import { Emitter, Event, trackSetChanges } from '../../../../base/common/event.js'; import { stringHash } from '../../../../base/common/hash.js'; -import { Disposable, DisposableMap, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { mixin } from '../../../../base/common/objects.js'; import { autorun } from '../../../../base/common/observable.js'; import * as resources from '../../../../base/common/resources.js'; @@ -1422,7 +1422,6 @@ export class DebugModel extends Disposable implements IDebugModel { private exceptionBreakpoints!: ExceptionBreakpoint[]; private dataBreakpoints!: DataBreakpoint[]; private watchExpressions!: Expression[]; - private watchExpressionChangeListeners: DisposableMap = this._register(new DisposableMap()); private instructionBreakpoints: InstructionBreakpoint[]; constructor( @@ -1446,12 +1445,14 @@ export class DebugModel extends Disposable implements IDebugModel { this._onDidChangeWatchExpressions.fire(undefined); })); + this._register(trackSetChanges( + () => new Set(this.watchExpressions), + this.onDidChangeWatchExpressions, + (we) => we.onDidChangeValue((e) => this._onDidChangeWatchExpressionValue.fire(e))) + ); + this.instructionBreakpoints = []; this.sessions = []; - - for (const we of this.watchExpressions) { - this.watchExpressionChangeListeners.set(we.getId(), we.onDidChangeValue((e) => this._onDidChangeWatchExpressionValue.fire(e))); - } } getId(): string { @@ -2025,7 +2026,6 @@ export class DebugModel extends Disposable implements IDebugModel { addWatchExpression(name?: string): IExpression { const we = new Expression(name || ''); - this.watchExpressionChangeListeners.set(we.getId(), we.onDidChangeValue((e) => this._onDidChangeWatchExpressionValue.fire(e))); this.watchExpressions.push(we); this._onDidChangeWatchExpressions.fire(we); @@ -2043,11 +2043,6 @@ export class DebugModel extends Disposable implements IDebugModel { removeWatchExpressions(id: string | null = null): void { this.watchExpressions = id ? this.watchExpressions.filter(we => we.getId() !== id) : []; this._onDidChangeWatchExpressions.fire(undefined); - if (!id) { - this.watchExpressionChangeListeners.clearAndDisposeAll(); - return; - } - this.watchExpressionChangeListeners.deleteAndDispose(id); } moveWatchExpression(id: string, position: number): void { From ce2e3712c96c8d5287b131a48a1dbc8c06f4e85a Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 10 Sep 2024 10:31:42 -0700 Subject: [PATCH 230/286] testing: implement getControllersWithTests command (#228135) Saves having to use a full test observer when only checking for presence --- .../contrib/testing/browser/testing.contribution.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index c7b89e49751..31b4073e7b4 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -235,5 +235,15 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: 'vscode.testing.getControllersWithTests', + handler: async (accessor: ServicesAccessor) => { + const testService = accessor.get(ITestService); + return [...testService.collection.rootItems] + .filter(r => r.children.size > 0) + .map(r => r.controllerId); + } +}); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(testingConfiguration); From 5e16ad2d31f3add6036f94864a331567098fd461 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 10 Sep 2024 19:45:09 +0200 Subject: [PATCH 231/286] Edit Context: type new line in the 'beforeinput' event of the dom node on typeInput 'insertParagraph' (#228128) emit the new line event in the beforeinput event --- .../controller/editContext/native/nativeEditContext.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 62749f46311..be0cc5053a8 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -78,14 +78,12 @@ export class NativeEditContext extends AbstractEditContext { if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) { standardKeyboardEvent.stopPropagation(); } - // Enter key presses are not sent as text update events, hence we need to handle them outside of the text update event - // The beforeinput and input events send `insertParagraph` and `insertLineBreak` events but only on input elements - // Hence we handle the enter key press in the keydown event - if (standardKeyboardEvent.keyCode === KeyCode.Enter) { + viewController.emitKeyDown(standardKeyboardEvent); + })); + this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => { + if (e.inputType === 'insertParagraph') { this._onType(viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); } - - viewController.emitKeyDown(standardKeyboardEvent); })); // Edit context events From 38ae3ed024e8282b012027ac01e1458595fe2c19 Mon Sep 17 00:00:00 2001 From: John Murray Date: Tue, 10 Sep 2024 18:46:57 +0100 Subject: [PATCH 232/286] Correct tooltip capitalization of debug panel in status bar (fix #228088) (#228089) --- src/vs/workbench/contrib/debug/browser/debugStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index 9c8c5141fba..c98e63ab7b7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -66,7 +66,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { name: nls.localize('status.debug', "Debug"), text: '$(debug-alt-small) ' + text, ariaLabel: nls.localize('debugTarget', "Debug: {0}", text), - tooltip: nls.localize('selectAndStartDebug', "Select and start debug configuration"), + tooltip: nls.localize('selectAndStartDebug', "Select and Start Debug Configuration"), command: 'workbench.action.debug.selectandstart' }; } From 64dcee7906413348556e9e22393cbd6cd59e8d98 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 10 Sep 2024 20:21:54 +0200 Subject: [PATCH 233/286] Edit Context: Update the edit context on config change (#228138) add code to update the edit context on config change --- src/vs/editor/browser/view.ts | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 13a63145c34..0ca1ceedb27 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -91,7 +91,9 @@ export class View extends ViewEventHandler { private readonly _glyphMarginWidgets: GlyphMarginWidgets; private readonly _viewCursors: ViewCursors; private readonly _viewParts: ViewPart[]; + private readonly _viewController: ViewController; + private _experimentalEditContextEnabled: boolean; private _editContext: AbstractEditContext; private readonly _pointerHandler: PointerHandler; @@ -117,7 +119,7 @@ export class View extends ViewEventHandler { this._selections = [new Selection(1, 1, 1, 1)]; this._renderAnimationFrame = null; - const viewController = new ViewController(configuration, model, userInputEvents, commandDelegate); + this._viewController = new ViewController(configuration, model, userInputEvents, commandDelegate); // The view context is passed on to most classes (basically to reduce param. counts in ctors) this._context = new ViewContext(configuration, colorTheme, model); @@ -128,10 +130,8 @@ export class View extends ViewEventHandler { this._viewParts = []; // Keyboard handler - const editContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); - this._editContext = editContextEnabled - ? this._instantiationService.createInstance(NativeEditContext, this._context, viewController) - : this._instantiationService.createInstance(TextAreaEditContext, this._context, viewController, this._createTextAreaHandlerHelper()); + this._experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); + this._editContext = this._instantiateEditContext(this._experimentalEditContextEnabled); this._viewParts.push(this._editContext); @@ -245,7 +245,29 @@ export class View extends ViewEventHandler { this._applyLayout(); // Pointer handler - this._pointerHandler = this._register(new PointerHandler(this._context, viewController, this._createPointerHandlerHelper())); + this._pointerHandler = this._register(new PointerHandler(this._context, this._viewController, this._createPointerHandlerHelper())); + } + + private _instantiateEditContext(experimentalEditContextEnabled: boolean): AbstractEditContext { + return experimentalEditContextEnabled + ? this._instantiationService.createInstance(NativeEditContext, this._context, this._viewController) + : this._instantiationService.createInstance(TextAreaEditContext, this._context, this._viewController, this._createTextAreaHandlerHelper()); + } + + private _updateEditContext(): void { + const experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); + if (this._experimentalEditContextEnabled === experimentalEditContextEnabled) { + return; + } + this._experimentalEditContextEnabled = experimentalEditContextEnabled; + this._editContext.dispose(); + this._editContext = this._instantiateEditContext(experimentalEditContextEnabled); + this._editContext.appendTo(this._overflowGuardContainer); + // Replace the view parts with the new edit context + const indexOfEditContextHandler = this._viewParts.indexOf(this._editContext); + if (indexOfEditContextHandler !== -1) { + this._viewParts.splice(indexOfEditContextHandler, 1, this._editContext); + } } private _computeGlyphMarginLanes(): IGlyphMarginLanesModel { @@ -361,6 +383,7 @@ export class View extends ViewEventHandler { } public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { this.domNode.setClassName(this._getEditorClassName()); + this._updateEditContext(); this._applyLayout(); return false; } From 4ade36e5ca66c0e66f68fe0c4871e59b1d9df686 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 20:27:27 +0200 Subject: [PATCH 234/286] =?UTF-8?q?SCM=20-=20=F0=9F=92=84=20remove=20more?= =?UTF-8?q?=20of=20the=20history=20item=20group=20terminology=20(#228139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/lib/stylelint/vscode-known-variables.json | 8 ++++---- src/vs/workbench/contrib/scm/browser/activity.ts | 6 +++--- .../workbench/contrib/scm/browser/scmHistoryViewPane.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index b1a96e14f0a..56aa02ebfb7 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -546,9 +546,9 @@ "--vscode-scmGraph-foreground1", "--vscode-scmGraph-foreground2", "--vscode-scmGraph-foreground3", - "--vscode-scmGraph-historyItemGroupBase", - "--vscode-scmGraph-historyItemGroupLocal", - "--vscode-scmGraph-historyItemGroupRemote", + "--vscode-scmGraph-historyItemBaseRefColor", + "--vscode-scmGraph-historyItemRefColor", + "--vscode-scmGraph-historyItemRemoteRefColor", "--vscode-scmGraph-historyItemHoverAdditionsForeground", "--vscode-scmGraph-historyItemHoverDefaultLabelBackground", "--vscode-scmGraph-historyItemHoverDefaultLabelForeground", @@ -881,4 +881,4 @@ "--widget-color", "--text-link-decoration" ] -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 181ccffb1eb..a5d9ca5adde 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -36,7 +36,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), () => this.scmService.repositories); - private readonly _activeRepositoryCurrentHistoryItemGroupName = derived(reader => { + private readonly _activeRepositoryHistoryItemRefName = derived(reader => { const repository = this.scmViewService.activeRepository.read(reader); const historyProvider = repository?.provider.historyProvider.read(reader); const historyItemRef = historyProvider?.historyItemRef.read(reader); @@ -109,9 +109,9 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe this._register(autorun(reader => { const repository = this.scmViewService.activeRepository.read(reader); - const currentHistoryItemGroupName = this._activeRepositoryCurrentHistoryItemGroupName.read(reader); + const historyItemRefName = this._activeRepositoryHistoryItemRefName.read(reader); - this._updateActiveRepositoryContextKeys(repository?.provider.name, currentHistoryItemGroupName); + this._updateActiveRepositoryContextKeys(repository?.provider.name, historyItemRefName); })); } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 2c39414854c..17283375763 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -766,10 +766,10 @@ class SCMHistoryViewModel extends Disposable { } const limit = clamp(this._configurationService.getValue('scm.graph.pageSize'), 1, 1000); - const historyItemGroupIds = historyItemRefs.map(ref => ref.revision ?? ref.id); + const historyItemRefIds = historyItemRefs.map(ref => ref.revision ?? ref.id); const historyItems = await historyProvider.provideHistoryItems({ - historyItemRefs: historyItemGroupIds, limit, skip: existingHistoryItems.length + historyItemRefs: historyItemRefIds, limit, skip: existingHistoryItems.length }) ?? []; state = { From 5bca50de062510593ee330ffe3712e1c023ad09c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 10 Sep 2024 11:33:44 -0700 Subject: [PATCH 235/286] debug: adopt formalized DAP ANSI styling support (#227729) * debug: adopt formalized DAP ANSI styling support Closes #227728 VS Code now advertises `supportsANSIStyling` and styles debug output with ANSI code if and only if the debug adapter supports it. This consolidates disparate output rendering logic into the `DebugExpressionRenderer` where previously we had a bunch of free-floating functions that needed different services injected. * update unstaged --- .../contrib/debug/browser/baseDebugView.ts | 124 +---------- .../debug/browser/debugANSIHandling.ts | 38 ++-- .../debug/browser/debugExpressionRenderer.ts | 193 ++++++++++++++++++ .../contrib/debug/browser/debugHover.ts | 24 +-- .../contrib/debug/browser/debugSession.ts | 5 +- .../browser/media/debug.contribution.css | 4 + .../workbench/contrib/debug/browser/repl.ts | 38 ++-- .../contrib/debug/browser/replViewer.ts | 64 +++--- .../contrib/debug/browser/variablesView.ts | 45 ++-- .../debug/browser/watchExpressionsView.ts | 27 +-- .../workbench/contrib/debug/common/debug.ts | 1 + .../contrib/debug/common/debugModel.ts | 7 +- .../contrib/debug/common/debugProtocol.d.ts | 11 +- .../contrib/debug/common/debugVisualizers.ts | 13 +- .../contrib/debug/common/replModel.ts | 16 +- .../debug/test/browser/baseDebugView.test.ts | 155 +++++++------- .../test/browser/debugANSIHandling.test.ts | 18 +- .../contrib/debug/test/browser/repl.test.ts | 4 +- .../debug/test/browser/variablesView.test.ts | 40 ++-- .../test/browser/watchExpressionView.test.ts | 21 +- .../notebookVariablesTree.ts | 16 +- .../notebookVariablesView.ts | 2 +- 22 files changed, 476 insertions(+), 390 deletions(-) create mode 100644 src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index db81687f055..9576b7e5bb7 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -17,20 +17,15 @@ import { KeyCode } from '../../../../base/common/keyCodes.js'; import { DisposableStore, IDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; -import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { COPY_EVALUATE_PATH_ID, COPY_VALUE_ID } from './debugCommands.js'; -import { DebugLinkHoverBehavior, DebugLinkHoverBehaviorTypeData, LinkDetector } from './linkDetector.js'; -import { IDebugService, IExpression, IExpressionValue } from '../common/debug.js'; -import { Expression, ExpressionContainer, Variable } from '../common/debugModel.js'; +import { IDebugService, IExpression } from '../common/debug.js'; +import { Variable } from '../common/debugModel.js'; import { IDebugVisualizerService } from '../common/debugVisualizers.js'; -import { ReplEvaluationResult } from '../common/replModel.js'; +import { LinkDetector } from './linkDetector.js'; -const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; -const booleanRegex = /^(true|false)$/i; -const stringRegex = /^(['"]).*\1$/; const $ = dom.$; export interface IRenderValueOptions { @@ -61,117 +56,6 @@ export function renderViewTree(container: HTMLElement): HTMLElement { return treeContainer; } -export function renderExpressionValue(store: DisposableStore, expressionOrValue: IExpressionValue | string, container: HTMLElement, options: IRenderValueOptions, hoverService: IHoverService): void { - let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value; - - // remove stale classes - container.className = 'value'; - // when resolving expressions we represent errors from the server as a variable with name === null. - if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable || expressionOrValue instanceof ReplEvaluationResult) && !expressionOrValue.available)) { - container.classList.add('unavailable'); - if (value !== Expression.DEFAULT_VALUE) { - container.classList.add('error'); - } - } else { - if (typeof expressionOrValue !== 'string' && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) { - // value changed color has priority over other colors. - container.className = 'value changed'; - expressionOrValue.valueChanged = false; - } - - if (options.colorize && typeof expressionOrValue !== 'string') { - if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') { - container.classList.add(expressionOrValue.type); - } else if (!isNaN(+value)) { - container.classList.add('number'); - } else if (booleanRegex.test(value)) { - container.classList.add('boolean'); - } else if (stringRegex.test(value)) { - container.classList.add('string'); - } - } - } - - if (options.maxValueLength && value && value.length > options.maxValueLength) { - value = value.substring(0, options.maxValueLength) + '...'; - } - if (!value) { - value = ''; - } - - const session = (expressionOrValue instanceof ExpressionContainer) ? expressionOrValue.getSession() : undefined; - // Only use hovers for links if thre's not going to be a hover for the value. - const hoverBehavior: DebugLinkHoverBehaviorTypeData = options.hover === false ? { type: DebugLinkHoverBehavior.Rich, store } : { type: DebugLinkHoverBehavior.None }; - if (expressionOrValue instanceof ExpressionContainer && expressionOrValue.valueLocationReference !== undefined && session && options.linkDetector) { - container.textContent = ''; - container.appendChild(options.linkDetector.linkifyLocation(value, expressionOrValue.valueLocationReference, session, hoverBehavior)); - } else if (options.linkDetector) { - container.textContent = ''; - container.appendChild(options.linkDetector.linkify(value, false, session ? session.root : undefined, true, hoverBehavior)); - } else { - container.textContent = value; - } - - if (options.hover !== false) { - const { commands = [], commandService } = options.hover || {}; - store.add(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, () => { - const container = dom.$('div'); - const markdownHoverElement = dom.$('div.hover-row'); - const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); - const hoverContentsPre = dom.append(hoverContentsElement, dom.$('pre.debug-var-hover-pre')); - hoverContentsPre.textContent = value; - container.appendChild(markdownHoverElement); - return container; - }, { - actions: commands.map(({ id, args }) => { - const description = CommandsRegistry.getCommand(id)?.metadata?.description; - return { - label: typeof description === 'string' ? description : description ? description.value : id, - commandId: id, - run: () => commandService!.executeCommand(id, ...args), - }; - }) - })); - } -} - -export function renderVariable(store: DisposableStore, commandService: ICommandService, hoverService: IHoverService, variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector, displayType?: boolean): void { - if (variable.available) { - data.type.textContent = ''; - let text = variable.name; - if (variable.value && typeof variable.name === 'string') { - if (variable.type && displayType) { - text += ': '; - data.type.textContent = variable.type + ' ='; - } else { - text += ' ='; - } - } - - data.label.set(text, highlights, variable.type && !displayType ? variable.type : variable.name); - data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual'); - data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal'); - } else if (variable.value && typeof variable.name === 'string' && variable.name) { - data.label.set(':'); - } - - data.expression.classList.toggle('lazy', !!variable.presentationHint?.lazy); - const commands = [ - { id: COPY_VALUE_ID, args: [variable, [variable]] as unknown[] } - ]; - if (variable.evaluateName) { - commands.push({ id: COPY_EVALUATE_PATH_ID, args: [{ variable }] }); - } - - renderExpressionValue(store, variable, data.value, { - showChanged, - maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - hover: { commands, commandService }, - colorize: true, - linkDetector - }, hoverService); -} - export interface IInputBoxOptions { initialValue: string; ariaLabel: string; diff --git a/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts b/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts index 6ca64c836b0..cccc8154a9f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts +++ b/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Color, RGBA } from '../../../../base/common/color.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { ansiColorIdentifiers } from '../../terminal/common/terminalColorRegistry.js'; import { ILinkDetector } from './linkDetector.js'; @@ -13,15 +12,15 @@ import { ILinkDetector } from './linkDetector.js'; * @param text The content to stylize. * @returns An {@link HTMLSpanElement} that contains the potentially stylized text. */ -export function handleANSIOutput(text: string, linkDetector: ILinkDetector, themeService: IThemeService, workspaceFolder: IWorkspaceFolder | undefined): HTMLSpanElement { +export function handleANSIOutput(text: string, linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined): HTMLSpanElement { const root: HTMLSpanElement = document.createElement('span'); const textLength: number = text.length; let styleNames: string[] = []; - let customFgColor: RGBA | undefined; - let customBgColor: RGBA | undefined; - let customUnderlineColor: RGBA | undefined; + let customFgColor: RGBA | string | undefined; + let customBgColor: RGBA | string | undefined; + let customUnderlineColor: RGBA | string | undefined; let colorsInverted: boolean = false; let currentPos: number = 0; let buffer: string = ''; @@ -116,7 +115,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, them * @param color Color to change to. If `undefined` or not provided, * will clear current color without adding a new one. */ - function changeColor(colorType: 'foreground' | 'background' | 'underline', color?: RGBA | undefined): void { + function changeColor(colorType: 'foreground' | 'background' | 'underline', color?: RGBA | string): void { if (colorType === 'foreground') { customFgColor = color; } else if (colorType === 'background') { @@ -135,7 +134,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, them * [] flag to make sure it is appropriate to turn ON or OFF (if it is already inverted don't call */ function reverseForegroundAndBackgroundColors(): void { - const oldFgColor: RGBA | undefined = customFgColor; + const oldFgColor = customFgColor; changeColor('foreground', customBgColor); changeColor('background', oldFgColor); } @@ -334,12 +333,8 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, them } else if (colorNumber >= 0 && colorNumber <= 15) { if (colorType === 'underline') { // for underline colors we just decode the 0-15 color number to theme color, set and return - const theme = themeService.getColorTheme(); const colorName = ansiColorIdentifiers[colorNumber]; - const color = theme.getColor(colorName); - if (color) { - changeColor(colorType, color.rgba); - } + changeColor(colorType, `--vscode-treminal-${colorName}`); return; } // Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107) @@ -364,7 +359,6 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, them * nothing. */ function setBasicColor(styleCode: number): void { - const theme = themeService.getColorTheme(); let colorType: 'foreground' | 'background' | undefined; let colorIndex: number | undefined; @@ -384,10 +378,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, them if (colorIndex !== undefined && colorType) { const colorName = ansiColorIdentifiers[colorIndex]; - const color = theme.getColor(colorName); - if (color) { - changeColor(colorType, color.rgba); - } + changeColor(colorType, `--vscode-${colorName.replaceAll('.', '-')}`); } } } @@ -407,9 +398,9 @@ export function appendStylizedStringToContainer( cssClasses: string[], linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined, - customTextColor?: RGBA, - customBackgroundColor?: RGBA, - customUnderlineColor?: RGBA + customTextColor?: RGBA | string, + customBackgroundColor?: RGBA | string, + customUnderlineColor?: RGBA | string, ): void { if (!root || !stringContent) { return; @@ -420,16 +411,17 @@ export function appendStylizedStringToContainer( container.className = cssClasses.join(' '); if (customTextColor) { container.style.color = - Color.Format.CSS.formatRGB(new Color(customTextColor)); + typeof customTextColor === 'string' ? `var(${customTextColor})` : Color.Format.CSS.formatRGB(new Color(customTextColor)); } if (customBackgroundColor) { container.style.backgroundColor = - Color.Format.CSS.formatRGB(new Color(customBackgroundColor)); + typeof customBackgroundColor === 'string' ? `var(${customBackgroundColor})` : Color.Format.CSS.formatRGB(new Color(customBackgroundColor)); } if (customUnderlineColor) { container.style.textDecorationColor = - Color.Format.CSS.formatRGB(new Color(customUnderlineColor)); + typeof customUnderlineColor === 'string' ? `var(${customUnderlineColor})` : Color.Format.CSS.formatRGB(new Color(customUnderlineColor)); } + root.appendChild(container); } diff --git a/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts b/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts new file mode 100644 index 00000000000..38630cd47e5 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; +import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../base/common/observable.js'; +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IDebugSession, IExpressionValue } from '../common/debug.js'; +import { Expression, ExpressionContainer, Variable } from '../common/debugModel.js'; +import { ReplEvaluationResult } from '../common/replModel.js'; +import { IVariableTemplateData } from './baseDebugView.js'; +import { handleANSIOutput } from './debugANSIHandling.js'; +import { COPY_EVALUATE_PATH_ID, COPY_VALUE_ID } from './debugCommands.js'; +import { DebugLinkHoverBehavior, DebugLinkHoverBehaviorTypeData, ILinkDetector, LinkDetector } from './linkDetector.js'; + +export interface IValueHoverOptions { + /** Commands to show in the hover footer. */ + commands?: { id: string; args: unknown[] }[]; +} + +export interface IRenderValueOptions { + showChanged?: boolean; + maxValueLength?: number; + /** If not false, a rich hover will be shown on the element. */ + hover?: false | IValueHoverOptions; + colorize?: boolean; + + /** @deprecated */ + wasANSI?: boolean; + session?: IDebugSession; + locationReference?: number; +} + +export interface IRenderVariableOptions { + showChanged?: boolean; + highlights?: IHighlight[]; +} + + +const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; +const booleanRegex = /^(true|false)$/i; +const stringRegex = /^(['"]).*\1$/; + +export class DebugExpressionRenderer { + private displayType: IObservable; + private readonly linkDetector: LinkDetector; + + constructor( + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IHoverService private readonly hoverService: IHoverService, + ) { + this.linkDetector = instantiationService.createInstance(LinkDetector); + this.displayType = observableConfigValue('debug.showVariableTypes', false, configurationService); + } + + renderVariable(data: IVariableTemplateData, variable: Variable, options: IRenderVariableOptions = {}): IDisposable { + const displayType = this.displayType.get(); + + if (variable.available) { + data.type.textContent = ''; + let text = variable.name; + if (variable.value && typeof variable.name === 'string') { + if (variable.type && displayType) { + text += ': '; + data.type.textContent = variable.type + ' ='; + } else { + text += ' ='; + } + } + + data.label.set(text, options.highlights, variable.type && !displayType ? variable.type : variable.name); + data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual'); + data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal'); + } else if (variable.value && typeof variable.name === 'string' && variable.name) { + data.label.set(':'); + } + + data.expression.classList.toggle('lazy', !!variable.presentationHint?.lazy); + const commands = [ + { id: COPY_VALUE_ID, args: [variable, [variable]] as unknown[] } + ]; + if (variable.evaluateName) { + commands.push({ id: COPY_EVALUATE_PATH_ID, args: [{ variable }] }); + } + + return this.renderValue(data.value, variable, { + showChanged: options.showChanged, + maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, + hover: { commands }, + colorize: true, + session: variable.getSession(), + }); + } + + renderValue(container: HTMLElement, expressionOrValue: IExpressionValue | string, options: IRenderValueOptions = {}): IDisposable { + const store = new DisposableStore(); + const supportsANSI = !!options.session?.capabilities.supportsANSIStyling; + + let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value; + + // remove stale classes + container.className = 'value'; + // when resolving expressions we represent errors from the server as a variable with name === null. + if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable || expressionOrValue instanceof ReplEvaluationResult) && !expressionOrValue.available)) { + container.classList.add('unavailable'); + if (value !== Expression.DEFAULT_VALUE) { + container.classList.add('error'); + } + } else { + if (typeof expressionOrValue !== 'string' && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) { + // value changed color has priority over other colors. + container.className = 'value changed'; + expressionOrValue.valueChanged = false; + } + + if (options.colorize && typeof expressionOrValue !== 'string') { + if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') { + container.classList.add(expressionOrValue.type); + } else if (!isNaN(+value)) { + container.classList.add('number'); + } else if (booleanRegex.test(value)) { + container.classList.add('boolean'); + } else if (stringRegex.test(value)) { + container.classList.add('string'); + } + } + } + + if (options.maxValueLength && value && value.length > options.maxValueLength) { + value = value.substring(0, options.maxValueLength) + '...'; + } + if (!value) { + value = ''; + } + + const session = options.session ?? ((expressionOrValue instanceof ExpressionContainer) ? expressionOrValue.getSession() : undefined); + // Only use hovers for links if thre's not going to be a hover for the value. + const hoverBehavior: DebugLinkHoverBehaviorTypeData = options.hover === false ? { type: DebugLinkHoverBehavior.Rich, store } : { type: DebugLinkHoverBehavior.None }; + dom.clearNode(container); + const locationReference = options.locationReference ?? (expressionOrValue instanceof ExpressionContainer && expressionOrValue.valueLocationReference); + + let linkDetector: ILinkDetector = this.linkDetector; + if (locationReference && session) { + linkDetector = this.linkDetector.makeReferencedLinkDetector(locationReference, session); + } + + if (supportsANSI) { + container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined)); + } else { + container.appendChild(linkDetector.linkify(value, false, session?.root, true, hoverBehavior)); + } + + if (options.hover !== false) { + const { commands = [] } = options.hover || {}; + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, () => { + const container = dom.$('div'); + const markdownHoverElement = dom.$('div.hover-row'); + const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); + const hoverContentsPre = dom.append(hoverContentsElement, dom.$('pre.debug-var-hover-pre')); + if (supportsANSI) { + // note: intentionally using `this.linkDetector` so we don't blindly linkify the + // entire contents and instead only link file paths that it contains. + hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined)); + } else { + hoverContentsPre.textContent = value; + } + container.appendChild(markdownHoverElement); + return container; + }, { + actions: commands.map(({ id, args }) => { + const description = CommandsRegistry.getCommand(id)?.metadata?.description; + return { + label: typeof description === 'string' ? description : description ? description.value : id, + commandId: id, + run: () => this.commandService.executeCommand(id, ...args), + }; + }) + })); + } + + return store; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index f0ba8e94878..0a381676af0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -29,17 +29,16 @@ import * as nls from '../../../../nls.js'; import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { asCssVariable, editorHoverBackground, editorHoverBorder, editorHoverForeground } from '../../../../platform/theme/common/colorRegistry.js'; -import { AbstractExpressionDataSource, renderExpressionValue } from './baseDebugView.js'; -import { LinkDetector } from './linkDetector.js'; -import { VariablesRenderer, VisualizedVariableRenderer, openContextMenuForVariableTreeElement } from './variablesView.js'; import { IDebugService, IDebugSession, IExpression, IExpressionContainer, IStackFrame } from '../common/debug.js'; import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js'; import { getEvaluatableExpressionAtPosition } from '../common/debugUtils.js'; +import { AbstractExpressionDataSource } from './baseDebugView.js'; +import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; +import { VariablesRenderer, VisualizedVariableRenderer, openContextMenuForVariableTreeElement } from './variablesView.js'; const $ = dom.$; @@ -102,6 +101,7 @@ export class DebugHoverWidget implements IContentWidget { private toDispose: lifecycle.IDisposable[]; private scrollbar!: DomScrollableElement; private debugHoverComputer: DebugHoverComputer; + private expressionRenderer: DebugExpressionRenderer; private expressionToRender: IExpression | undefined; private isUpdatingTree = false; @@ -117,13 +117,13 @@ export class DebugHoverWidget implements IContentWidget { @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IHoverService private readonly hoverService: IHoverService, ) { this.toDispose = []; this.showAtPosition = null; this.positionPreference = [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]; this.debugHoverComputer = this.instantiationService.createInstance(DebugHoverComputer, this.editor); + this.expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer); } private create(): void { @@ -135,10 +135,9 @@ export class DebugHoverWidget implements IContentWidget { const tip = dom.append(this.complexValueContainer, $('.tip')); tip.textContent = nls.localize({ key: 'quickTip', comment: ['"switch to editor language hover" means to show the programming language hover widget instead of the debug hover'] }, 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt'); const dataSource = this.instantiationService.createInstance(DebugHoverDataSource); - const linkeDetector = this.instantiationService.createInstance(LinkDetector); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [ - this.instantiationService.createInstance(VariablesRenderer, linkeDetector), - this.instantiationService.createInstance(VisualizedVariableRenderer, linkeDetector), + this.instantiationService.createInstance(VariablesRenderer, this.expressionRenderer), + this.instantiationService.createInstance(VisualizedVariableRenderer, this.expressionRenderer), ], dataSource, { accessibilityProvider: new DebugHoverAccessibilityProvider(), @@ -282,7 +281,7 @@ export class DebugHoverWidget implements IContentWidget { options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS }]); - return this.doShow(result.range.getStartPosition(), expression, focus, mouseEvent); + return this.doShow(session, result.range.getStartPosition(), expression, focus, mouseEvent); } private static readonly _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({ @@ -290,7 +289,7 @@ export class DebugHoverWidget implements IContentWidget { className: 'hoverHighlight' }); - private async doShow(position: Position, expression: IExpression, focus: boolean, mouseEvent: IMouseEvent | undefined): Promise { + private async doShow(session: IDebugSession | undefined, position: Position, expression: IExpression, focus: boolean, mouseEvent: IMouseEvent | undefined): Promise { if (!this.domNode) { this.create(); } @@ -302,11 +301,12 @@ export class DebugHoverWidget implements IContentWidget { if (!expression.hasChildren) { this.complexValueContainer.hidden = true; this.valueContainer.hidden = false; - renderExpressionValue(store, expression, this.valueContainer, { + store.add(this.expressionRenderer.renderValue(this.valueContainer, expression, { showChanged: false, colorize: true, hover: false, - }, this.hoverService); + session, + })); this.valueContainer.title = ''; this.editor.layoutContentWidget(this); this.scrollbar.scanDomNode(); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index b275e635fa8..55085e77ea9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -363,7 +363,8 @@ export class DebugSession implements IDebugSession, IDisposable { supportsMemoryReferences: true, //#129684 supportsArgsCanBeInterpretedByShell: true, // #149910 supportsMemoryEvent: true, // #133643 - supportsStartDebuggingRequest: true + supportsStartDebuggingRequest: true, + supportsANSIStyling: true, }); this.initialized = true; @@ -1201,7 +1202,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (event.body.group === 'start' || event.body.group === 'startCollapsed') { const expanded = event.body.group === 'start'; - this.repl.startGroup(event.body.output || '', expanded, source); + this.repl.startGroup(this, event.body.output || '', expanded, source); return; } if (event.body.group === 'end') { diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index b808f8b5a7c..2a8f7661cb2 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -59,6 +59,10 @@ margin: 0; } +.debug-var-hover-pre span { + display: inline !important; +} + /* Do not push text with inline decoration when decoration on start of line */ .monaco-editor .debug-top-stack-frame-column.start-of-line { position: absolute; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 01133271bdc..7e33db78057 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -12,6 +12,7 @@ import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from '../../../../ import { IAction } from '../../../../base/common/actions.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { memoize } from '../../../../base/common/decorators.js'; import { Emitter } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; @@ -21,7 +22,6 @@ import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI as uri } from '../../../../base/common/uri.js'; -import './media/repl.css'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorAction, registerEditorAction } from '../../../../editor/browser/editorExtensions.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; @@ -38,6 +38,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js'; import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js'; import { localize, localize2 } from '../../../../nls.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { createAndFillInContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; @@ -45,6 +46,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -56,25 +58,23 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { editorForeground, resolveColorValue } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; import { FilterViewPane, IViewPaneOptions, ViewAction } from '../../../browser/parts/views/viewPane.js'; import { IViewDescriptorService } from '../../../common/views.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; +import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; -import { FocusSessionActionViewItem } from './debugActionViewItems.js'; -import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from './debugIcons.js'; -import { LinkDetector } from './linkDetector.js'; -import { ReplFilter } from './replFilter.js'; -import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplOutputElementRenderer, ReplRawObjectsRenderer, ReplVariablesRenderer } from './replViewer.js'; import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, DEBUG_SCHEME, IDebugConfiguration, IDebugService, IDebugSession, IReplConfiguration, IReplElement, IReplOptions, REPL_VIEW_ID, State, getStateLabel } from '../common/debug.js'; import { Variable } from '../common/debugModel.js'; import { ReplEvaluationResult, ReplGroup } from '../common/replModel.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; -import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; -import { Codicon } from '../../../../base/common/codicons.js'; +import { FocusSessionActionViewItem } from './debugActionViewItems.js'; +import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; +import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from './debugIcons.js'; +import './media/repl.css'; +import { ReplFilter } from './replFilter.js'; +import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplOutputElementRenderer, ReplRawObjectsRenderer, ReplVariablesRenderer } from './replViewer.js'; const $ = dom.$; @@ -648,7 +648,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.replDelegate = new ReplDelegate(this.configurationService, this.replOptions); const wordWrap = this.configurationService.getValue('debug').console.wordWrap; this.treeContainer.classList.toggle('word-wrap', wordWrap); - const linkDetector = this.instantiationService.createInstance(LinkDetector); + const expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer); this.replDataSource = new ReplDataSource(); const tree = this.tree = >this.instantiationService.createInstance( @@ -657,12 +657,12 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.treeContainer, this.replDelegate, [ - this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector), - this.instantiationService.createInstance(ReplOutputElementRenderer, linkDetector), + this.instantiationService.createInstance(ReplVariablesRenderer, expressionRenderer), + this.instantiationService.createInstance(ReplOutputElementRenderer, expressionRenderer), new ReplEvaluationInputsRenderer(), - this.instantiationService.createInstance(ReplGroupRenderer, linkDetector), - new ReplEvaluationResultsRenderer(linkDetector, this.hoverService), - new ReplRawObjectsRenderer(linkDetector, this.hoverService), + this.instantiationService.createInstance(ReplGroupRenderer, expressionRenderer), + new ReplEvaluationResultsRenderer(expressionRenderer), + new ReplRawObjectsRenderer(expressionRenderer), ], this.replDataSource, { diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 45df05a87aa..82a76c804bb 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -17,22 +17,19 @@ import { basename } from '../../../../base/common/path.js'; import severity from '../../../../base/common/severity.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable } from './baseDebugView.js'; -import { handleANSIOutput } from './debugANSIHandling.js'; -import { debugConsoleEvaluationInput } from './debugIcons.js'; -import { ILinkDetector, LinkDetector } from './linkDetector.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpressionContainer, INestingReplElement, IReplElement, IReplElementSource, IReplOptions } from '../common/debug.js'; import { Variable } from '../common/debugModel.js'; import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, ReplOutputElement, ReplVariableElement } from '../common/replModel.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions } from './baseDebugView.js'; +import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; +import { debugConsoleEvaluationInput } from './debugIcons.js'; const $ = dom.$; @@ -43,6 +40,7 @@ interface IReplEvaluationInputTemplateData { interface IReplGroupTemplateData { label: HTMLElement; source: SourceWidget; + elementDisposable?: IDisposable; } interface IReplEvaluationResultTemplateData { @@ -57,7 +55,7 @@ interface IOutputReplElementTemplateData { value: HTMLElement; source: SourceWidget; getReplElementSource(): IReplElementSource | undefined; - elementListener: IDisposable; + elementDisposable: DisposableStore; } interface IRawObjectReplTemplateData { @@ -97,8 +95,7 @@ export class ReplGroupRenderer implements ITreeRenderer, _index: number, templateData: IReplGroupTemplateData): void { + + templateData.elementDisposable?.dispose(); const replGroup = element.element; dom.clearNode(templateData.label); - const result = handleANSIOutput(replGroup.name, this.linkDetector, this.themeService, undefined); - templateData.label.appendChild(result); + templateData.elementDisposable = this.expressionRenderer.renderValue(templateData.label, replGroup.name, { wasANSI: true, session: element.element.session }); templateData.source.setSource(replGroup.sourceData); } disposeTemplate(templateData: IReplGroupTemplateData): void { + templateData.elementDisposable?.dispose(); templateData.source.dispose(); } } @@ -135,8 +134,7 @@ export class ReplEvaluationResultsRenderer implements ITreeRenderer, index: number, templateData: IReplEvaluationResultTemplateData): void { templateData.elementStore.clear(); const expression = element.element; - renderExpressionValue(templateData.elementStore, expression, templateData.value, { + templateData.elementStore.add(this.expressionRenderer.renderValue(templateData.value, expression, { colorize: true, hover: false, - linkDetector: this.linkDetector, - }, this.hoverService); + session: element.element.getSession(), + })); } disposeTemplate(templateData: IReplEvaluationResultTemplateData): void { @@ -165,8 +163,7 @@ export class ReplOutputElementRenderer implements ITreeRenderer, index: number, templateData: IOutputReplElementTemplateData): void { + templateData.elementDisposable.clear(); this.setElementCount(element, templateData); - templateData.elementListener = element.onDidChangeCount(() => this.setElementCount(element, templateData)); + templateData.elementDisposable.add(element.onDidChangeCount(() => this.setElementCount(element, templateData))); // value dom.clearNode(templateData.value); // Reset classes to clear ansi decorations since templates are reused templateData.value.className = 'value'; const locationReference = element.expression?.valueLocationReference; - const detector: ILinkDetector = locationReference !== undefined ? this.linkDetector.makeReferencedLinkDetector(locationReference, element.session) : this.linkDetector; - templateData.value.appendChild(handleANSIOutput(element.value, detector, this.themeService, element.session.root)); + templateData.elementDisposable.add(this.expressionRenderer.renderValue(templateData.value, element.value, { wasANSI: true, session: element.session, locationReference })); templateData.value.classList.add((element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info'); templateData.source.setSource(element.sourceData); @@ -216,10 +214,11 @@ export class ReplOutputElementRenderer implements ITreeRenderer, _index: number, templateData: IOutputReplElementTemplateData): void { - templateData.elementListener.dispose(); + templateData.elementDisposable.clear(); } } @@ -232,11 +231,10 @@ export class ReplVariablesRenderer extends AbstractExpressionsRenderer>this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'VariablesView', treeContainer, new VariablesDelegate(), [ - this.instantiationService.createInstance(VariablesRenderer, linkDetector), - this.instantiationService.createInstance(VisualizedVariableRenderer, linkDetector), + this.instantiationService.createInstance(VariablesRenderer, expressionRenderer), + this.instantiationService.createInstance(VisualizedVariableRenderer, expressionRenderer), new ScopesRenderer(), new ScopeErrorRenderer(), ], @@ -434,7 +434,7 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer { } constructor( - private readonly linkDetector: LinkDetector, + private readonly expressionRenderer: DebugExpressionRenderer, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, @IHoverService hoverService: IHoverService, @@ -461,12 +461,12 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer { text += ':'; } data.label.set(text, highlights, viz.name); - renderExpressionValue(data.elementDisposable, viz, data.value, { + data.elementDisposable.add(this.expressionRenderer.renderValue(data.value, viz, { showChanged: false, maxValueLength: 1024, colorize: true, - linkDetector: this.linkDetector - }, this.hoverService); + session: expression.getSession(), + })); } protected override getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined { @@ -516,16 +516,14 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { static readonly ID = 'variable'; constructor( - private readonly linkDetector: LinkDetector, + private readonly expressionRenderer: DebugExpressionRenderer, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IDebugVisualizerService private readonly visualization: IDebugVisualizerService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @ICommandService private readonly commandService: ICommandService, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, @IHoverService hoverService: IHoverService, - @IConfigurationService private configurationService: IConfigurationService, ) { super(debugService, contextViewService, hoverService); } @@ -535,17 +533,14 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { } protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { - const showType = this.configurationService.getValue('debug').showVariableTypes; - renderVariable(data.elementDisposable, this.commandService, this.hoverService, expression as Variable, data, true, highlights, this.linkDetector, showType); + data.elementDisposable.add(this.expressionRenderer.renderVariable(data, expression as Variable, { + highlights, + showChanged: true, + })); } public override renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void { data.elementDisposable.clear(); - data.elementDisposable.add(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('debug.showVariableTypes')) { - super.renderExpressionElement(node.element, node, data); - } - })); super.renderExpressionElement(node.element, node, data); } diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index bec7f644a84..03ec10eedf1 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -30,12 +30,12 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { IViewDescriptorService } from '../../../common/views.js'; -import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderViewTree } from './baseDebugView.js'; -import { watchExpressionsAdd, watchExpressionsRemoveAll } from './debugIcons.js'; -import { LinkDetector } from './linkDetector.js'; -import { VariablesRenderer, VisualizedVariableRenderer } from './variablesView.js'; import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IExpression, WATCH_VIEW_ID } from '../common/debug.js'; import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js'; +import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js'; +import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; +import { watchExpressionsAdd, watchExpressionsRemoveAll } from './debugIcons.js'; +import { VariablesRenderer, VisualizedVariableRenderer } from './variablesView.js'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreViewUpdates = false; @@ -50,6 +50,7 @@ export class WatchExpressionsView extends ViewPane { private watchItemType: IContextKey; private variableReadonly: IContextKey; private menu: IMenu; + private expressionRenderer: DebugExpressionRenderer; constructor( options: IViewletViewOptions, @@ -78,6 +79,7 @@ export class WatchExpressionsView extends ViewPane { this.variableReadonly = CONTEXT_VARIABLE_IS_READONLY.bindTo(contextKeyService); this.watchExpressionsExist.set(this.debugService.getModel().getWatchExpressions().length > 0); this.watchItemType = CONTEXT_WATCH_ITEM_TYPE.bindTo(contextKeyService); + this.expressionRenderer = instantiationService.createInstance(DebugExpressionRenderer); } protected override renderBody(container: HTMLElement): void { @@ -87,13 +89,12 @@ export class WatchExpressionsView extends ViewPane { container.classList.add('debug-watch'); const treeContainer = renderViewTree(container); - const linkDetector = this.instantiationService.createInstance(LinkDetector); - const expressionsRenderer = this.instantiationService.createInstance(WatchExpressionsRenderer, linkDetector); + const expressionsRenderer = this.instantiationService.createInstance(WatchExpressionsRenderer, this.expressionRenderer); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'WatchExpressions', treeContainer, new WatchExpressionsDelegate(), [ expressionsRenderer, - this.instantiationService.createInstance(VariablesRenderer, linkDetector), - this.instantiationService.createInstance(VisualizedVariableRenderer, linkDetector), + this.instantiationService.createInstance(VariablesRenderer, this.expressionRenderer), + this.instantiationService.createInstance(VisualizedVariableRenderer, this.expressionRenderer), ], this.instantiationService.createInstance(WatchExpressionsDataSource), { accessibilityProvider: new WatchExpressionsAccessibilityProvider(), @@ -279,7 +280,7 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { static readonly ID = 'watchexpression'; constructor( - private readonly linkDetector: LinkDetector, + private readonly expressionRenderer: DebugExpressionRenderer, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IDebugService debugService: IDebugService, @@ -330,12 +331,12 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { } data.label.set(text, highlights, title); - renderExpressionValue(data.elementDisposable, expression, data.value, { + data.elementDisposable.add(this.expressionRenderer.renderValue(data.value, expression, { showChanged: true, maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - linkDetector: this.linkDetector, - colorize: true - }, this.hoverService); + colorize: true, + session: expression.getSession(), + })); } protected getInputBoxOptions(expression: IExpression, settingValue: boolean): IInputBoxOptions { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index cc6138857e7..b225f145056 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -164,6 +164,7 @@ export interface IExpressionValue { export interface IExpressionContainer extends ITreeElement, IExpressionValue { readonly hasChildren: boolean; + getSession(): IDebugSession | undefined; evaluateLazy(): Promise; getChildren(): Promise; readonly reference?: number; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 9ae7c7333a5..4660763365f 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -258,7 +258,7 @@ export class VisualizedExpression implements IExpression { return Promise.resolve(); } getChildren(): Promise { - return this.visualizer.getVisualizedChildren(this.treeId, this.treeItem.id); + return this.visualizer.getVisualizedChildren(this.session, this.treeId, this.treeItem.id); } getId(): string { @@ -278,12 +278,17 @@ export class VisualizedExpression implements IExpression { } constructor( + private readonly session: IDebugSession | undefined, private readonly visualizer: IDebugVisualizerService, public readonly treeId: string, public readonly treeItem: IDebugVisualizationTreeItem, public readonly original?: Variable, ) { } + public getSession(): IDebugSession | undefined { + return this.session; + } + /** Edits the value, sets the {@link errorMessage} and returns false if unsuccessful */ public async edit(newValue: string) { try { diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 09dd44e76b6..560d252edd3 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -221,7 +221,12 @@ declare module DebugProtocol { etc. */ category?: 'console' | 'important' | 'stdout' | 'stderr' | 'telemetry' | string; - /** The output to report. */ + /** The output to report. + + ANSI escape sequences may be used to inflience text color and styling if `supportsANSIStyling` is present in both the adapter's `Capabilities` and the client's `InitializeRequestArguments`. A client may strip any unrecognized ANSI sequences. + + If the `supportsANSIStyling` capabilities are not both true, then the client should display the output literally. + */ output: string; /** Support for keeping an output log organized by grouping related messages. 'start': Start a new group in expanded mode. Subsequent output events are members of the group and should be shown indented. @@ -530,6 +535,8 @@ declare module DebugProtocol { supportsArgsCanBeInterpretedByShell?: boolean; /** Client supports the `startDebugging` request. */ supportsStartDebuggingRequest?: boolean; + /** The client will interpret ANSI escape sequences in the display of `OutputEvent.output` and `Variable.value` fields when `Capabilities.supportsANSIStyling` is also enabled. */ + supportsANSIStyling?: boolean; } /** Response to `initialize` request. */ @@ -1840,6 +1847,8 @@ declare module DebugProtocol { Clients may present the first applicable mode in this array as the 'default' mode in gestures that set breakpoints. */ breakpointModes?: BreakpointMode[]; + /** The debug adapter supports ANSI escape sequences in styling of `OutputEvent.output` and `Variable.value` fields. */ + supportsANSIStyling?: boolean; } /** An `ExceptionBreakpointsFilter` is shown in the UI as an filter option for configuring how exceptions are dealt with. */ diff --git a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts index ec14f81961b..c607fbe166b 100644 --- a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts +++ b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts @@ -10,7 +10,7 @@ import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../ import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer, IDebugVisualizationTreeItem } from './debug.js'; +import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer, IDebugVisualizationTreeItem, IDebugSession } from './debug.js'; import { getContextForVariable } from './debugContext.js'; import { Scope, Variable, VisualizedExpression } from './debugModel.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; @@ -84,7 +84,7 @@ export interface IDebugVisualizerService { /** * Gets children for a visualized tree node. */ - getVisualizedChildren(treeId: string, treeElementId: number): Promise; + getVisualizedChildren(session: IDebugSession | undefined, treeId: string, treeElementId: number): Promise; /** * Gets children for a visualized tree node. @@ -202,7 +202,7 @@ export class DebugVisualizerService implements IDebugVisualizerService { return; } - return new VisualizedExpression(this, treeId, treeItem, expr); + return new VisualizedExpression(expr.getSession(), this, treeId, treeItem, expr); } catch (e) { this.logService.warn('Failed to get visualized node', e); return; @@ -210,9 +210,10 @@ export class DebugVisualizerService implements IDebugVisualizerService { } /** @inheritdoc */ - public async getVisualizedChildren(treeId: string, treeElementId: number): Promise { - const children = await this.trees.get(treeId)?.getChildren(treeElementId) || []; - return children.map(c => new VisualizedExpression(this, treeId, c, undefined)); + public async getVisualizedChildren(session: IDebugSession | undefined, treeId: string, treeElementId: number): Promise { + const node = this.trees.get(treeId); + const children = await node?.getChildren(treeElementId) || []; + return children.map(c => new VisualizedExpression(session, this, treeId, c, undefined)); } /** @inheritdoc */ diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index 5e8edf783f2..e2e388eed51 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -76,6 +76,7 @@ export class ReplVariableElement implements INestingReplElement { private readonly id = generateUuid(); constructor( + private readonly session: IDebugSession, public readonly expression: IExpression, public readonly severity: severity, public readonly sourceData?: IReplElementSource, @@ -83,6 +84,10 @@ export class ReplVariableElement implements INestingReplElement { this.hasChildren = expression.hasChildren; } + getSession() { + return this.session; + } + getChildren(): IReplElement[] | Promise { return this.expression.getChildren(); } @@ -106,6 +111,10 @@ export class RawObjectReplElement implements IExpression, INestingReplElement { return this.id; } + getSession(): IDebugSession | undefined { + return undefined; + } + get value(): string { if (this.valueObj === null) { return 'null'; @@ -193,6 +202,7 @@ export class ReplGroup implements INestingReplElement { static COUNTER = 0; constructor( + public readonly session: IDebugSession, public name: string, public autoExpand: boolean, public sourceData?: IReplElementSource @@ -291,7 +301,7 @@ export class ReplModel { // have formatted it nicely e.g. with ANSI color codes. this.addReplElement(output ? new ReplOutputElement(session, getUniqueId(), output, sev, source, expression) - : new ReplVariableElement(expression, sev, source)); + : new ReplVariableElement(session, expression, sev, source)); return; } @@ -315,8 +325,8 @@ export class ReplModel { this.addReplElement(element); } - startGroup(name: string, autoExpand: boolean, sourceData?: IReplElementSource): void { - const group = new ReplGroup(name, autoExpand, sourceData); + startGroup(session: IDebugSession, name: string, autoExpand: boolean, sourceData?: IReplElementSource): void { + const group = new ReplGroup(session, name, autoExpand, sourceData); this.addReplElement(group); } diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index 09ed6d73bed..7c66831722b 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -9,92 +9,97 @@ import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabe import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { isWindows } from '../../../../../base/common/platform.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { NullCommandService } from '../../../../../platform/commands/test/common/nullCommandService.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { renderExpressionValue, renderVariable, renderViewTree } from '../../browser/baseDebugView.js'; -import { LinkDetector } from '../../browser/linkDetector.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { renderViewTree } from '../../browser/baseDebugView.js'; +import { DebugExpressionRenderer } from '../../browser/debugExpressionRenderer.js'; import { isStatusbarInDebugMode } from '../../browser/statusbarColorProvider.js'; import { State } from '../../common/debug.js'; import { Expression, Scope, StackFrame, Thread, Variable } from '../../common/debugModel.js'; +import { MockSession } from '../common/mockDebug.js'; import { createTestSession } from './callStack.test.js'; import { createMockDebugModel } from './mockDebugModel.js'; -import { MockSession } from '../common/mockDebug.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; const $ = dom.$; -function assertVariable(session: MockSession, scope: Scope, disposables: Pick, linkDetector: LinkDetector, displayType: boolean) { - let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'); - let expression = $('.'); - let name = $('.'); - let type = $('.'); - let value = $('.'); - const label = new HighlightedLabel(name); - const lazyButton = $('.'); - const store = disposables.add(new DisposableStore()); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], undefined, displayType); - - assert.strictEqual(label.element.textContent, 'foo'); - assert.strictEqual(value.textContent, ''); - - variable.value = 'hey'; - expression = $('.'); - name = $('.'); - type = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.strictEqual(value.textContent, 'hey'); - assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); - assert.strictEqual(type.textContent, displayType ? 'string =' : ''); - - variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; - expression = $('.'); - name = $('.'); - type = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.ok(value.querySelector('a')); - assert.strictEqual(value.querySelector('a')!.textContent, variable.value); - - variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); - expression = $('.'); - name = $('.'); - type = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.strictEqual(name.className, 'virtual'); - assert.strictEqual(label.element.textContent, 'console ='); - assert.strictEqual(value.className, 'value number'); - - variable = new Variable(session, 1, scope, 2, 'xpto', 'xpto.xpto', undefined, 0, 0, undefined, {}, 'custom-type'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.strictEqual(label.element.textContent, 'xpto'); - assert.strictEqual(value.textContent, ''); - variable.value = '2'; - expression = $('.'); - name = $('.'); - type = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.strictEqual(value.textContent, '2'); - assert.strictEqual(label.element.textContent, displayType ? 'xpto: ' : 'xpto ='); - assert.strictEqual(type.textContent, displayType ? 'custom-type =' : ''); - - label.dispose(); -} suite('Debug - Base Debug View', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - let linkDetector: LinkDetector; + let renderer: DebugExpressionRenderer; + let configurationService: TestConfigurationService; + + function assertVariable(session: MockSession, scope: Scope, disposables: Pick, displayType: boolean) { + let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'); + let expression = $('.'); + let name = $('.'); + let type = $('.'); + let value = $('.'); + const label = new HighlightedLabel(name); + const lazyButton = $('.'); + const store = disposables.add(new DisposableStore()); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + + assert.strictEqual(label.element.textContent, 'foo'); + assert.strictEqual(value.textContent, ''); + + variable.value = 'hey'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.strictEqual(value.textContent, 'hey'); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); + assert.strictEqual(type.textContent, displayType ? 'string =' : ''); + + variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.ok(value.querySelector('a')); + assert.strictEqual(value.querySelector('a')!.textContent, variable.value); + + variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.strictEqual(name.className, 'virtual'); + assert.strictEqual(label.element.textContent, 'console ='); + assert.strictEqual(value.className, 'value number'); + + variable = new Variable(session, 1, scope, 2, 'xpto', 'xpto.xpto', undefined, 0, 0, undefined, {}, 'custom-type'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.strictEqual(label.element.textContent, 'xpto'); + assert.strictEqual(value.textContent, ''); + variable.value = '2'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.strictEqual(value.textContent, '2'); + assert.strictEqual(label.element.textContent, displayType ? 'xpto: ' : 'xpto ='); + assert.strictEqual(type.textContent, displayType ? 'custom-type =' : ''); + + label.dispose(); + } /** * Instantiate services for use by the functions being tested. */ setup(() => { const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); - linkDetector = instantiationService.createInstance(LinkDetector); + configurationService = instantiationService.createInstance(TestConfigurationService); + instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IHoverService, NullHoverService); + renderer = instantiationService.createInstance(DebugExpressionRenderer); }); test('render view tree', () => { @@ -110,37 +115,37 @@ suite('Debug - Base Debug View', () => { test('render expression value', () => { let container = $('.container'); const store = disposables.add(new DisposableStore()); - renderExpressionValue(store, 'render \n me', container, {}, NullHoverService); + store.add(renderer.renderValue(container, 'render \n me', {})); assert.strictEqual(container.className, 'value'); assert.strictEqual(container.textContent, 'render \n me'); const expression = new Expression('console'); expression.value = 'Object'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true })); assert.strictEqual(container.className, 'value unavailable error'); expression.available = true; expression.value = '"string value"'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true, linkDetector }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true })); assert.strictEqual(container.className, 'value string'); assert.strictEqual(container.textContent, '"string value"'); expression.type = 'boolean'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true })); assert.strictEqual(container.className, 'value boolean'); assert.strictEqual(container.textContent, expression.value); expression.value = 'this is a long string'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true, maxValueLength: 4, linkDetector }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true, maxValueLength: 4 })); assert.strictEqual(container.textContent, 'this...'); expression.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true, linkDetector }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true })); assert.ok(container.querySelector('a')); assert.strictEqual(container.querySelector('a')!.textContent, expression.value); }); @@ -157,7 +162,8 @@ suite('Debug - Base Debug View', () => { const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - assertVariable(session, scope, disposables, linkDetector, false); + configurationService.setUserConfiguration('debug.showVariableTypes', false); + assertVariable(session, scope, disposables, false); }); @@ -173,7 +179,8 @@ suite('Debug - Base Debug View', () => { const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - assertVariable(session, scope, disposables, linkDetector, true); + configurationService.setUserConfiguration('debug.showVariableTypes', true); + assertVariable(session, scope, disposables, true); }); test('statusbar in debug mode', () => { diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index 6f19be34819..eb31a388b2e 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -10,16 +10,14 @@ import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; -import { TestColorTheme, TestThemeService } from '../../../../../platform/theme/test/common/testThemeService.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { registerColors } from '../../../terminal/common/terminalColorRegistry.js'; import { appendStylizedStringToContainer, calcANSI8bitColor, handleANSIOutput } from '../../browser/debugANSIHandling.js'; import { DebugSession } from '../../browser/debugSession.js'; import { LinkDetector } from '../../browser/linkDetector.js'; import { DebugModel } from '../../common/debugModel.js'; import { createTestSession } from './callStack.test.js'; import { createMockDebugModel } from './mockDebugModel.js'; -import { ansiColorMap, registerColors } from '../../../terminal/common/terminalColorRegistry.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; suite('Debug - ANSI Handling', () => { @@ -27,7 +25,6 @@ suite('Debug - ANSI Handling', () => { let model: DebugModel; let session: DebugSession; let linkDetector: LinkDetector; - let themeService: IThemeService; /** * Instantiate services for use by the functions being tested. @@ -39,13 +36,6 @@ suite('Debug - ANSI Handling', () => { const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); linkDetector = instantiationService.createInstance(LinkDetector); - - const colors: { [id: string]: string } = {}; - for (const color in ansiColorMap) { - colors[color] = ansiColorMap[color].defaults.dark; - } - const testTheme = new TestColorTheme(colors); - themeService = new TestThemeService(testTheme); registerColors(); }); @@ -92,7 +82,7 @@ suite('Debug - ANSI Handling', () => { * @returns An {@link HTMLSpanElement} that contains the stylized text. */ function getSequenceOutput(sequence: string): HTMLSpanElement { - const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, themeService, session.root); + const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root); assert.strictEqual(1, root.children.length); const child: Node = root.lastChild!; if (isHTMLSpanElement(child)) { @@ -405,7 +395,7 @@ suite('Debug - ANSI Handling', () => { if (elementsExpected === undefined) { elementsExpected = assertions.length; } - const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, themeService, session.root); + const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root); assert.strictEqual(elementsExpected, root.children.length); for (let i = 0; i < elementsExpected; i++) { const child: Node = root.children[i]; diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index da8c447483e..7256a353905 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -201,7 +201,7 @@ suite('Debug - REPL', () => { const repl = new ReplModel(configurationService); repl.appendToRepl(session, { output: 'first global line', sev: severity.Info }); - repl.startGroup('group_1', true); + repl.startGroup(session, 'group_1', true); repl.appendToRepl(session, { output: 'first line in group', sev: severity.Info }); repl.appendToRepl(session, { output: 'second line in group', sev: severity.Info }); const elements = repl.getReplElements(); @@ -212,7 +212,7 @@ suite('Debug - REPL', () => { assert.strictEqual(group.hasChildren, true); assert.strictEqual(group.hasEnded, false); - repl.startGroup('group_2', false); + repl.startGroup(session, 'group_2', false); repl.appendToRepl(session, { output: 'first line in subgroup', sev: severity.Info }); repl.appendToRepl(session, { output: 'second line in subgroup', sev: severity.Info }); const children = group.getChildren(); diff --git a/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts b/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts index 2120d34641c..0ca1a84601a 100644 --- a/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts @@ -8,17 +8,17 @@ import * as dom from '../../../../../base/browser/dom.js'; import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { Scope, StackFrame, Thread, Variable } from '../../common/debugModel.js'; -import { MockDebugService, MockSession } from '../common/mockDebug.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js'; -import { IDebugService, IViewModel } from '../../common/debug.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { DebugExpressionRenderer } from '../../browser/debugExpressionRenderer.js'; import { VariablesRenderer } from '../../browser/variablesView.js'; -import { LinkDetector } from '../../browser/linkDetector.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IDebugService, IViewModel } from '../../common/debug.js'; +import { Scope, StackFrame, Thread, Variable } from '../../common/debugModel.js'; +import { MockDebugService, MockSession } from '../common/mockDebug.js'; const $ = dom.$; @@ -75,18 +75,22 @@ function assertVariable(disposables: Pick, variablesRend assert.strictEqual(value.textContent, 'xpto'); assert.strictEqual(type.textContent, displayType ? 'string =' : ''); assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); + + variablesRenderer.disposeTemplate(data); } suite('Debug - Variable Debug View', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let variablesRenderer: VariablesRenderer; let instantiationService: TestInstantiationService; - let linkDetector: LinkDetector; + let expressionRenderer: DebugExpressionRenderer; let configurationService: TestConfigurationService; setup(() => { instantiationService = workbenchInstantiationService(undefined, disposables); - linkDetector = instantiationService.createInstance(LinkDetector); + configurationService = instantiationService.createInstance(TestConfigurationService); + instantiationService.stub(IConfigurationService, configurationService); + expressionRenderer = instantiationService.createInstance(DebugExpressionRenderer); const debugService = new MockDebugService(); instantiationService.stub(IHoverService, NullHoverService); debugService.getViewModel = () => { focusedStackFrame: undefined, getSelectedExpression: () => undefined }; @@ -95,24 +99,16 @@ suite('Debug - Variable Debug View', () => { }); test('variable expressions with display type', () => { - configurationService = new TestConfigurationService({ - debug: { - showVariableTypes: true - } - }); + configurationService.setUserConfiguration('debug.showVariableTypes', true); instantiationService.stub(IConfigurationService, configurationService); - variablesRenderer = instantiationService.createInstance(VariablesRenderer, linkDetector); + variablesRenderer = instantiationService.createInstance(VariablesRenderer, expressionRenderer); assertVariable(disposables, variablesRenderer, true); }); test('variable expressions', () => { - configurationService = new TestConfigurationService({ - debug: { - showVariableTypes: false - } - }); + configurationService.setUserConfiguration('debug.showVariableTypes', false); instantiationService.stub(IConfigurationService, configurationService); - variablesRenderer = instantiationService.createInstance(VariablesRenderer, linkDetector); + variablesRenderer = instantiationService.createInstance(VariablesRenderer, expressionRenderer); assertVariable(disposables, variablesRenderer, false); }); }); diff --git a/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts b/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts index 3770ea6b6c5..f3e818393eb 100644 --- a/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts @@ -18,6 +18,7 @@ import { NullHoverService } from '../../../../../platform/hover/test/browser/nul import { IDebugService, IViewModel } from '../../common/debug.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { DebugExpressionRenderer } from '../../browser/debugExpressionRenderer.js'; const $ = dom.$; function assertWatchVariable(disposables: Pick, watchExpressionsRenderer: WatchExpressionsRenderer, displayType: boolean) { @@ -80,9 +81,13 @@ suite('Debug - Watch Debug View', () => { let watchExpressionsRenderer: WatchExpressionsRenderer; let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; + let expressionRenderer: DebugExpressionRenderer; setup(() => { instantiationService = workbenchInstantiationService(undefined, disposables); + configurationService = instantiationService.createInstance(TestConfigurationService); + instantiationService.stub(IConfigurationService, configurationService); + expressionRenderer = instantiationService.createInstance(DebugExpressionRenderer); const debugService = new MockDebugService(); instantiationService.stub(IHoverService, NullHoverService); debugService.getViewModel = () => { focusedStackFrame: undefined, getSelectedExpression: () => undefined }; @@ -91,24 +96,16 @@ suite('Debug - Watch Debug View', () => { }); test('watch expressions with display type', () => { - configurationService = new TestConfigurationService({ - debug: { - showVariableTypes: true - } - }); + configurationService.setUserConfiguration('debug', { showVariableTypes: true }); instantiationService.stub(IConfigurationService, configurationService); - watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer, null as any); + watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer, expressionRenderer); assertWatchVariable(disposables, watchExpressionsRenderer, true); }); test('watch expressions', () => { - configurationService = new TestConfigurationService({ - debug: { - showVariableTypes: false - } - }); + configurationService.setUserConfiguration('debug', { showVariableTypes: false }); instantiationService.stub(IConfigurationService, configurationService); - watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer, null as any); + watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer, expressionRenderer); assertWatchVariable(disposables, watchExpressionsRenderer, false); }); }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts index 80bb7aaa87c..9daccd6d5ba 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts @@ -10,9 +10,9 @@ import { ITreeNode, ITreeRenderer } from '../../../../../../base/browser/ui/tree import { FuzzyScore } from '../../../../../../base/common/filters.js'; import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../../nls.js'; -import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchObjectTree } from '../../../../../../platform/list/browser/listService.js'; -import { renderExpressionValue } from '../../../../debug/browser/baseDebugView.js'; +import { DebugExpressionRenderer } from '../../../../debug/browser/debugExpressionRenderer.js'; import { INotebookVariableElement } from './notebookVariablesDataSource.js'; const $ = dom.$; @@ -40,15 +40,16 @@ export interface IVariableTemplateData { export class NotebookVariableRenderer implements ITreeRenderer { + private expressionRenderer: DebugExpressionRenderer; + static readonly ID = 'variableElement'; get templateId(): string { return NotebookVariableRenderer.ID; } - constructor( - @IHoverService private readonly _hoverService: IHoverService - ) { + constructor(@IInstantiationService instantiationService: IInstantiationService) { + this.expressionRenderer = instantiationService.createInstance(DebugExpressionRenderer); } renderTemplate(container: HTMLElement): IVariableTemplateData { @@ -66,10 +67,11 @@ export class NotebookVariableRenderer implements ITreeRenderer, index: number, templateData: IVariableTemplateData, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts index bd70b53a444..253488522d5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts @@ -88,7 +88,7 @@ export class NotebookVariablesView extends ViewPane { 'notebookVariablesTree', container, new NotebookVariablesDelegate(), - [new NotebookVariableRenderer(this.hoverService)], + [this.instantiationService.createInstance(NotebookVariableRenderer)], this.dataSource, { accessibilityProvider: new NotebookVariableAccessibilityProvider(), From 7a483feef359cd237d75fb6f570eb6a429bbefd0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:35:46 -0700 Subject: [PATCH 236/286] Don't add ViewLines to parts --- src/vs/editor/browser/view.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 5113e56c22f..460871eb304 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -162,7 +162,6 @@ export class View extends ViewEventHandler { // View Lines this._viewLines = new ViewLines(this._context, this._linesContent); - this._viewParts.push(this._viewLines); if (this._viewGpuContext) { this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext); } From f49e4a15cef337bb35998ef64636729c212d9488 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:36:46 -0700 Subject: [PATCH 237/286] Remove unneeded instantiation service --- src/vs/editor/browser/view.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 460871eb304..ab059a2b47d 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -178,7 +178,7 @@ export class View extends ViewEventHandler { const scrollDecoration = new ScrollDecorationViewPart(this._context); this._viewParts.push(scrollDecoration); - const contentViewOverlays = this._instantiationService.createInstance(ContentViewOverlays, this._context); + const contentViewOverlays = new ContentViewOverlays(this._context); this._viewParts.push(contentViewOverlays); contentViewOverlays.addDynamicOverlay(new CurrentLineHighlightOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new SelectionsOverlay(this._context)); @@ -186,7 +186,7 @@ export class View extends ViewEventHandler { contentViewOverlays.addDynamicOverlay(new DecorationsOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new WhitespaceOverlay(this._context)); - const marginViewOverlays = this._instantiationService.createInstance(MarginViewOverlays, this._context); + const marginViewOverlays = new MarginViewOverlays(this._context); this._viewParts.push(marginViewOverlays); marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context)); From d1b1801acdb26cd06cce4f43906a99a4bc9cfc27 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:38:38 -0700 Subject: [PATCH 238/286] Use CharCode over plain numbers --- src/vs/editor/browser/gpu/atlas/textureAtlas.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 67eaa2d8a4c..47583ab791b 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { CharCode } from '../../../../base/common/charCode.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { TwoKeyMap } from '../../../../base/common/map.js'; @@ -126,7 +127,7 @@ export class TextureAtlas extends Disposable { const taskQueue = this._warmUpTask.value = new IdleTaskQueue(); // Warm up using roughly the larger glyphs first to help optimize atlas allocation // A-Z - for (let code = 65; code <= 90; code++) { + for (let code = CharCode.A; code <= CharCode.Z; code++) { taskQueue.enqueue(() => { for (const fgColor of this._colorMap.keys()) { this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); @@ -134,7 +135,7 @@ export class TextureAtlas extends Disposable { }); } // a-z - for (let code = 97; code <= 122; code++) { + for (let code = CharCode.a; code <= CharCode.z; code++) { taskQueue.enqueue(() => { for (const fgColor of this._colorMap.keys()) { this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); @@ -142,7 +143,7 @@ export class TextureAtlas extends Disposable { }); } // Remaining ascii - for (let code = 33; code <= 126; code++) { + for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) { taskQueue.enqueue(() => { for (const fgColor of this._colorMap.keys()) { this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); From 3933f08810906fe69a92eadb613b53b1041f281b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:39:39 -0700 Subject: [PATCH 239/286] Use readonly for allocator props --- .../browser/gpu/atlas/textureAtlasShelfAllocator.ts | 2 +- .../browser/gpu/atlas/textureAtlasSlabAllocator.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts index 82b7777fac4..f120601e990 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts @@ -22,7 +22,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { }; /** A set of all glyphs allocated, this is only tracked to enable debug related functionality */ - private _allocatedGlyphs: Set> = new Set(); + private readonly _allocatedGlyphs: Set> = new Set(); private _nextIndex = 0; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index cfc8b231695..0f2aaacfafe 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -27,16 +27,16 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private readonly _ctx: OffscreenCanvasRenderingContext2D; - private _slabs: ITextureAtlasSlab[] = []; - private _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); + private readonly _slabs: ITextureAtlasSlab[] = []; + private readonly _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); - private _unusedRects: ITextureAtlasSlabUnusedRect[] = []; + private readonly _unusedRects: ITextureAtlasSlabUnusedRect[] = []; - private _openRegionsByHeight: Map = new Map(); - private _openRegionsByWidth: Map = new Map(); + private readonly _openRegionsByHeight: Map = new Map(); + private readonly _openRegionsByWidth: Map = new Map(); /** A set of all glyphs allocated, this is only tracked to enable debug related functionality */ - private _allocatedGlyphs: Set> = new Set(); + private readonly _allocatedGlyphs: Set> = new Set(); private _slabW: number; private _slabH: number; From f575f09be76d35b514ed2c5d2dbee192f35ef4f2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:40:25 -0700 Subject: [PATCH 240/286] Use BugIndicatingError for glyph too large error --- src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts | 3 ++- src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts | 3 ++- .../test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts index f120601e990..1bbf920997f 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BugIndicatingError } from '../../../../base/common/errors.js'; import { ensureNonNullable } from '../gpuUtils.js'; import type { IRasterizedGlyph } from '../raster/raster.js'; import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js'; @@ -40,7 +41,7 @@ export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) { - throw new Error('Glyph is too large for the atlas page'); + throw new BugIndicatingError('Glyph is too large for the atlas page'); } // Finalize and increment row if it doesn't fix horizontally diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index 0f2aaacfafe..b41fed6978c 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; import { TwoKeyMap } from '../../../../base/common/map.js'; import { ensureNonNullable } from '../gpuUtils.js'; import type { IRasterizedGlyph } from '../raster/raster.js'; @@ -72,7 +73,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { // The glyph does not fit into the atlas page, glyphs should never be this large in practice if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) { - throw new Error('Glyph is too large for the atlas page'); + throw new BugIndicatingError('Glyph is too large for the atlas page'); } // The glyph does not fit into a slab diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts index 49c51836d54..92d354a5f78 100644 --- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -11,6 +11,7 @@ import { TextureAtlasShelfAllocator } from '../../../../../browser/gpu/atlas/tex import { TextureAtlasSlabAllocator, type TextureAtlasSlabAllocatorOptions } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { assertIsValidGlyph } from './testUtil.js'; +import { BugIndicatingError } from '../../../../../../base/common/errors.js'; const blackArr = [0x00, 0x00, 0x00, 0xFF]; @@ -74,7 +75,7 @@ suite('TextureAtlasAllocator', () => { // Skipping because it fails unexpectedly on web only when asserting the error message test.skip(`(${name}) glyph too large for canvas`, () => { const { allocator } = initAllocator(1, 1); - throws(() => allocateAndAssert(allocator, pixel2x1, undefined), new Error('Glyph is too large for the atlas page')); + throws(() => allocateAndAssert(allocator, pixel2x1, undefined), new BugIndicatingError('Glyph is too large for the atlas page')); }); } }); From dca69fec396edf2fbcab70719b49d0d59e86b1f1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:42:05 -0700 Subject: [PATCH 241/286] Throw when drawing 0 objects This will help us find out if/when this actually happens in practice. --- .../editor/browser/gpu/fullFileRenderStrategy.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index c05ae43b66f..563770d98bc 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { getActiveWindow } from '../../../base/browser/dom.js'; +import { BugIndicatingError } from '../../../base/common/errors.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; @@ -358,14 +359,13 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { if (this._visibleObjectCount <= 0) { - console.error('Attempt to draw 0 objects'); - } else { - pass.draw( - quadVertices.length / 2, - this._visibleObjectCount, - undefined, - (viewportData.startLineNumber - 1) * FullFileRenderStrategy._columnCount - ); + throw new BugIndicatingError('Attempt to draw 0 objects'); } + pass.draw( + quadVertices.length / 2, + this._visibleObjectCount, + undefined, + (viewportData.startLineNumber - 1) * FullFileRenderStrategy._columnCount + ); } } From 15bb8be73e7c5854a2e2dafe0a52069a85ff413d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:42:53 -0700 Subject: [PATCH 242/286] Throw when resize observer can't observe --- src/vs/editor/browser/gpu/gpuUtils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/editor/browser/gpu/gpuUtils.ts b/src/vs/editor/browser/gpu/gpuUtils.ts index b7035cbb920..6ced420acc2 100644 --- a/src/vs/editor/browser/gpu/gpuUtils.ts +++ b/src/vs/editor/browser/gpu/gpuUtils.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BugIndicatingError } from '../../../base/common/errors.js'; import { toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; export const quadVertices = new Float32Array([ @@ -52,6 +53,7 @@ export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: } catch { observer.disconnect(); observer = undefined; + throw new BugIndicatingError('Could not observe device pixel dimensions'); } return toDisposable(() => observer?.disconnect()); } From cc2f6981088d74568641c2fb9a3b4805295818c4 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Tue, 10 Sep 2024 11:55:33 -0700 Subject: [PATCH 243/286] Fix empty username in chat (#228142) There's a race where the chat participant is registered but hasn't set the username data yet (this requires a request to github). So, changing to always fall back to the persisted username. I think this isn't 100% complete and I'll follow up as debt. I think we need to rerender the list on ChatAgentService.onDidChangeAgents (thought we already did this but I guess not?) or I think this is a good place to start using observables. --- .../workbench/contrib/chat/common/chatModel.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 49535f0cc9c..30100ee1940 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -809,28 +809,24 @@ export class ChatModel extends Disposable implements IChatModel { } get requesterUsername(): string { - return (this._defaultAgent ? - this._defaultAgent.metadata.requester?.name : - this.initialData?.requesterUsername) ?? ''; + return this._defaultAgent?.metadata.requester?.name ?? + this.initialData?.requesterUsername ?? ''; } get responderUsername(): string { - return (this._defaultAgent ? - this._defaultAgent.fullName : - this.initialData?.responderUsername) ?? ''; + return this._defaultAgent?.fullName ?? + this.initialData?.responderUsername ?? ''; } private readonly _initialRequesterAvatarIconUri: URI | undefined; get requesterAvatarIconUri(): URI | undefined { - return this._defaultAgent ? - this._defaultAgent.metadata.requester?.icon : + return this._defaultAgent?.metadata.requester?.icon ?? this._initialRequesterAvatarIconUri; } private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined; get responderAvatarIcon(): ThemeIcon | URI | undefined { - return this._defaultAgent ? - this._defaultAgent?.metadata.themeIcon : + return this._defaultAgent?.metadata.themeIcon ?? this._initialResponderAvatarIconUri; } From 8f39b2bcb90a7abb3792ed3a705c61c77b6a329d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:22:25 -0700 Subject: [PATCH 244/286] Document descriptions of some view parts Part of #227087 --- .../viewParts/currentLineHighlight/currentLineHighlight.ts | 6 ++++++ .../browser/viewParts/editorScrollbar/editorScrollbar.ts | 4 ++++ .../editor/browser/viewParts/indentGuides/indentGuides.ts | 4 ++++ src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts | 3 +++ src/vs/editor/browser/viewParts/lines/viewLines.ts | 4 ++++ src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts | 3 +++ src/vs/editor/browser/viewParts/minimap/minimap.ts | 4 ++++ .../editor/browser/viewParts/overviewRuler/overviewRuler.ts | 4 ++++ src/vs/editor/browser/viewParts/rulers/rulers.ts | 4 ++++ src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts | 4 ++++ src/vs/editor/browser/viewParts/viewZones/viewZones.ts | 5 +++++ src/vs/editor/browser/viewParts/whitespace/whitespace.ts | 4 ++++ 12 files changed, 49 insertions(+) diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 6e99b4c1f90..8d627025769 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -201,6 +201,9 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { protected abstract _renderOne(ctx: RenderingContext, exact: boolean): string; } +/** + * Emphasizes the current line by drawing a border around it. + */ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { @@ -215,6 +218,9 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { } } +/** + * Emphasizes the current line margin/gutter by drawing a border around it. + */ export class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-margin' : '') + (this._shouldRenderOther() ? ' current-line-margin-both' : '') + (this._shouldRenderInMargin() && exact ? ' current-line-exact-margin' : ''); diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 459ad1f12b1..0e0f9413ab0 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -16,6 +16,10 @@ import { getThemeTypeSelector } from '../../../../platform/theme/common/themeSer import { EditorOption } from '../../../common/config/editorOptions.js'; import { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js'; +/** + * The editor scrollbar built on VS Code's scrollable element that sits beside + * the minimap. + */ export class EditorScrollbar extends ViewPart { private readonly scrollbar: SmoothScrollableElement; diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 6d936b92c3b..a140386e6ca 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -18,6 +18,10 @@ import { isDefined } from '../../../../base/common/types.js'; import { BracketPairGuidesClassNames } from '../../../common/model/guidesTextModelPart.js'; import { IndentGuide, HorizontalGuidesState } from '../../../common/textModelGuides.js'; +/** + * Indent guides are vertical lines that help identify the indentation level of + * the code. + */ export class IndentGuidesOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts index 9321db6339b..ee6461fd7f8 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts @@ -15,6 +15,9 @@ import * as viewEvents from '../../../common/viewEvents.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; import { editorDimmedLineNumber, editorLineNumbers } from '../../../common/core/editorColorRegistry.js'; +/** + * Renders line numbers to the left of the main view lines content. + */ export class LineNumbersOverlay extends DynamicViewOverlay { public static readonly CLASS_NAME = 'line-numbers'; diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 134bebd5cf3..b93a0028b0b 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -88,6 +88,10 @@ class HorizontalRevealSelectionsRequest { type HorizontalRevealRequest = HorizontalRevealRangeRequest | HorizontalRevealSelectionsRequest; +/** + * The view lines part is responsible for rendering the actual content of a + * file. + */ export class ViewLines extends ViewPart implements IViewLines { /** * Adds this amount of pixels to the right of lines (no-one wants to type near the edge of the viewport) diff --git a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts index dc58a1592d9..60c42904ed2 100644 --- a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts @@ -31,6 +31,9 @@ const enum GlyphStorageBufferInfo { Offset_OriginPosition = 4, } +/** + * The GPU implementation of the ViewLines part. + */ export class ViewLinesGpu extends ViewPart { private readonly canvas: HTMLCanvasElement; diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index d6aa690bc3b..a54d780d996 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -803,6 +803,10 @@ class MinimapSamplingState { } } +/** + * The minimap appears beside the editor scroll bar and visualizes a zoomed out + * view of the file. + */ export class Minimap extends ViewPart implements IMinimapModel { public readonly tokensColorTracker: MinimapTokensColorTracker; diff --git a/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts index 2643979cb97..11292eb56a1 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts @@ -11,6 +11,10 @@ import { ViewContext } from '../../../common/viewModel/viewContext.js'; import * as viewEvents from '../../../common/viewEvents.js'; import { ViewEventHandler } from '../../../common/viewEventHandler.js'; +/** + * The overview ruler appears underneath the editor scroll bar and shows things + * like the cursor, various decorations, etc. + */ export class OverviewRuler extends ViewEventHandler implements IOverviewRuler { private readonly _context: ViewContext; diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.ts b/src/vs/editor/browser/viewParts/rulers/rulers.ts index eb03b1a9ea9..c0a46927d17 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.ts +++ b/src/vs/editor/browser/viewParts/rulers/rulers.ts @@ -11,6 +11,10 @@ import { ViewContext } from '../../../common/viewModel/viewContext.js'; import * as viewEvents from '../../../common/viewEvents.js'; import { EditorOption, IRulerOption } from '../../../common/config/editorOptions.js'; +/** + * Rulers are vertical lines that appear at certain columns in the editor. There can be >= 0 rulers + * at a time. + */ export class Rulers extends ViewPart { public domNode: FastDomNode; diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index b5ac061aab5..6f16ed5253b 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -23,6 +23,10 @@ import { isHighContrast } from '../../../../platform/theme/common/theme.js'; import { CursorChangeReason } from '../../../common/cursorEvents.js'; import { WindowIntervalTimer, getWindow } from '../../../../base/browser/dom.js'; +/** + * View cursors is a view part responsible for rendering the primary cursor and + * any secondary cursors that are currently active. + */ export class ViewCursors extends ViewPart { static readonly BLINK_INTERVAL = 500; diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 4bf3ee4029b..4b94e5660d3 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -32,6 +32,11 @@ interface IComputedViewZoneProps { const invalidFunc = () => { throw new Error(`Invalid change accessor`); }; +/** + * A view zone is a rectangle that is a section that is inserted into the editor + * lines that can be used for various purposes such as showing a diffs, peeking + * an implementation, etc. + */ export class ViewZones extends ViewPart { private _zones: { [id: string]: IMyViewZone }; diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 1fcdff94ed4..56cc4692dc0 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -18,6 +18,10 @@ import { LineRange } from '../../../common/viewLayout/viewLineRenderer.js'; import { Position } from '../../../common/core/position.js'; import { editorWhitespaces } from '../../../common/core/editorColorRegistry.js'; +/** + * The whitespace overlay will visual certain whitespace depending on the + * current editor configuration (boundary, selection, etc.). + */ export class WhitespaceOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; From ff8664319686c9b143ded6d0eabb9f1bf53cd566 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:43:46 +0200 Subject: [PATCH 245/286] Git - clean-up history provider (#228148) --- extensions/git/src/historyProvider.ts | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 02e3da449d8..5db5b90d532 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -65,7 +65,6 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private _HEAD: Branch | undefined; private historyItemRefs: SourceControlHistoryItemRef[] = []; - private historyItemBaseRef: SourceControlHistoryItemRef | undefined; private historyItemDecorations = new Map(); @@ -95,10 +94,19 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec historyItemRefId = `refs/heads/${this.repository.HEAD.name}`; historyItemRefName = this.repository.HEAD.name; - // Merge base if the branch has changed + // Remote + this._currentHistoryItemRemoteRef = this.repository.HEAD.upstream ? { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + revision: this.repository.HEAD.upstream.commit, + icon: new ThemeIcon('cloud') + } : undefined; + + // Base - compute only if the branch has changed if (this._HEAD?.name !== this.repository.HEAD.name) { const mergeBase = await this.resolveHEADMergeBase(); - this.historyItemBaseRef = mergeBase && + + this._currentHistoryItemBaseRef = mergeBase && (mergeBase.remote !== this.repository.HEAD.upstream?.remote || mergeBase.name !== this.repository.HEAD.upstream?.name) ? { id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, @@ -110,7 +118,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Detached commit historyItemRefId = this.repository.HEAD.commit ?? ''; historyItemRefName = this.repository.HEAD.commit ?? ''; - this.historyItemBaseRef = undefined; + + this._currentHistoryItemRemoteRef = undefined; + this._currentHistoryItemBaseRef = undefined; } break; } @@ -118,7 +128,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec // Tag historyItemRefId = `refs/tags/${this.repository.HEAD.name}`; historyItemRefName = this.repository.HEAD.name ?? this.repository.HEAD.commit ?? ''; - this.historyItemBaseRef = undefined; + + this._currentHistoryItemRemoteRef = undefined; + this._currentHistoryItemBaseRef = undefined; break; } } @@ -132,15 +144,6 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec icon: new ThemeIcon('target'), }; - this._currentHistoryItemRemoteRef = this.repository.HEAD.upstream ? { - id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - revision: this.repository.HEAD.upstream.commit, - icon: new ThemeIcon('cloud') - } : undefined; - - this._currentHistoryItemBaseRef = this.historyItemBaseRef; - this._onDidChangeCurrentHistoryItemRefs.fire(); this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemRef: ${JSON.stringify(this._currentHistoryItemRef)}`); this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemRemoteRef: ${JSON.stringify(this._currentHistoryItemRemoteRef)}`); From 2413c2b20075b5de00b96575fb34077dd59086cc Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 10 Sep 2024 12:46:14 -0700 Subject: [PATCH 246/286] rearrange content in comment accessible view, provide relevant editor range (#228112) fix #226087 --- .../contrib/comments/browser/commentsView.ts | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 5db9aebcd4a..f0a12bb0f02 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -37,6 +37,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { AccessibleViewAction } from '../../accessibility/browser/accessibleViewActions.js'; import type { ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; import { IPathService } from '../../../services/path/common/pathService.js'; +import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; export const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey('commentsView.hasComments', false); export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey('commentsView.someCommentsExpanded', false); @@ -149,7 +150,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { @IHoverService hoverService: IHoverService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IStorageService storageService: IStorageService, - @IPathService private readonly pathService: IPathService + @IPathService private readonly pathService: IPathService, ) { const stateMemento = new Memento(VIEW_STORAGE_ID, storageService); const viewState = stateMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -343,32 +344,43 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } const replyCount = this.getReplyCountAsString(element, forAriaLabel); const replies = this.getRepliesAsString(element, forAriaLabel); + const editor = this.editorService.findEditors(element.resource); + const codeEditor = this.editorService.activeEditorPane?.getControl(); + let content; + if (element.range && editor?.length && isCodeEditor(codeEditor)) { + content = codeEditor.getModel()?.getValueInRange(element.range); + if (content) { + content = '\nCorresponding code: \n' + content; + } + } if (element.range) { if (element.threadRelevance === CommentThreadApplicability.Outdated) { return accessibleViewHint + nls.localize('resourceWithCommentLabelOutdated', - "Outdated from {0} at line {1} column {2} in {3},{4} comment: {5}", - element.comment.userName, - element.range.startLineNumber, - element.range.startColumn, - basename(element.resource), - replyCount, - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ) + replies; - } else { - return accessibleViewHint + nls.localize('resourceWithCommentLabel', - "{0} at line {1} column {2} in {3},{4} comment: {5}", + "Outdated from {0} at line {1} column {2} in {3}{4}\nComment: {5}{6}", element.comment.userName, element.range.startLineNumber, element.range.startColumn, basename(element.resource), replyCount, (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value, + content, + ) + replies; + } else { + return accessibleViewHint + nls.localize('resourceWithCommentLabel', + "{0} at line {1} column {2} in {3} {4}\nComment: {5}{6}", + element.comment.userName, + element.range.startLineNumber, + element.range.startColumn, + basename(element.resource), + replyCount, + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value, + content, ) + replies; } } else { if (element.threadRelevance === CommentThreadApplicability.Outdated) { return accessibleViewHint + nls.localize('resourceWithCommentLabelFileOutdated', - "Outdated from {0} in {1},{2} comment: {3}", + "Outdated from {0} in {1} {2}\nComment: {3}{4}{5}", element.comment.userName, basename(element.resource), replyCount, @@ -376,11 +388,12 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { ) + replies; } else { return accessibleViewHint + nls.localize('resourceWithCommentLabelFile', - "{0} in {1},{2} comment: {3}", + "{0} in {1} {2}\nComment: {3}{4}", element.comment.userName, basename(element.resource), replyCount, - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value, + content ) + replies; } } From ea12329f3e2d4b35759a621b3123f1106016b7e8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:06:32 -0700 Subject: [PATCH 247/286] Remove only --- .../test/browser/terminalSuggestAddon.integrationTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts index c5c983f05ac..119993e65cd 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts @@ -63,7 +63,7 @@ interface IRecordedSessionResizeEvent { rows: number; } -suite.only('Terminal Contrib Suggest Recordings', () => { +suite('Terminal Contrib Suggest Recordings', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let xterm: Terminal; From e5fe1de0d0d5587cbe6df5b995fa3acc99294ea3 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Tue, 10 Sep 2024 13:36:55 -0700 Subject: [PATCH 248/286] Update distro --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b413d73e784..5b3d18c55dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.94.0", - "distro": "fcaeb73de7ac6ff11a3e732c63987e01b88341e7", + "distro": "94e8178dbd2d80f72a25452eff94052eb9fbcf68", "author": { "name": "Microsoft Corporation" }, From 10ff6e993436e7917ce65469bc9d495277b97191 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 10 Sep 2024 14:03:55 -0700 Subject: [PATCH 249/286] Add header ids in interactive playground Fixes #228013 One of the only other places that uses marked directly for rendering --- .../browser/markdownDocumentRenderer.ts | 64 +------------------ .../browser/markdownSettingRenderer.ts | 18 +++--- .../browser/markedGfmHeadingIdPlugin.ts | 63 ++++++++++++++++++ .../browser/walkThroughInput.ts | 26 ++++---- 4 files changed, 88 insertions(+), 83 deletions(-) create mode 100644 src/vs/workbench/contrib/markdown/browser/markedGfmHeadingIdPlugin.ts diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index e55f0410798..6b90a6520dc 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -13,6 +13,7 @@ import { escape } from '../../../../base/common/strings.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { tokenizeToString } from '../../../../editor/common/languages/textToHtmlTokenizer.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { markedGfmHeadingIdPlugin } from './markedGfmHeadingIdPlugin.js'; export const DEFAULT_MARKDOWN_STYLES = ` body { @@ -218,7 +219,7 @@ export async function renderMarkdownDocument( return tokenizeToString(languageService, code, languageId); } }), - MarkedGfmHeadings.gfmHeadingId(), + markedGfmHeadingIdPlugin(), ...(options?.markedExtensions ?? []), ); @@ -315,64 +316,3 @@ namespace MarkedHighlight { return html; } } - -namespace MarkedGfmHeadings { - - // Copied from https://github.com/Flet/github-slugger since we can't use esm yet. - // eslint-disable-next-line no-control-regex, no-misleading-character-class - const githubSlugReplaceRegex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g; - - function slugify(heading: string): string { - const slugifiedHeading = heading.trim() - .toLowerCase() - .replace(githubSlugReplaceRegex, '') - .replace(/\s/g, '-'); // Replace whitespace with - - - return slugifiedHeading; - } - - // Copied from https://github.com/markedjs/marked-gfm-heading-id/blob/main/src/index.js - // Removed logic for handling duplicate header ids for now - - // unescape from marked helpers - const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; - function unescape(html: string) { - // explicitly match decimal, hex, and named HTML entities - return html.replace(unescapeTest, (_, n) => { - n = n.toLowerCase(); - if (n === 'colon') { return ':'; } - if (n.charAt(0) === '#') { - return n.charAt(1) === 'x' - ? String.fromCharCode(parseInt(n.substring(2), 16)) - : String.fromCharCode(+n.substring(1)); - } - return ''; - }); - } - - export function gfmHeadingId({ prefix = '', globalSlugs = false } = {}): marked.MarkedExtension { - return { - // hooks: { - // preprocess(src: string) { - // if (!globalSlugs) { - // resetHeadings(); - // } - // return src; - // }, - // }, - renderer: { - heading({ tokens, depth }) { - const text = this.parser.parseInline(tokens); - const raw = unescape(this.parser.parseInline(tokens, this.parser.textRenderer)) - .trim() - .replace(/<[!\/a-z].*?>/gi, ''); - const level = depth; - const id = `${prefix}${slugify(raw)}`; - // const heading = { level, text, id, raw }; - // headings.push(heading); - return `${text}\n`; - }, - }, - }; - } -} diff --git a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts index 9403958b8f0..456dff11557 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from '../../../../nls.js'; -import { IPreferencesService, ISetting } from '../../../services/preferences/common/preferences.js'; -import { settingKeyToDisplayFormat } from '../../preferences/browser/settingsTreeModels.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../base/common/actions.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import type { Tokens } from '../../../../base/common/marked/marked.js'; import { Schemas } from '../../../../base/common/network.js'; -import { Tokens } from '../../../../base/common/marked/marked.js'; +import { URI } from '../../../../base/common/uri.js'; +import * as nls from '../../../../nls.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IPreferencesService, ISetting } from '../../../services/preferences/common/preferences.js'; +import { settingKeyToDisplayFormat } from '../../preferences/browser/settingsTreeModels.js'; export class SimpleSettingRenderer { private readonly codeSettingRegex: RegExp; diff --git a/src/vs/workbench/contrib/markdown/browser/markedGfmHeadingIdPlugin.ts b/src/vs/workbench/contrib/markdown/browser/markedGfmHeadingIdPlugin.ts new file mode 100644 index 00000000000..39ed0d95270 --- /dev/null +++ b/src/vs/workbench/contrib/markdown/browser/markedGfmHeadingIdPlugin.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as marked from '../../../../base/common/marked/marked.js'; + +// Copied from https://github.com/Flet/github-slugger since we can't use esm yet. +// eslint-disable-next-line no-control-regex, no-misleading-character-class +const githubSlugReplaceRegex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g; + +function slugify(heading: string): string { + const slugifiedHeading = heading.trim() + .toLowerCase() + .replace(githubSlugReplaceRegex, '') + .replace(/\s/g, '-'); // Replace whitespace with - + + return slugifiedHeading; +} + +// Copied from https://github.com/markedjs/marked-gfm-heading-id/blob/main/src/index.js +// Removed logic for handling duplicate header ids for now + +// unescape from marked helpers +const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; +function unescape(html: string) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(unescapeTest, (_, n) => { + n = n.toLowerCase(); + if (n === 'colon') { return ':'; } + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} + +export function markedGfmHeadingIdPlugin({ prefix = '', globalSlugs = false } = {}): marked.MarkedExtension { + return { + // hooks: { + // preprocess(src: string) { + // if (!globalSlugs) { + // resetHeadings(); + // } + // return src; + // }, + // }, + renderer: { + heading({ tokens, depth }) { + const text = this.parser.parseInline(tokens); + const raw = unescape(this.parser.parseInline(tokens, this.parser.textRenderer)) + .trim() + .replace(/<[!\/a-z].*?>/gi, ''); + const level = depth; + const id = `${prefix}${slugify(raw)}`; + // const heading = { level, text, id, raw }; + // headings.push(heading); + return `${text}\n`; + }, + }, + }; +} diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughInput.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughInput.ts index 02562eadc8b..2e09781b796 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughInput.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Dimension } from '../../../../base/browser/dom.js'; +import { DisposableStore, IReference } from '../../../../base/common/lifecycle.js'; +import * as marked from '../../../../base/common/marked/marked.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ITextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { EditorInputCapabilities, IUntypedEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { EditorModel } from '../../../common/editor/editorModel.js'; -import { URI } from '../../../../base/common/uri.js'; -import { DisposableStore, IReference } from '../../../../base/common/lifecycle.js'; -import { ITextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { marked, Tokens } from '../../../../base/common/marked/marked.js'; -import { isEqual } from '../../../../base/common/resources.js'; +import { markedGfmHeadingIdPlugin } from '../../markdown/browser/markedGfmHeadingIdPlugin.js'; import { moduleToContent } from '../common/walkThroughContentProvider.js'; -import { Dimension } from '../../../../base/browser/dom.js'; -import { EditorInputCapabilities, IUntypedEditorInput } from '../../../common/editor.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Schemas } from '../../../../base/common/network.js'; class WalkThroughModel extends EditorModel { @@ -115,15 +116,16 @@ export class WalkThroughInput extends EditorInput { const snippets: Promise>[] = []; let i = 0; - const renderer = new marked.Renderer(); - renderer.code = ({ lang }: Tokens.Code) => { + const renderer = new marked.marked.Renderer(); + renderer.code = ({ lang }: marked.Tokens.Code) => { i++; const resource = this.options.resource.with({ scheme: Schemas.walkThroughSnippet, fragment: `${i}.${lang}` }); snippets.push(this.textModelResolverService.createModelReference(resource)); return `
`; }; - content = marked(content, { async: false, renderer }); + const m = new marked.Marked({ renderer }, markedGfmHeadingIdPlugin()); + content = m.parse(content, { async: false }); return Promise.all(snippets) .then(refs => new WalkThroughModel(content, refs)); }); From fa3b9ad0c33cdf655edffec19796937f4d04d8ca Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 10 Sep 2024 14:15:37 -0700 Subject: [PATCH 250/286] add accessibility getting started entry to help menu (#228160) --- .../workbench/browser/actions/helpActions.ts | 26 +++++++++++++++++++ .../common/gettingStartedContent.ts | 7 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/actions/helpActions.ts b/src/vs/workbench/browser/actions/helpActions.ts index df5d8f99d3d..158dfb1d7de 100644 --- a/src/vs/workbench/browser/actions/helpActions.ts +++ b/src/vs/workbench/browser/actions/helpActions.ts @@ -15,6 +15,8 @@ import { IProductService } from '../../../platform/product/common/productService import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { Categories } from '../../../platform/action/common/actionCommonCategories.js'; +import { ICommandService } from '../../../platform/commands/common/commands.js'; +import { IAccessibilityService } from '../../../platform/accessibility/common/accessibility.js'; class KeybindingsReferenceAction extends Action2 { @@ -307,6 +309,28 @@ class OpenPrivacyStatementUrlAction extends Action2 { } } +class GetStartedWithAccessibilityFeatures extends Action2 { + static readonly ID = 'workbench.action.getStartedWithAccessibilityFeatures'; + constructor() { + super({ + id: GetStartedWithAccessibilityFeatures.ID, + title: localize2('getStartedWithAccessibilityFeatures', 'Get Started with Accessibility Features'), + category: Categories.Help, + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '1_welcome', + order: 6 + } + }); + } + run(accessor: ServicesAccessor): void { + const commandService = accessor.get(ICommandService); + const accessibilityService = accessor.get(IAccessibilityService); + commandService.executeCommand('workbench.action.openWalkthrough', accessibilityService.isScreenReaderOptimized() ? 'SetupScreenReader' : 'SetupAccessibility'); + } +} + // --- Actions Registration if (KeybindingsReferenceAction.AVAILABLE) { @@ -344,3 +368,5 @@ if (OpenLicenseUrlAction.AVAILABLE) { if (OpenPrivacyStatementUrlAction.AVAILABE) { registerAction2(OpenPrivacyStatementUrlAction); } + +registerAction2(GetStartedWithAccessibilityFeatures); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 485e749acf3..e06b5285451 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -184,7 +184,6 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ 'onSettingChanged:workbench.colorTheme', 'onCommand:workbench.action.selectTheme' ], - when: '!accessibilityModeEnabled', media: { type: 'markdown', path: 'theme_picker', } }, { @@ -425,6 +424,12 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ media: { type: 'markdown', path: 'empty' } + }, + { + id: 'accessibilitySettings', + title: localize('gettingStarted.accessibilitySettings.title', "Configure accessibility settings"), + description: localize('gettingStarted.accessibilitySettings.description.interpolated', "Accessibility settings can be configured by running the Open Accessibility Settings command.\n{0}", Button(localize('openAccessibilitySettings', "Open Accessibility Settings"), 'command:workbench.action.openAccessibilitySettings')), + media: { type: 'markdown', path: 'empty' } } ] } From e7ca87c15d163c13b6a21a81b4a40bcb76620003 Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 10 Sep 2024 14:16:42 -0700 Subject: [PATCH 251/286] feat: associate URIs with markdown code blocks for codemapper consumption (#228145) --- .../workbench/api/common/extHost.api.impl.ts | 1 + .../api/common/extHostChatAgents2.ts | 8 +++ .../api/common/extHostTypeConverters.ts | 16 ++++- src/vs/workbench/api/common/extHostTypes.ts | 7 ++ .../browser/actions/chatCodeblockActions.ts | 70 ++++++++++--------- src/vs/workbench/contrib/chat/browser/chat.ts | 1 + .../chatMarkdownContentPart.ts | 12 +++- .../contrib/chat/browser/codeBlockPart.ts | 6 +- .../contrib/chat/common/annotations.ts | 16 +++++ .../contrib/chat/common/chatModel.ts | 12 ++-- .../contrib/chat/common/chatService.ts | 6 ++ .../chat/common/codeBlockModelCollection.ts | 33 ++++++--- ...ode.proposed.chatParticipantAdditions.d.ts | 6 ++ 13 files changed, 143 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e82d2034bfd..a885606999e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1751,6 +1751,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, ChatResponseReferencePart2: extHostTypes.ChatResponseReferencePart, ChatResponseCodeCitationPart: extHostTypes.ChatResponseCodeCitationPart, + ChatResponseCodeblockUriPart: extHostTypes.ChatResponseCodeblockUriPart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 5940c351d10..27ca9a8bbcc 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -121,6 +121,14 @@ class ChatAgentResponseStream { _report(dto); return this; }, + codeblockUri(value) { + throwIfDone(this.codeblockUri); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + const part = new extHostTypes.ChatResponseCodeblockUriPart(value); + const dto = typeConvert.ChatResponseCodeblockUriPart.from(part); + _report(dto); + return this; + }, filetree(value, baseUri) { throwIfDone(this.filetree); const part = new extHostTypes.ChatResponseFileTreePart(value, baseUri); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1abbe45add0..2f411ced17e 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestVariableEntry } from '../../contrib/chat/common/chatModel.js'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; @@ -2405,6 +2405,18 @@ export namespace ChatResponseMarkdownPart { } } +export namespace ChatResponseCodeblockUriPart { + export function from(part: vscode.ChatResponseCodeblockUriPart): Dto { + return { + kind: 'codeblockUri', + uri: part.value, + }; + } + export function to(part: Dto): vscode.ChatResponseCodeblockUriPart { + return new types.ChatResponseCodeblockUriPart(URI.revive(part.uri)); + } +} + export namespace ChatResponseMarkdownWithVulnerabilitiesPart { export function from(part: vscode.ChatResponseMarkdownWithVulnerabilitiesPart): Dto { return { @@ -2673,6 +2685,8 @@ export namespace ChatResponsePart { return ChatResponseTextEditPart.from(part); } else if (part instanceof types.ChatResponseMarkdownWithVulnerabilitiesPart) { return ChatResponseMarkdownWithVulnerabilitiesPart.from(part); + } else if (part instanceof types.ChatResponseCodeblockUriPart) { + return ChatResponseCodeblockUriPart.from(part); } else if (part instanceof types.ChatResponseDetectedParticipantPart) { return ChatResponseDetectedParticipantPart.from(part); } else if (part instanceof types.ChatResponseWarningPart) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 58e0404d8ce..ada4e5e5156 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4477,6 +4477,13 @@ export class ChatResponseReferencePart { } } +export class ChatResponseCodeblockUriPart { + value: vscode.Uri; + constructor(value: vscode.Uri) { + this.value = value; + } +} + export class ChatResponseCodeCitationPart { value: vscode.Uri; license: string; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index fd64f264501..1c06c235045 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -3,10 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from '../../../../../base/common/arrays.js'; +import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { CharCode } from '../../../../../base/common/charCode.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; import { isEqual } from '../../../../../base/common/resources.js'; +import * as strings from '../../../../../base/common/strings.js'; +import { URI } from '../../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; @@ -28,26 +34,20 @@ import { INotificationService, Severity } from '../../../../../platform/notifica import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { CHAT_CATEGORY } from './chatActions.js'; -import { IChatWidgetService, IChatCodeBlockContextProviderService } from '../chat.js'; -import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../codeBlockPart.js'; -import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_EDIT_APPLIED } from '../../common/chatContextKeys.js'; -import { ChatCopyKind, IChatContentReference, IChatService, IDocumentContext } from '../../common/chatService.js'; -import { IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; -import * as strings from '../../../../../base/common/strings.js'; -import { CharCode } from '../../../../../base/common/charCode.js'; -import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; -import { coalesce } from '../../../../../base/common/arrays.js'; -import { AsyncIterableObject } from '../../../../../base/common/async.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; -import { URI } from '../../../../../base/common/uri.js'; +import { CONTEXT_CHAT_EDIT_APPLIED, CONTEXT_CHAT_ENABLED, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from '../../common/chatContextKeys.js'; +import { ChatCopyKind, IChatContentReference, IChatService, IDocumentContext } from '../../common/chatService.js'; +import { IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { IChatCodeBlockContextProviderService, IChatWidgetService } from '../chat.js'; +import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../codeBlockPart.js'; +import { CHAT_CATEGORY } from './chatActions.js'; const shellLangIds = [ 'fish', @@ -158,14 +158,26 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { const editorService = accessor.get(IEditorService); const textFileService = accessor.get(ITextFileService); + const bulkEditService = accessor.get(IBulkEditService); + const codeEditorService = accessor.get(ICodeEditorService); + const chatService = accessor.get(IChatService); + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const notificationService = accessor.get(INotificationService); + const progressService = accessor.get(IProgressService); + const languageService = accessor.get(ILanguageService); if (isResponseFiltered(context)) { // When run from command palette return; } + if (context.codemapperUri) { + // If the code block is from a code mapper, first reveal the target file + await editorService.openEditor({ resource: context.codemapperUri }); + } + if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); + return this.handleNotebookEditor(languageService, progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, editorService.activeEditorPane.getControl() as INotebookEditor, context); } let activeEditorControl = editorService.activeTextEditorControl; @@ -188,10 +200,10 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { return; } - await this.handleTextEditor(accessor, activeEditorControl, context); + await this.handleTextEditor(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, activeEditorControl, context); } - private async handleNotebookEditor(accessor: ServicesAccessor, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { + private async handleNotebookEditor(languageService: ILanguageService, progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { if (!notebookEditor.hasModel()) { return; } @@ -203,20 +215,17 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { if (notebookEditor.activeCodeEditor?.hasTextFocus()) { const codeEditor = notebookEditor.activeCodeEditor; if (codeEditor.hasModel()) { - return this.handleTextEditor(accessor, codeEditor, context); + return this.handleTextEditor(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, codeEditor, context); } } - const languageService = accessor.get(ILanguageService); - const chatService = accessor.get(IChatService); - const focusRange = notebookEditor.getFocus(); const next = Math.max(focusRange.end - 1, 0); insertCell(languageService, notebookEditor, next, CellKind.Code, 'below', context.code, true); this.notifyUserAction(chatService, context); } - protected async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + protected async computeEdits(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { const activeModel = codeEditor.getModel(); const range = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); const text = reindent(codeBlockActionContext.code, activeModel, range.startLineNumber); @@ -230,12 +239,8 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { return false; } - private async handleTextEditor(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext) { - const bulkEditService = accessor.get(IBulkEditService); - const codeEditorService = accessor.get(ICodeEditorService); - const chatService = accessor.get(IChatService); - - const result = await this.computeEdits(accessor, codeEditor, codeBlockActionContext); + private async handleTextEditor(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext) { + const result = await this.computeEdits(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, codeEditor, codeBlockActionContext); this.notifyUserAction(chatService, codeBlockActionContext, result); if (!result) { return; @@ -489,14 +494,12 @@ export function registerChatCodeBlockActions() { }); } - protected override async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + protected override async computeEdits(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { - const progressService = accessor.get(IProgressService); - const notificationService = accessor.get(INotificationService); const activeModel = codeEditor.getModel(); - const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); + const mappedEditsProviders = languageFeaturesService.mappedEditsProvider.ordered(activeModel); if (mappedEditsProviders.length > 0) { // 0th sub-array - editor selections array if there are any selections @@ -819,6 +822,7 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): codeBlockIndex: codeBlockInfo.codeBlockIndex, code: editor.getValue(), languageId: editor.getModel()!.getLanguageId(), + codemapperUri: codeBlockInfo.codemapperUri }; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index fc3af5c6ee6..6273a5e0310 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -80,6 +80,7 @@ export interface IChatCodeBlockInfo { codeBlockIndex: number; element: ChatTreeItem; uri: URI | undefined; + codemapperUri: URI | undefined; focus(): void; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index e6184f510ae..da42f3e51d3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -25,6 +25,7 @@ import { IMarkdownVulnerability } from '../../common/annotations.js'; import { IChatProgressRenderableResponseContent } from '../../common/chatModel.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js'; +import { URI } from '../../../../../base/common/uri.js'; const $ = dom.$; @@ -66,6 +67,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP let textModel: Promise; let range: Range | undefined; let vulns: readonly IMarkdownVulnerability[] | undefined; + let codemapperUri: URI | undefined; if (equalsIgnoreCase(languageId, localFileLanguageId)) { try { const parsedBody = parseLocalFileData(text); @@ -83,11 +85,12 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); vulns = modelEntry.vulns; + codemapperUri = modelEntry.codemapperUri; textModel = modelEntry.model; } const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns }, text, currentWidth, rendererOptions.editableCodeBlock); + const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns, codemapperUri }, text, currentWidth, rendererOptions.editableCodeBlock); this.allRefs.push(ref); // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) @@ -100,7 +103,8 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP focus() { ref.object.focus(); }, - uri: ref.object.uri + uri: ref.object.uri, + codemapperUri: undefined }; this.codeblocks.push(info); orderedDisposablesList.push(ref); @@ -119,7 +123,9 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const ref = this.editorPool.get(); const editorInfo = ref.object; if (isResponseVM(data.element)) { - this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); + this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }).then((e) => { + this.codeblocks[data.codeBlockIndex] = { ...this.codeblocks[data.codeBlockIndex]!, codemapperUri: e.codemapperUri }; + }); } editorInfo.render(data, currentWidth, editableCodeBlock); diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index b83e09cd7d4..9fdb65576e6 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -79,6 +79,8 @@ export interface ICodeBlockData { readonly textModel: Promise; readonly languageId: string; + readonly codemapperUri?: URI; + readonly vulns?: readonly IMarkdownVulnerability[]; readonly range?: Range; @@ -126,6 +128,7 @@ export function parseLocalFileData(text: string) { export interface ICodeBlockActionContext { code: string; + codemapperUri?: URI; languageId?: string; codeBlockIndex: number; element: unknown; @@ -424,7 +427,8 @@ export class CodeBlockPart extends Disposable { code: textModel.getTextBuffer().getValueInRange(data.range ?? textModel.getFullModelRange(), EndOfLinePreference.TextDefined), codeBlockIndex: data.codeBlockIndex, element: data.element, - languageId: textModel.getLanguageId() + languageId: textModel.getLanguageId(), + codemapperUri: data.codemapperUri, } satisfies ICodeBlockActionContext; this.resourceContextKey.set(textModel.uri); } diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index 5d0fd7db153..bd95ecb599e 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -61,6 +61,12 @@ export function annotateSpecialMarkdownContent(response: ReadonlyArray${item.uri.toString()}`; + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + result[result.length - 1] = { content: merged, kind: 'markdownContent' }; + } } else { result.push(item); } @@ -99,6 +105,16 @@ export function annotateVulnerabilitiesInText(response: ReadonlyArray(.*?)<\/vscode_codeblock_uri>/ms.exec(text); + if (match && match[1]) { + const result = URI.parse(match[1]); + const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + match[0].length); + return { uri: result, textWithoutResult }; + } + return undefined; +} + export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } { const vulnerabilities: IMarkdownVulnerability[] = []; let newText = text; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 30100ee1940..515156496d7 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; export interface IChatRequestVariableEntry { @@ -74,6 +74,7 @@ export interface IChatTextEditGroup { export type IChatProgressResponseContent = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability + | IChatResponseCodeblockUriPart | IChatTreeData | IChatContentInlineReference | IChatProgressMessage @@ -83,7 +84,7 @@ export type IChatProgressResponseContent = | IChatTextEditGroup | IChatConfirmation; -export type IChatProgressRenderableResponseContent = Exclude; +export type IChatProgressRenderableResponseContent = Exclude; export interface IResponse { readonly value: ReadonlyArray; @@ -203,7 +204,7 @@ export class Response extends Disposable implements IResponse { return this._responseParts; } - constructor(value: IMarkdownString | ReadonlyArray) { + constructor(value: IMarkdownString | ReadonlyArray) { super(); this._responseParts = asArray(value).map((v) => (isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent : @@ -301,7 +302,7 @@ export class Response extends Disposable implements IResponse { return part.command.title; } else if (part.kind === 'textEditGroup') { return localize('editsSummary', "Made changes."); - } else if (part.kind === 'progressMessage') { + } else if (part.kind === 'progressMessage' || part.kind === 'codeblockUri') { return ''; } else if (part.kind === 'confirmation') { return `${part.title}\n${part.message}`; @@ -422,7 +423,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } constructor( - _response: IMarkdownString | ReadonlyArray, + _response: IMarkdownString | ReadonlyArray, private _session: ChatModel, private _agent: IChatAgentData | undefined, private _slashCommand: IChatAgentCommand | undefined, @@ -1047,6 +1048,7 @@ export class ChatModel extends Disposable implements IChatModel { if (progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || + progress.kind === 'codeblockUri' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command' || diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 99edf364e6f..25804510aca 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -154,6 +154,11 @@ export interface IChatAgentVulnerabilityDetails { description: string; } +export interface IChatResponseCodeblockUriPart { + kind: 'codeblockUri'; + uri: URI; +} + export interface IChatAgentMarkdownContentWithVulnerability { content: IMarkdownString; vulnerabilities: IChatAgentVulnerabilityDetails[]; @@ -202,6 +207,7 @@ export type IChatProgress = | IChatWarningMessage | IChatTextEdit | IChatMoveMessage + | IChatResponseCodeblockUriPart | IChatConfirmation; export interface IChatFollowup { diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index 92a93ae015a..ad67a6ccc24 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -11,8 +11,8 @@ import { Range } from '../../../../editor/common/core/range.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from './chatViewModel.js'; -import { extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; export class CodeBlockModelCollection extends Disposable { @@ -20,6 +20,7 @@ export class CodeBlockModelCollection extends Disposable { private readonly _models = new ResourceMap<{ readonly model: Promise>; vulns: readonly IMarkdownVulnerability[]; + codemapperUri?: URI; }>(); /** @@ -41,16 +42,16 @@ export class CodeBlockModelCollection extends Disposable { this.clear(); } - get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } | undefined { + get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[]; readonly codemapperUri?: URI } | undefined { const uri = this.getUri(sessionId, chat, codeBlockIndex); const entry = this._models.get(uri); if (!entry) { return; } - return { model: entry.model.then(ref => ref.object), vulns: entry.vulns }; + return { model: entry.model.then(ref => ref.object), vulns: entry.vulns, codemapperUri: entry.codemapperUri }; } - getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } { + getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[]; readonly codemapperUri?: URI } { const existing = this.get(sessionId, chat, codeBlockIndex); if (existing) { return existing; @@ -58,7 +59,7 @@ export class CodeBlockModelCollection extends Disposable { const uri = this.getUri(sessionId, chat, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); - this._models.set(uri, { model: ref, vulns: [] }); + this._models.set(uri, { model: ref, vulns: [], codemapperUri: undefined }); while (this._models.size > this.maxModelCount) { const first = Array.from(this._models.keys()).at(0); @@ -68,7 +69,7 @@ export class CodeBlockModelCollection extends Disposable { this.delete(first); } - return { model: ref.then(ref => ref.object), vulns: [] }; + return { model: ref.then(ref => ref.object), vulns: [], codemapperUri: undefined }; } private delete(codeBlockUri: URI) { @@ -90,9 +91,15 @@ export class CodeBlockModelCollection extends Disposable { const entry = this.getOrCreate(sessionId, chat, codeBlockIndex); const extractedVulns = extractVulnerabilitiesFromText(content.text); - const newText = fixCodeText(extractedVulns.newText, content.languageId); + let newText = fixCodeText(extractedVulns.newText, content.languageId); this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities); + const codeblockUri = extractCodeblockUrisFromText(newText); + if (codeblockUri) { + this.setCodemapperUri(sessionId, chat, codeBlockIndex, codeblockUri.uri); + newText = codeblockUri.textWithoutResult; + } + const textModel = (await entry.model).textEditorModel; if (content.languageId) { const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(content.languageId); @@ -103,7 +110,7 @@ export class CodeBlockModelCollection extends Disposable { const currentText = textModel.getValue(EndOfLinePreference.LF); if (newText === currentText) { - return; + return entry; } if (newText.startsWith(currentText)) { @@ -115,6 +122,16 @@ export class CodeBlockModelCollection extends Disposable { // console.log(`Failed to optimize setText`); textModel.setValue(newText); } + + return entry; + } + + private setCodemapperUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, codemapperUri: URI) { + const uri = this.getUri(sessionId, chat, codeBlockIndex); + const entry = this._models.get(uri); + if (entry) { + entry.codemapperUri = codemapperUri; + } } private setVulns(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, vulnerabilities: IMarkdownVulnerability[]) { diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 9352503656d..ab40805bb9b 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -36,6 +36,11 @@ declare module 'vscode' { constructor(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]); } + export class ChatResponseCodeblockUriPart { + value: Uri; + constructor(value: Uri); + } + /** * Displays a {@link Command command} as a button in the chat response. */ @@ -155,6 +160,7 @@ declare module 'vscode' { textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; + codeblockUri(uri: Uri): void; detectedParticipant(participant: string, command?: ChatCommand): void; push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): void; From 647abeeea3f14e661e4b4eb94131009e4ecbfb75 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 10 Sep 2024 14:52:36 -0700 Subject: [PATCH 252/286] fix comment accessibility bug (#227998) --- .../workbench/contrib/comments/browser/commentThreadWidget.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 0ea52c03bf5..84e2e387458 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -176,7 +176,7 @@ export class CommentThreadWidget extends } if (keybinding) { ariaLabel = localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding); - } else { + } else if (verbose) { ariaLabel = localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel); } this._body.container.ariaLabel = ariaLabel; From 6e2c9f77e8dd978667200a036d647f8a7d22a9bb Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 10 Sep 2024 15:11:17 -0700 Subject: [PATCH 253/286] Don't strip command uris in walkthrough Fixes #228013 --- src/vs/base/browser/dom.ts | 4 ++-- .../contrib/markdown/browser/markdownDocumentRenderer.ts | 2 +- .../browser/editor/vs_code_editor_walkthrough.ts | 2 +- .../contrib/welcomeWalkthrough/browser/walkThroughPart.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 1b937448c24..e502b62da26 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1951,10 +1951,10 @@ const defaultDomPurifyConfig = Object.freeze { From 0485695683bb9e4a5577423fd03fd7151443f0dd Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 11 Sep 2024 00:40:22 +0200 Subject: [PATCH 254/286] Git - fix remote reference id (#228164) * Git - fix remote reference id * Remove unnecessary code --- extensions/git/src/historyProvider.ts | 2 +- src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 5db5b90d532..8beaeef0132 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -16,7 +16,7 @@ function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { switch (ref.type) { case RefType.RemoteHead: return { - id: `refs/remotes/${ref.remote}/${ref.name}`, + id: `refs/remotes/${ref.name}`, name: ref.name ?? '', description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined, revision: ref.commit, diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 17283375763..0e7648eaf9a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -52,7 +52,6 @@ import { ContextKeys } from './scmViewPane.js'; import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IDropdownMenuActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { Event } from '../../../../base/common/event.js'; import { Iterable } from '../../../../base/common/iterator.js'; @@ -115,7 +114,6 @@ class SCMHistoryItemRefsActionViewItem extends ActionViewItem { name.textContent = localize('auto', "Auto"); } else if (this._historyItemsFilter.length === 1) { name.textContent = this._historyItemsFilter[0].name; - reset(this.label, ...renderLabelWithIcons(`$(git-branch) ${this._historyItemsFilter[0].name}`)); } else { name.textContent = localize('items', "{0} Items", this._historyItemsFilter.length); } From 9b777cc0263ef8ce74c2066374dee35b3fa6c1b6 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 10 Sep 2024 19:32:48 -0700 Subject: [PATCH 255/286] fix accessible view bug (#228022) --- src/vs/base/browser/markdownRenderer.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 5a9c703ecd9..38a54691094 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -545,9 +545,8 @@ export function renderMarkdownAsPlaintext(markdown: IMarkdownString, withCodeBlo value = `${value.substr(0, 100_000)}…`; } - const html = marked.parse(value, { async: false, renderer: withCodeBlocks ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value }).replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m); - - return sanitizeRenderedMarkdown({ isTrusted: false }, html).toString(); + const html = marked.parse(value, { async: false, renderer: withCodeBlocks ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value }); + return sanitizeRenderedMarkdown({ isTrusted: false }, html).toString().replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m); } const unescapeInfo = new Map([ From 6e9d8a281003211220a6732ec538a28c483ff78b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 11 Sep 2024 06:42:24 +0200 Subject: [PATCH 256/286] Workers fail when single-quote in application dir (#227971) (#228122) * Workers fail when single-quote in application dir (#227971) * Fix script-src content security policy in webWorkerExtensionHostIframe --- src/vs/base/browser/defaultWorkerFactory.ts | 9 ++++++--- .../worker/webWorkerExtensionHostIframe.esm.html | 12 ++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/vs/base/browser/defaultWorkerFactory.ts b/src/vs/base/browser/defaultWorkerFactory.ts index 6add6ba6288..8cf67fd4a2e 100644 --- a/src/vs/base/browser/defaultWorkerFactory.ts +++ b/src/vs/base/browser/defaultWorkerFactory.ts @@ -93,15 +93,18 @@ function getWorkerBootstrapUrl(label: string, workerScriptUrl: string, workerBas } } + // In below blob code, we are using JSON.stringify to ensure the passed + // in values are not breaking our script. The values may contain string + // terminating characters (such as ' or "). const blob = new Blob([coalesce([ `/*${label}*/`, - workerBaseUrl ? `globalThis.MonacoEnvironment = { baseUrl: '${workerBaseUrl}' };` : undefined, + workerBaseUrl ? `globalThis.MonacoEnvironment = { baseUrl: ${JSON.stringify(workerBaseUrl)};` : undefined, `globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(getNLSMessages())};`, `globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(getNLSLanguage())};`, - `globalThis._VSCODE_FILE_ROOT = '${globalThis._VSCODE_FILE_ROOT}';`, + `globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(globalThis._VSCODE_FILE_ROOT)};`, `const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });`, `globalThis.workerttPolicy = ttPolicy;`, - isESM ? `await import(ttPolicy?.createScriptURL('${workerScriptUrl}') ?? '${workerScriptUrl}');` : `importScripts(ttPolicy?.createScriptURL('${workerScriptUrl}') ?? '${workerScriptUrl}');`, // + isESM ? `await import(ttPolicy?.createScriptURL(${JSON.stringify(workerScriptUrl)}) ?? ${JSON.stringify(workerScriptUrl)});` : `importScripts(ttPolicy?.createScriptURL(${JSON.stringify(workerScriptUrl)}) ?? ${JSON.stringify(workerScriptUrl)});`, isESM ? `globalThis.postMessage({ type: 'vscode-worker-ready' });` : undefined, // in ESM signal we are ready after the async import `/*${label}*/` ]).join('')], { type: 'application/javascript' }); diff --git a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html index bdceb3fe7ed..53bc54bb5aa 100644 --- a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html +++ b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html @@ -4,7 +4,7 @@ @@ -100,13 +100,17 @@ const isESM = true; // ESM-uncomment-end + // In below blob code, we are using JSON.stringify to ensure the passed + // in values are not breaking our script. The values may contain string + // terminating characters (such as ' or "). + const blob = new Blob([[ `/*extensionHostWorker*/`, - `globalThis.MonacoEnvironment = { baseUrl: '${baseUrl}' };`, + `globalThis.MonacoEnvironment = { baseUrl: ${JSON.stringify(baseUrl)} };`, `globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(nlsMessages)};`, `globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(nlsLanguage)};`, - `globalThis._VSCODE_FILE_ROOT = '${fileRoot}';`, - isESM ? `await import('${workerUrl}');` : `importScripts('${workerUrl}');`, + `globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(fileRoot)};`, + isESM ? `await import(${JSON.stringify(workerUrl)});` : `importScripts(${JSON.stringify(workerUrl)});`, `/*extensionHostWorker*/` ].join('')], { type: 'application/javascript' }); From c8d5f8c0f945b530e5e0afd2af5e18c207f59fb0 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 11 Sep 2024 17:26:34 +1000 Subject: [PATCH 257/286] Ignore cell metadata height when computing height in nb diff view (#228193) * Collapse unchanged lines in input/metadata editors of NB Diff view * Ignore Cell Metadata height when computing height in nb diff * oops --- .../contrib/notebook/browser/diff/diffElementViewModel.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index 9e82e57bee0..b9a9ae6ecc9 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -193,6 +193,10 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB return this.configurationService.getValue('notebook.diff.ignoreOutputs') || !!(this.mainDocumentTextModel?.transientOptions.transientOutputs); } + private get ignoreMetadata() { + return this.configurationService.getValue('notebook.diff.ignoreMetadata'); + } + private _sourceEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; private _outputEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; @@ -226,10 +230,10 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB editorMargin: 0, metadataHeight: 0, cellStatusHeight, - metadataStatusHeight: 25, + metadataStatusHeight: this.ignoreMetadata ? 0 : 25, rawOutputHeight: 0, outputTotalHeight: 0, - outputStatusHeight: 25, + outputStatusHeight: this.ignoreOutputs ? 0 : 25, outputMetadataHeight: 0, bodyMargin: 32, totalHeight: 82 + cellStatusHeight + editorHeight, From d62804977ba09345d8f6c7a8aceec08c8a40973d Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Wed, 11 Sep 2024 10:41:18 +0200 Subject: [PATCH 258/286] Fixes #227466 by disabling hot reload (#228204) --- src/vs/base/common/hotReload.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/hotReload.ts b/src/vs/base/common/hotReload.ts index fbf76480587..80901b470f0 100644 --- a/src/vs/base/common/hotReload.ts +++ b/src/vs/base/common/hotReload.ts @@ -6,8 +6,12 @@ import { IDisposable } from './lifecycle.js'; import { env } from './process.js'; +function hotReloadDisabled() { + return true; // TODO@hediet fix hot reload. +} + export function isHotReloadEnabled(): boolean { - return env && !!env['VSCODE_DEV']; + return !hotReloadDisabled() && env && !!env['VSCODE_DEV']; } export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable { if (!isHotReloadEnabled()) { From 1906fb40a58eebfe55f304d6a2103409ea8a0cb4 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:05:47 +0200 Subject: [PATCH 259/286] =?UTF-8?q?SCM=20Graph=20-=20=F0=9F=92=84=20polish?= =?UTF-8?q?=20node=20rendering=20(#228205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contrib/scm/browser/media/scm.css | 12 ++++ .../contrib/scm/browser/scmHistory.ts | 56 +++++++++++-------- .../contrib/scm/browser/scmHistoryViewPane.ts | 3 +- .../workbench/contrib/scm/common/history.ts | 1 + 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 2306e5e0c50..e422ba15e60 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -139,10 +139,22 @@ height: 22px; } +.scm-view .monaco-list-row .history-item > .graph-container.current > .graph > circle:last-child { + fill: var(--vscode-sideBar-background); +} + +.scm-view .monaco-list-row:hover .history-item > .graph-container.current > .graph > circle:last-child { + fill: var(--vscode-list-hoverBackground); +} + .scm-view .monaco-list-row .history-item > .graph-container > .graph > circle { stroke: var(--vscode-sideBar-background); } +.scm-view .monaco-list-row:hover .history-item > .graph-container > .graph > circle { + stroke: var(--vscode-list-hoverBackground); +} + .scm-view .monaco-list-row .history-item > .label-container { display: flex; flex-shrink: 0; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index 0343b271fc5..f87712b51c3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -5,18 +5,18 @@ import { localize } from '../../../../nls.js'; import { deepClone } from '../../../../base/common/objects.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; import { buttonForeground, foreground } from '../../../../platform/theme/common/colorRegistry.js'; import { chartsBlue, chartsGreen, chartsOrange, chartsPurple, chartsRed, chartsYellow } from '../../../../platform/theme/common/colors/chartsColors.js'; import { asCssVariable, ColorIdentifier, registerColor, transparent } from '../../../../platform/theme/common/colorUtils.js'; -import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from '../common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel } from '../common/history.js'; import { rot } from '../../../../base/common/numbers.js'; import { svgElem } from '../../../../base/browser/dom.js'; export const SWIMLANE_HEIGHT = 22; export const SWIMLANE_WIDTH = 11; -const CIRCLE_RADIUS = 4; const SWIMLANE_CURVE_RADIUS = 5; +const CIRCLE_RADIUS = 4; +const CIRCLE_STROKE_WIDTH = 2; /** * History item reference colors (local, remote, base) @@ -64,12 +64,16 @@ function createPath(colorIdentifier: string): SVGPathElement { return path; } -function drawCircle(index: number, radius: number, colorIdentifier: string): SVGCircleElement { +function drawCircle(index: number, radius: number, strokeWidth: number, colorIdentifier?: string): SVGCircleElement { const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', `${SWIMLANE_WIDTH * (index + 1)}`); circle.setAttribute('cy', `${SWIMLANE_WIDTH}`); circle.setAttribute('r', `${radius}`); - circle.style.fill = asCssVariable(colorIdentifier); + + circle.style.strokeWidth = `${strokeWidth}px`; + if (colorIdentifier) { + circle.style.fill = asCssVariable(colorIdentifier); + } return circle; } @@ -205,24 +209,26 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV } // Draw * - if (historyItem.parentIds.length > 1) { - // Multi-parent node - const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, circleColor); - svg.append(circleOuter); - - const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, circleColor); - svg.append(circleInner); - } else { + if (historyItemViewModel.isCurrent) { // HEAD - // TODO@lszomoru - implement a better way to determine if the commit is HEAD - if (historyItem.references?.some(ref => ThemeIcon.isThemeIcon(ref.icon) && ref.icon.id === 'target')) { - const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, circleColor); - svg.append(outerCircle); - } + const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 3, CIRCLE_STROKE_WIDTH, circleColor); + svg.append(outerCircle); - // Node - const circle = drawCircle(circleIndex, CIRCLE_RADIUS, circleColor); - svg.append(circle); + const innerCircle = drawCircle(circleIndex, CIRCLE_STROKE_WIDTH, CIRCLE_RADIUS); + svg.append(innerCircle); + } else { + if (historyItem.parentIds.length > 1) { + // Multi-parent node + const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 2, CIRCLE_STROKE_WIDTH, circleColor); + svg.append(circleOuter); + + const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, CIRCLE_STROKE_WIDTH, circleColor); + svg.append(circleInner); + } else { + // Node + const circle = drawCircle(circleIndex, CIRCLE_RADIUS + 1, CIRCLE_STROKE_WIDTH, circleColor); + svg.append(circle); + } } // Set dimensions @@ -246,13 +252,18 @@ export function renderSCMHistoryGraphPlaceholder(columns: ISCMHistoryItemGraphNo return elements.root; } -export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map()): ISCMHistoryItemViewModel[] { +export function toISCMHistoryItemViewModelArray( + historyItems: ISCMHistoryItem[], + colorMap = new Map(), + currentHistoryItemRef?: ISCMHistoryItemRef +): ISCMHistoryItemViewModel[] { let colorIndex = -1; const viewModels: ISCMHistoryItemViewModel[] = []; for (let index = 0; index < historyItems.length; index++) { const historyItem = historyItems[index]; + const isCurrent = historyItem.id === currentHistoryItemRef?.revision; const outputSwimlanesFromPreviousItem = viewModels.at(-1)?.outputSwimlanes ?? []; const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); const outputSwimlanes: ISCMHistoryItemGraphNode[] = []; @@ -326,6 +337,7 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], ...historyItem, references }, + isCurrent, inputSwimlanes, outputSwimlanes, }); diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 0e7648eaf9a..f42718848fb 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -323,6 +323,7 @@ class HistoryItemRenderer implements ITreeRenderer ({ repository, historyItemViewModel, diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index c40e5b214f2..813e43f97cf 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -74,6 +74,7 @@ export interface ISCMHistoryItemGraphNode { export interface ISCMHistoryItemViewModel { readonly historyItem: ISCMHistoryItem; + readonly isCurrent: boolean; readonly inputSwimlanes: ISCMHistoryItemGraphNode[]; readonly outputSwimlanes: ISCMHistoryItemGraphNode[]; } From 022664fc048be69899befbe3a10aa8ba9aa7d0f1 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:58:59 +0200 Subject: [PATCH 260/286] SCM Graph - bring selected items to the top of the references picker (#228212) --- .../contrib/scm/browser/scmHistoryViewPane.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index f42718848fb..e3fb3701038 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -907,31 +907,38 @@ class HistoryItemRefPicker extends Disposable { quickPick.busy = true; quickPick.show(); - quickPick.items = await this._createQuickPickItems(); - quickPick.busy = false; + const items = await this._createQuickPickItems(); // Set initial selection let selectedItems: HistoryItemRefQuickPickItem[] = []; if (this._historyItemsFilter === 'all') { selectedItems.push(this._allQuickPickItem); - quickPick.selectedItems = [this._allQuickPickItem]; } else if (this._historyItemsFilter === 'auto') { selectedItems.push(this._autoQuickPickItem); - quickPick.selectedItems = [this._autoQuickPickItem]; } else { - for (const item of quickPick.items) { - if (item.type === 'separator') { + let index = 0; + while (index < items.length) { + if (items[index].type === 'separator') { + index++; continue; } - if (this._historyItemsFilter.some(ref => ref.id === item.id)) { - selectedItems.push(item); + if (this._historyItemsFilter.some(ref => ref.id === items[index].id)) { + const item = items.splice(index, 1) as HistoryItemRefQuickPickItem[]; + selectedItems.push(...item); + } else { + index++; } } - quickPick.selectedItems = selectedItems; + // Insert the selected items after `All` and `Auto` + items.splice(2, 0, { type: 'separator' }, ...selectedItems); } + quickPick.items = items; + quickPick.selectedItems = selectedItems; + quickPick.busy = false; + return new Promise<'all' | 'auto' | ISCMHistoryItemRef[] | undefined>(resolve => { this._store.add(quickPick.onDidChangeSelection(items => { const { added } = delta(selectedItems, items, (a, b) => compare(a.id ?? '', b.id ?? '')); From a5fd004fb1539e491660c3819d102fbe457eeb8d Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 11 Sep 2024 12:24:43 +0200 Subject: [PATCH 261/286] polish showing warnings in extensions view (#228214) - do not show warning for disabled extensions in untrusted workspace - fix showing warning message when clicked on show link --- .../extensions/browser/extensionsViewlet.ts | 8 +- .../browser/extensionsWorkbenchService.ts | 92 +++++++++++-------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index f6f9270f85a..9666c2c5ed6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -707,7 +707,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE clearNode(this.notificationContainer); this.notificationDisposables.value = new DisposableStore(); const status = this.extensionsWorkbenchService.getExtensionsNotification(); - if (status && !this.searchMarketplaceExtensionsContextKey.get()) { + const query = status?.extensions.map(extension => `@id:${extension.identifier.id}`).join(' '); + if (status && (query === this.searchBox?.getValue() || !this.searchMarketplaceExtensionsContextKey.get())) { this.notificationContainer.setAttribute('aria-label', status.message); this.notificationContainer.classList.remove('hidden'); const messageContainer = append(this.notificationContainer, $('.message-container')); @@ -719,12 +720,11 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE 'role': 'button', 'aria-label': `${status.message}. ${localize('click show', "Click to Show")}` }, localize('show', "Show"))); - const showExtensions = () => this.search(status.extensions.map(extension => `@id:${extension.identifier.id}`).join(' ')); - this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.CLICK, () => showExtensions())); + this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.CLICK, () => this.search(query ?? ''))); this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.KEY_DOWN, (e: KeyboardEvent) => { const standardKeyboardEvent = new StandardKeyboardEvent(e); if (standardKeyboardEvent.keyCode === KeyCode.Enter || standardKeyboardEvent.keyCode === KeyCode.Space) { - showExtensions(); + this.search(query ?? ''); } standardKeyboardEvent.stopPropagation(); })); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 6e966272405..002a159a5df 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -62,6 +62,7 @@ import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; interface IExtensionStateProvider { (extension: Extension): T; @@ -953,6 +954,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IUpdateService private readonly updateService: IUpdateService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IViewsService private readonly viewsService: IViewsService, ) { super(); @@ -1340,48 +1342,16 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private updateExtensionsNotificaiton(): void { - let extensionsNotification: IExtensionsNotification | undefined; - - let message = ''; - const severity = Severity.Warning; - const extensions: IExtension[] = []; - - extensions.push(...this.local.filter(e => e.enablementState === EnablementState.DisabledByInvalidExtension)); - if (extensions.length) { - if (extensions.some(e => e.local && - (!isEngineValid(e.local.manifest.engines.vscode, this.productService.version, this.productService.date) || areApiProposalsCompatible([...e.local.manifest.enabledApiProposals ?? []])) - )) { - message = nls.localize('incompatibleExtensions', "Some extensions are disabled due to version incompatibility. Review and update them."); - } else { - message = nls.localize('invalidExtensions', "You have invalid extensions installed. Review them."); - } - } - - else { - extensions.push(...this.local.filter(e => e.enablementState === EnablementState.DisabledByExtensionDependency)); - if (extensions.length) { - message = nls.localize('missingDependencies', "Some extensions are disabled due to missing dependencies. Review them."); - } - - else { - extensions.push(...this.local.filter(e => !!e.deprecationInfo)); - if (extensions.length) { - message = nls.localize('deprecated extensions', "You have deprecated extensions installed. Review them and migrate to alternatives."); - } - } - } - - if (extensions.length && !this.dismissedNotifications.includes(message)) { - extensionsNotification = { - message, - severity, - extensions, + const computedNotificiation = this.computeExtensionsNotification(); + const extensionsNotification = computedNotificiation && !this.dismissedNotifications.includes(computedNotificiation.message) ? + { + ...computedNotificiation, dismiss: () => { - this.dismissedNotifications.push(message); + this.dismissedNotifications.push(computedNotificiation.message); this.updateExtensionsNotificaiton(); }, - }; - } + } + : undefined; if (this.extensionsNotification?.message !== extensionsNotification?.message) { this.extensionsNotification = extensionsNotification; @@ -1389,6 +1359,50 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } + private computeExtensionsNotification(): Omit | undefined { + + const invalidExtensions = this.local.filter(e => e.enablementState === EnablementState.DisabledByInvalidExtension); + if (invalidExtensions.length) { + if (invalidExtensions.some(e => e.local && + (!isEngineValid(e.local.manifest.engines.vscode, this.productService.version, this.productService.date) || areApiProposalsCompatible([...e.local.manifest.enabledApiProposals ?? []])) + )) { + return { + message: nls.localize('incompatibleExtensions', "Some extensions are disabled due to version incompatibility. Review and update them."), + severity: Severity.Warning, + extensions: invalidExtensions, + }; + } else { + return { + message: nls.localize('invalidExtensions', "You have invalid extensions installed. Review them."), + severity: Severity.Warning, + extensions: invalidExtensions, + }; + } + } + + if (this.workspaceTrustManagementService.isWorkspaceTrusted()) { + const disabledExtensions = this.local.filter(e => e.enablementState === EnablementState.DisabledByExtensionDependency); + if (disabledExtensions.length) { + return { + message: nls.localize('missingDependencies', "Some extensions are disabled due to missing or disabled dependencies. Review them."), + severity: Severity.Warning, + extensions: disabledExtensions, + }; + } + } + + const deprecatedExtensions = this.local.filter(e => !!e.deprecationInfo); + if (deprecatedExtensions.length) { + return { + message: nls.localize('deprecated extensions', "You have deprecated extensions installed. Review them and migrate to alternatives."), + severity: Severity.Warning, + extensions: deprecatedExtensions, + }; + } + + return undefined; + } + getExtensionsNotification(): IExtensionsNotification | undefined { return this.extensionsNotification; } From aa6b3e999063211d980152cef10510b550065f32 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 11 Sep 2024 12:37:06 +0200 Subject: [PATCH 262/286] =?UTF-8?q?SCM=20Graph=20-=20=F0=9F=92=84=20more?= =?UTF-8?q?=20picker=20polish=20(#228216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vs/workbench/contrib/scm/browser/media/scm.css | 1 + src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index e422ba15e60..5088f3878d9 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -537,6 +537,7 @@ .monaco-toolbar .action-label.scm-graph-repository-picker, .monaco-toolbar .action-label.scm-graph-history-item-picker { + color: var(--vscode-icon-foreground); align-items: center; font-weight: normal; line-height: 16px; diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index e3fb3701038..5d03872692f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -956,7 +956,9 @@ class HistoryItemRefPicker extends Disposable { })); this._store.add(quickPick.onDidAccept(() => { - if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'all') { + if (selectedItems.length === 0) { + resolve(undefined); + } else if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'all') { resolve('all'); } else if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'auto') { resolve('auto'); From e961142a8e30eabac3f3b767bdca671f339807cb Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 11 Sep 2024 21:10:02 +1000 Subject: [PATCH 263/286] Avoid passing cell into callbacks of the cell headers of notebook diff editor (#228194) * Collapse unchanged lines in input/metadata editors of NB Diff view * Avoid passing cell into callbacks in nb diff editor cell headers * Ignore --- .../notebook/browser/diff/diffComponents.ts | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 7018c9f126f..b2f0cda260e 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -103,9 +103,9 @@ class PropertyHeader extends Disposable { readonly notebookEditor: INotebookTextDiffEditor, readonly accessor: { updateInfoRendering: (renderOutput: boolean) => void; - checkIfModified: (cell: DiffElementCellViewModelBase) => false | { reason: string | undefined }; - getFoldingState: (cell: DiffElementCellViewModelBase) => PropertyFoldingState; - updateFoldingState: (cell: DiffElementCellViewModelBase, newState: PropertyFoldingState) => void; + checkIfModified: () => false | { reason: string | undefined }; + getFoldingState: () => PropertyFoldingState; + updateFoldingState: (newState: PropertyFoldingState) => void; unChangedLabel: string; changedLabel: string; prefix: string; @@ -166,8 +166,8 @@ class PropertyHeader extends Disposable { target === this._foldingIndicator || this._foldingIndicator.contains(target) || target === metadataStatus || metadataStatus.contains(target) ) { - const oldFoldingState = this.accessor.getFoldingState(this.cell); - this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); + const oldFoldingState = this.accessor.getFoldingState(); + this.accessor.updateFoldingState(oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); this._updateFoldingIcon(); this.accessor.updateInfoRendering(this.cell.renderOutput); } @@ -180,7 +180,7 @@ class PropertyHeader extends Disposable { this.updateMenu(); this._updateFoldingIcon(); - const metadataChanged = this.accessor.checkIfModified(this.cell); + const metadataChanged = this.accessor.checkIfModified(); if (this._propertyChanged) { this._propertyChanged.set(!!metadataChanged); } @@ -200,7 +200,7 @@ class PropertyHeader extends Disposable { } private updateMenu() { - const metadataChanged = this.accessor.checkIfModified(this.cell); + const metadataChanged = this.accessor.checkIfModified(); if (metadataChanged) { const actions: IAction[] = []; createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); @@ -211,7 +211,7 @@ class PropertyHeader extends Disposable { } private _updateFoldingIcon() { - if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { + if (this.accessor.getFoldingState() === PropertyFoldingState.Collapsed) { DOM.reset(this._foldingIndicator, renderIcon(collapsedIcon)); this._propertyExpanded?.set(false); } else { @@ -903,9 +903,9 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: () => renderSourceEditor(), - checkIfModified: (_) => ({ reason: undefined }), - getFoldingState: (cell) => cell.cellFoldingState, - updateFoldingState: (cell, state) => cell.cellFoldingState = state, + checkIfModified: () => ({ reason: undefined }), + getFoldingState: () => this.cell.cellFoldingState, + updateFoldingState: (state) => this.cell.cellFoldingState = state, unChangedLabel: 'Input', changedLabel: 'Input', prefix: 'input', @@ -970,14 +970,14 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: this.updateMetadataRendering.bind(this), - checkIfModified: (cell) => { - return cell.checkMetadataIfModified(); + checkIfModified: () => { + return this.cell.checkMetadataIfModified(); }, - getFoldingState: (cell) => { - return cell.metadataFoldingState; + getFoldingState: () => { + return this.cell.metadataFoldingState; }, - updateFoldingState: (cell, state) => { - cell.metadataFoldingState = state; + updateFoldingState: (state) => { + this.cell.metadataFoldingState = state; }, unChangedLabel: 'Metadata', changedLabel: 'Metadata changed', @@ -1006,14 +1006,14 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: this.updateOutputRendering.bind(this), - checkIfModified: (cell) => { - return cell.checkIfOutputsModified(); + checkIfModified: () => { + return this.cell.checkIfOutputsModified(); }, - getFoldingState: (cell) => { - return cell.outputFoldingState; + getFoldingState: () => { + return this.cell.outputFoldingState; }, - updateFoldingState: (cell, state) => { - cell.outputFoldingState = state; + updateFoldingState: (state) => { + this.cell.outputFoldingState = state; }, unChangedLabel: 'Outputs', changedLabel: 'Outputs changed', @@ -1367,14 +1367,14 @@ export class ModifiedElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: this.updateMetadataRendering.bind(this), - checkIfModified: (cell) => { - return cell.checkMetadataIfModified(); + checkIfModified: () => { + return this.cell.checkMetadataIfModified(); }, - getFoldingState: (cell) => { - return cell.metadataFoldingState; + getFoldingState: () => { + return this.cell.metadataFoldingState; }, - updateFoldingState: (cell, state) => { - cell.metadataFoldingState = state; + updateFoldingState: (state) => { + this.cell.metadataFoldingState = state; }, unChangedLabel: 'Metadata', changedLabel: 'Metadata changed', @@ -1421,14 +1421,14 @@ export class ModifiedElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: this.updateOutputRendering.bind(this), - checkIfModified: (cell) => { - return cell.checkIfOutputsModified(); + checkIfModified: () => { + return this.cell.checkIfOutputsModified(); }, - getFoldingState: (cell) => { - return cell.outputFoldingState; + getFoldingState: () => { + return this.cell.outputFoldingState; }, - updateFoldingState: (cell, state) => { - cell.outputFoldingState = state; + updateFoldingState: (state) => { + this.cell.outputFoldingState = state; }, unChangedLabel: 'Outputs', changedLabel: 'Outputs changed', @@ -1648,11 +1648,11 @@ export class ModifiedElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: () => renderSourceEditor(), - checkIfModified: (cell) => { - return cell.modified?.textModel.getTextBufferHash() !== cell.original?.textModel.getTextBufferHash() ? { reason: undefined } : false; + checkIfModified: () => { + return this.cell.modified?.textModel.getTextBufferHash() !== this.cell.original?.textModel.getTextBufferHash() ? { reason: undefined } : false; }, - getFoldingState: (cell) => cell.cellFoldingState, - updateFoldingState: (cell, state) => cell.cellFoldingState = state, + getFoldingState: () => this.cell.cellFoldingState, + updateFoldingState: (state) => this.cell.cellFoldingState = state, unChangedLabel: 'Input', changedLabel: 'Input changed', prefix: 'input', From 8be2dfa399fe956da6638ed921a5cc42dae7e152 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 11 Sep 2024 21:10:20 +1000 Subject: [PATCH 264/286] Remove context variable name in menu contributions of nb diff view (#228196) --- .../notebook/browser/diff/diffComponents.ts | 8 +--- .../browser/diff/notebookDiffActions.ts | 46 +++++++++---------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index b2f0cda260e..9c4043426b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -142,9 +142,7 @@ class PropertyHeader extends Disposable { return undefined; } }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService)); - this._toolbar.context = { - cell: this.cell - }; + this._toolbar.context = this.cell; const scopedContextKeyService = this.contextKeyService.createScoped(cellToolbarContainer); this._register(scopedContextKeyService); @@ -1673,9 +1671,7 @@ export class ModifiedElement extends AbstractElementRenderer { this._toolbar = this.templateData.toolbar; - this._toolbar.context = { - cell: this.cell - }; + this._toolbar.context = this.cell; const refreshToolbar = () => { const ignore = this.textConfigurationService.getValue(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace'); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index 7c776ef9806..8e5666d8ecb 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -356,29 +356,29 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { if (!context) { return; } - if (!(context.cell instanceof SideBySideDiffElementViewModel)) { + if (!(context instanceof SideBySideDiffElementViewModel)) { return; } - const original = context.cell.original; - const modified = context.cell.modified; + const original = context.original; + const modified = context.modified; - const modifiedCellIndex = context.cell.mainDocumentTextModel.cells.indexOf(modified.textModel); + const modifiedCellIndex = context.mainDocumentTextModel.cells.indexOf(modified.textModel); if (modifiedCellIndex === -1) { return; } const rawEdits: ICellEditOperation[] = [{ editType: CellEditType.Metadata, index: modifiedCellIndex, metadata: original.metadata }]; - if (context.cell.original.language && context.cell.modified.language !== context.cell.original.language) { - rawEdits.push({ editType: CellEditType.CellLanguage, index: modifiedCellIndex, language: context.cell.original.language }); + if (context.original.language && context.modified.language !== context.original.language) { + rawEdits.push({ editType: CellEditType.CellLanguage, index: modifiedCellIndex, language: context.original.language }); } - context.cell.modifiedDocument.applyEdits(rawEdits, true, undefined, () => undefined, undefined, true); + context.modifiedDocument.applyEdits(rawEdits, true, undefined, () => undefined, undefined, true); } }); @@ -396,12 +396,12 @@ registerAction2(class extends Action2 { // } // ); // } -// run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { +// run(accessor: ServicesAccessor, context?: DiffElementViewModelBase) { // if (!context) { // return; // } -// context.cell.renderOutput = true; +// context.renderOutput = true; // } // }); @@ -421,12 +421,12 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { if (!context) { return; } - context.cell.renderOutput = !context.cell.renderOutput; + context.renderOutput = !context.renderOutput; } }); @@ -446,24 +446,24 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { if (!context) { return; } - if (!(context.cell instanceof SideBySideDiffElementViewModel)) { + if (!(context instanceof SideBySideDiffElementViewModel)) { return; } - const original = context.cell.original; - const modified = context.cell.modified; + const original = context.original; + const modified = context.modified; - const modifiedCellIndex = context.cell.mainDocumentTextModel.cells.indexOf(modified.textModel); + const modifiedCellIndex = context.mainDocumentTextModel.cells.indexOf(modified.textModel); if (modifiedCellIndex === -1) { return; } - context.cell.mainDocumentTextModel.applyEdits([{ + context.mainDocumentTextModel.applyEdits([{ editType: CellEditType.Output, index: modifiedCellIndex, outputs: original.outputs }], true, undefined, () => undefined, undefined, true); } @@ -488,8 +488,8 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { - const cell = context?.cell; + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { + const cell = context; if (!cell?.modified) { return; } @@ -519,13 +519,13 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { if (!context) { return; } - const original = context.cell.original; - const modified = context.cell.modified; + const original = context.original; + const modified = context.modified; if (!original || !modified) { return; From fd5ee87cf6fe327d9276fe94445ad1a7bc7f9299 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Wed, 11 Sep 2024 21:10:39 +1000 Subject: [PATCH 265/286] Minor updates to notebook diff editor (#228198) --- .../notebook/browser/diff/diffElementViewModel.ts | 2 -- .../notebook/browser/diff/notebookDiffEditor.ts | 6 +++--- .../notebook/browser/diff/unchangedEditorRegions.ts | 9 +++++++++ .../notebook/common/model/notebookCellTextModel.ts | 10 ++-------- .../contrib/notebook/common/notebookCommon.ts | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index b9a9ae6ecc9..da39e4f6d3c 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -243,8 +243,6 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB this.cellFoldingState = modified?.getTextBufferHash() !== original?.getTextBufferHash() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed; this.metadataFoldingState = PropertyFoldingState.Collapsed; this.outputFoldingState = PropertyFoldingState.Collapsed; - - this._register(this.editorEventDispatcher.onDidChangeLayout(e => this._layoutInfoEmitter.fire({ outerWidth: true }))); } layoutChange() { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index dadcdba8537..973b3ef0587 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -617,11 +617,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, 10); } - private pendingLayouts = new WeakMap(); + private pendingLayouts = new WeakMap(); - layoutNotebookCell(cell: DiffElementCellViewModelBase, height: number) { - const relayout = (cell: DiffElementCellViewModelBase, height: number) => { + layoutNotebookCell(cell: IDiffElementViewModelBase, height: number) { + const relayout = (cell: IDiffElementViewModelBase, height: number) => { this._list.updateElementHeight2(cell, height); }; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts b/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts index 4d039c03b4e..a365cbcb822 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts @@ -86,6 +86,15 @@ function createHideUnchangedRegionOptions(configurationService: IConfigurationSe }; disposables.add(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.minimumLineCount')) { + options.minimumLineCount = configurationService.getValue('diffEditor.hideUnchangedRegions.minimumLineCount'); + } + if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.contextLineCount')) { + options.contextLineCount = configurationService.getValue('diffEditor.hideUnchangedRegions.contextLineCount'); + } + if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.revealLineCount')) { + options.revealLineCount = configurationService.getValue('diffEditor.hideUnchangedRegions.revealLineCount'); + } if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.enabled')) { options.enabled = configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); unchangedRegionsEnablementEmitter.fire(options.enabled); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index d7fd7e68189..6debc78a96d 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -11,8 +11,7 @@ import * as UUID from '../../../../../base/common/uuid.js'; import { Range } from '../../../../../editor/common/core/range.js'; import * as model from '../../../../../editor/common/model.js'; import { PieceTreeTextBuffer } from '../../../../../editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.js'; -import { PieceTreeTextBufferBuilder } from '../../../../../editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.js'; -import { TextModel } from '../../../../../editor/common/model/textModel.js'; +import { createTextBuffer, TextModel } from '../../../../../editor/common/model/textModel.js'; import { PLAINTEXT_LANGUAGE_ID } from '../../../../../editor/common/languages/modesRegistry.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { NotebookCellOutputTextModel } from './notebookCellOutputTextModel.js'; @@ -113,12 +112,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._textBuffer; } - const builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(this._source); - const bufferFactory = builder.finish(true); - const { textBuffer, disposable } = bufferFactory.create(model.DefaultEndOfLine.LF); - this._textBuffer = textBuffer; - this._register(disposable); + this._textBuffer = this._register(createTextBuffer(this._source, model.DefaultEndOfLine.LF).textBuffer); this._register(this._textBuffer.onDidChangeContent(() => { this._hash = null; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 8a526b263ee..b84d74a275d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -562,7 +562,7 @@ export interface INotebookContributionData { priority?: RegisteredEditorPriority; } -export namespace NotebookUri { +export namespace NotebookMetadataUri { export const scheme = Schemas.vscodeNotebookMetadata; export function generate(notebook: URI): URI { return generateMetadataUri(notebook); From 819cf1cd22389f2acf39ce8ebb6553191c89b4b6 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 11 Sep 2024 13:21:52 +0200 Subject: [PATCH 266/286] watcher - allow to use latest version of `@parcel/watcher` behind experimental setting (#228200) We are pulling this in from `@bpasero/watcher` as a temporary solutionto: - fix a deadlock issue in upstream - allow to switch back and forth between the old and the new version --- .eslintrc.json | 1 + build/.moduleignore | 6 +++ build/gulpfile.scan.js | 3 +- package-lock.json | 39 +++++++++++++++++++ package.json | 1 + remote/package-lock.json | 39 +++++++++++++++++++ remote/package.json | 1 + .../node/nativeModules.integrationTest.ts | 5 +++ .../node/watcher/parcel/parcelWatcher.ts | 12 ++++-- .../files/node/watcher/watcherStats.ts | 4 +- .../utilityProcessWorkerMainService.ts | 24 +++++++++--- .../files/browser/files.contribution.ts | 6 +++ .../browser/relauncher.contribution.ts | 8 +++- 13 files changed, 137 insertions(+), 12 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 4f0f7dc565e..97333387f86 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -693,6 +693,7 @@ "when": "hasNode", "allow": [ "@parcel/watcher", + "@bpasero/watcher", "@vscode/sqlite3", "@vscode/vscode-languagedetection", "@vscode/ripgrep", diff --git a/build/.moduleignore b/build/.moduleignore index 3f573e06078..026c0d3b51d 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -110,6 +110,12 @@ node-pty/third_party/** @parcel/watcher/src/** !@parcel/watcher/build/Release/*.node +@bpasero/watcher/binding.gyp +@bpasero/watcher/build/** +@bpasero/watcher/prebuilds/** +@bpasero/watcher/src/** +!@bpasero/watcher/build/Release/*.node + vsda/build/** vsda/ci/** vsda/src/** diff --git a/build/gulpfile.scan.js b/build/gulpfile.scan.js index 582481bf1eb..be685a8b145 100644 --- a/build/gulpfile.scan.js +++ b/build/gulpfile.scan.js @@ -83,7 +83,8 @@ function nodeModules(destinationExe, destinationPdb, platform) { // We don't build the prebuilt node files so we don't scan them '!**/prebuilds/**/*.node', // These are 3rd party modules that we should ignore - '!**/@parcel/watcher/**/*'])) + '!**/@parcel/watcher/**/*', + '!**/@bpasero/watcher/**/*'])) .pipe(gulp.dest(destinationExe)); }; diff --git a/package-lock.json b/package-lock.json index 1cf4f0e71a8..2311c494b63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@bpasero/watcher": "https://github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", @@ -940,6 +941,44 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bpasero/watcher": { + "version": "2.4.2-alpha.0", + "resolved": "git+ssh://git@github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", + "integrity": "sha512-8AWyO22MDRxp6zAJxDWXGFCHtBoioaku+x/IQrDT3VADhBRAeCVp/47qCVrHFyU0ixo0m8KGDBN6MtxqsFFU2g==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@bpasero/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@bpasero/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", diff --git a/package.json b/package.json index 5b3d18c55dc..d3ce3fd36a8 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@bpasero/watcher": "https://github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", diff --git a/remote/package-lock.json b/remote/package-lock.json index f7f57d71ba8..2560f7cdbc1 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -8,6 +8,7 @@ "name": "vscode-reh", "version": "0.0.0", "dependencies": { + "@bpasero/watcher": "https://github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", @@ -44,6 +45,44 @@ "yazl": "^2.4.3" } }, + "node_modules/@bpasero/watcher": { + "version": "2.4.2-alpha.0", + "resolved": "git+ssh://git@github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", + "integrity": "sha512-8AWyO22MDRxp6zAJxDWXGFCHtBoioaku+x/IQrDT3VADhBRAeCVp/47qCVrHFyU0ixo0m8KGDBN6MtxqsFFU2g==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@bpasero/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@bpasero/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/@microsoft/1ds-core-js": { "version": "3.2.13", "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", diff --git a/remote/package.json b/remote/package.json index ab787bb880d..606faaede14 100644 --- a/remote/package.json +++ b/remote/package.json @@ -6,6 +6,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@bpasero/watcher": "https://github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.23.0", diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index 95aa93b8617..43c09df91e5 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -91,6 +91,11 @@ flakySuite('Native Modules (all platforms)', () => { assert.ok(typeof parcelWatcher.subscribe === 'function', testErrorMessage('@parcel/watcher')); }); + test('@bpasero/watcher', async () => { + const parcelWatcher2 = await import('@bpasero/watcher'); + assert.ok(typeof parcelWatcher2.subscribe === 'function', testErrorMessage('@bpasero/watcher')); + }); + test('@vscode/deviceid', async () => { const deviceIdPackage = await import('@vscode/deviceid'); assert.ok(typeof deviceIdPackage.getDeviceId === 'function', testErrorMessage('@vscode/deviceid')); diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 40ba9732603..ee38b02a765 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as parcelWatcher from '@parcel/watcher'; +import * as parcelWatcher2 from '@bpasero/watcher'; import { existsSync, statSync, unlinkSync } from 'fs'; import { tmpdir, homedir } from 'os'; import { URI } from '../../../../../base/common/uri.js'; @@ -24,6 +25,9 @@ import { FileChangeType, IFileChange } from '../../../common/files.js'; import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +const useParcelWatcher2 = process.env.VSCODE_USE_WATCHER2 === 'true'; +const parcelWatcherLib = useParcelWatcher2 ? parcelWatcher2 : parcelWatcher; + export class ParcelWatcherInstance extends Disposable { private readonly _onDidStop = this._register(new Emitter<{ joinRestart?: Promise }>()); @@ -302,7 +306,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // We already ran before, check for events since if (counter > 1) { - const parcelEvents = await parcelWatcher.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); + const parcelEvents = await parcelWatcherLib.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); if (cts.token.isCancellationRequested) { return; @@ -313,7 +317,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } // Store a snapshot of files to the snapshot file - await parcelWatcher.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); + await parcelWatcherLib.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); // Signal we are ready now when the first snapshot was written if (counter === 1) { @@ -358,7 +362,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); try { - const parcelWatcherInstance = await parcelWatcher.subscribe(realPath, (error, parcelEvents) => { + const parcelWatcherInstance = await parcelWatcherLib.subscribe(realPath, (error, parcelEvents) => { if (watcher.token.isCancellationRequested) { return; // return early when disposed } @@ -858,7 +862,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } private toMessage(message: string, request?: IRecursiveWatchRequest): string { - return request ? `[File Watcher (parcel)] ${message} (path: ${request.path})` : `[File Watcher (parcel)] ${message}`; + return request ? `[File Watcher (${useParcelWatcher2 ? 'parcel-next' : 'parcel-classic'})] ${message} (path: ${request.path})` : `[File Watcher (${useParcelWatcher2 ? 'parcel-next' : 'parcel-classic'})] ${message}`; } protected get recursiveWatcher() { return this; } diff --git a/src/vs/platform/files/node/watcher/watcherStats.ts b/src/vs/platform/files/node/watcher/watcherStats.ts index 4e56825fb19..af911739c47 100644 --- a/src/vs/platform/files/node/watcher/watcherStats.ts +++ b/src/vs/platform/files/node/watcher/watcherStats.ts @@ -7,6 +7,8 @@ import { IUniversalWatchRequest, requestFilterToString } from '../../common/watc import { INodeJSWatcherInstance, NodeJSWatcher } from './nodejs/nodejsWatcher.js'; import { ParcelWatcher, ParcelWatcherInstance } from './parcel/parcelWatcher.js'; +const useParcelWatcher2 = process.env.VSCODE_USE_WATCHER2 === 'true'; + export function computeStats( requests: IUniversalWatchRequest[], recursiveWatcher: ParcelWatcher, @@ -59,7 +61,7 @@ export function computeStats( fillNonRecursiveWatcherStats(nonRecursiveWatcheLines, nonRecursiveWatcher); lines.push(...alignTextColumns(nonRecursiveWatcheLines)); - return `\n\n[File Watcher] request stats:\n\n${lines.join('\n')}\n\n`; + return useParcelWatcher2 ? `\n\n[File Watcher NEXT] request stats:\n\n${lines.join('\n')}\n\n` : `\n\n[File Watcher CLASSIC] request stats:\n\n${lines.join('\n')}\n\n`; } function alignTextColumns(lines: string[]) { diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts index 9ab01b9be15..2a622905d4b 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts @@ -8,12 +8,13 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ILogService } from '../../log/common/log.js'; import { IUtilityProcessWorkerCreateConfiguration, IOnDidTerminateUtilityrocessWorkerProcess, IUtilityProcessWorkerConfiguration, IUtilityProcessWorkerProcessExit, IUtilityProcessWorkerService } from '../common/utilityProcessWorkerService.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; -import { WindowUtilityProcess } from './utilityProcess.js'; +import { IWindowUtilityProcessConfiguration, WindowUtilityProcess } from './utilityProcess.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { hash } from '../../../base/common/hash.js'; import { Event, Emitter } from '../../../base/common/event.js'; import { DeferredPromise } from '../../../base/common/async.js'; import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; export const IUtilityProcessWorkerMainService = createDecorator('utilityProcessWorker'); @@ -32,7 +33,8 @@ export class UtilityProcessWorkerMainService extends Disposable implements IUtil @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); } @@ -50,7 +52,7 @@ export class UtilityProcessWorkerMainService extends Disposable implements IUtil } // Create new worker - const worker = new UtilityProcessWorker(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService, configuration); + const worker = new UtilityProcessWorker(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService, this.configurationService, configuration); if (!worker.spawn()) { return { reason: { code: 1, signal: 'EINVALID' } }; } @@ -106,6 +108,7 @@ class UtilityProcessWorker extends Disposable { @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @IConfigurationService private readonly configurationService: IConfigurationService, private readonly configuration: IUtilityProcessWorkerCreateConfiguration ) { super(); @@ -122,7 +125,7 @@ class UtilityProcessWorker extends Disposable { const window = this.windowsMainService.getWindowById(this.configuration.reply.windowId); const windowPid = window?.win?.webContents.getOSProcessId(); - return this.utilityProcess.start({ + let configuration: IWindowUtilityProcessConfiguration = { type: this.configuration.process.type, entryPoint: this.configuration.process.moduleId, parentLifecycleBound: windowPid, @@ -131,7 +134,18 @@ class UtilityProcessWorker extends Disposable { responseWindowId: this.configuration.reply.windowId, responseChannel: this.configuration.reply.channel, responseNonce: this.configuration.reply.nonce - }); + }; + + if (this.configuration.process.type === 'fileWatcher' && this.configurationService.getValue('files.experimentalWatcherNext') === true) { + configuration = { + ...configuration, + env: { + VSCODE_USE_WATCHER2: 'true' + } + }; + } + + return this.utilityProcess.start(configuration); } kill() { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 2a98f7a1286..68ad1b72c93 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -308,6 +308,12 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('watcherInclude', "Configure extra paths to watch for changes inside the workspace. By default, all workspace folders will be watched recursively, except for folders that are symbolic links. You can explicitly add absolute or relative paths to support watching folders that are symbolic links. Relative paths will be resolved to an absolute path using the currently opened workspace."), 'scope': ConfigurationScope.RESOURCE }, + 'files.experimentalWatcherNext': { // TODO@bpasero decide on default and experiment enlisting + 'type': 'boolean', + 'default': false, + 'markdownDescription': nls.localize('experimentalWatcherNext', "Enables a newer, experimental version of the file watcher."), + scope: ConfigurationScope.APPLICATION + }, 'files.hotExit': hotExitConfiguration, 'files.defaultLanguage': { 'type': 'string', diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 56166ad831a..ad62b0a04d2 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -30,6 +30,7 @@ interface IConfiguration extends IWindowsConfiguration { workbench?: { enableExperiments?: boolean }; _extensionsGallery?: { enablePPE?: boolean }; accessibility?: { verbosity?: { debug?: boolean } }; + files?: { experimentalWatcherNext?: boolean }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { @@ -46,7 +47,8 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'workbench.enableExperiments', '_extensionsGallery.enablePPE', 'security.restrictUNCAccess', - 'accessibility.verbosity.debug' + 'accessibility.verbosity.debug', + 'files.experimentalWatcherNext' ]; private readonly titleBarStyle = new ChangeObserver('string'); @@ -61,6 +63,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private readonly enablePPEExtensionsGallery = new ChangeObserver('boolean'); private readonly restrictUNCAccess = new ChangeObserver('boolean'); private readonly accessibilityVerbosityDebug = new ChangeObserver('boolean'); + private readonly filesExperimentalWatcherNext = new ChangeObserver('boolean'); constructor( @IHostService private readonly hostService: IHostService, @@ -123,6 +126,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Debug accessibility verbosity processChanged(this.accessibilityVerbosityDebug.handleChange(config?.accessibility?.verbosity?.debug)); + + // File watcher next + processChanged(this.filesExperimentalWatcherNext.handleChange(config?.files?.experimentalWatcherNext)); } // Experiments From 8b3867e54d5d52da7a5d1c2b50528d08fc3d5dd5 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 11 Sep 2024 14:06:06 +0200 Subject: [PATCH 267/286] make event readonly (#228124) --- src/vs/workbench/contrib/extensions/common/extensions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 9cae86e743c..0b880a40d55 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -156,7 +156,7 @@ export interface IExtensionsWorkbenchService { updateAll(): Promise; updateRunningExtensions(): Promise; - onDidChangeExtensionsNotification: Event; + readonly onDidChangeExtensionsNotification: Event; getExtensionsNotification(): IExtensionsNotification | undefined; // Sync APIs From de8d1f7b4630108954b6c2186bed74afe62a7597 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 11 Sep 2024 14:53:35 +0200 Subject: [PATCH 268/286] feedback (#228229) --- .../extensions/browser/extensionsWorkbenchService.ts | 4 ++-- src/vs/workbench/services/activity/common/activity.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 002a159a5df..cf4ad497858 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1373,7 +1373,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }; } else { return { - message: nls.localize('invalidExtensions', "You have invalid extensions installed. Review them."), + message: nls.localize('invalidExtensions', "Invalid extensions detected. Review them."), severity: Severity.Warning, extensions: invalidExtensions, }; @@ -1394,7 +1394,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const deprecatedExtensions = this.local.filter(e => !!e.deprecationInfo); if (deprecatedExtensions.length) { return { - message: nls.localize('deprecated extensions', "You have deprecated extensions installed. Review them and migrate to alternatives."), + message: nls.localize('deprecated extensions', "Deprecated extensions detected. Review them and migrate to alternatives."), severity: Severity.Warning, extensions: deprecatedExtensions, }; diff --git a/src/vs/workbench/services/activity/common/activity.ts b/src/vs/workbench/services/activity/common/activity.ts index 481a4648a4f..be1239fc725 100644 --- a/src/vs/workbench/services/activity/common/activity.ts +++ b/src/vs/workbench/services/activity/common/activity.ts @@ -139,17 +139,17 @@ export class ErrorBadge extends IconBadge { } const activityWarningBadgeForeground = registerColor('activityWarningBadge.foreground', - { dark: Color.black.lighten(0.2), light: Color.white, hcDark: null, hcLight: null }, + { dark: Color.black.lighten(0.2), light: Color.white, hcDark: null, hcLight: Color.black.lighten(0.2) }, localize('activityWarningBadge.foreground', 'Foreground color of the warning activity badge')); const activityWarningBadgeBackground = registerColor('activityWarningBadge.background', - { dark: '#CCA700', light: '#BF8803', hcDark: null, hcLight: null }, + { dark: '#CCA700', light: '#BF8803', hcDark: null, hcLight: '#CCA700' }, localize('activityWarningBadge.background', 'Background color of the warning activity badge')); const activityErrorBadgeForeground = registerColor('activityErrorBadge.foreground', - { dark: Color.black.lighten(0.2), light: Color.white, hcDark: null, hcLight: null }, + { dark: Color.black.lighten(0.2), light: Color.white, hcDark: null, hcLight: Color.black.lighten(0.2) }, localize('activityErrorBadge.foreground', 'Foreground color of the error activity badge')); const activityErrorBadgeBackground = registerColor('activityErrorBadge.background', - { dark: '#F14C4C', light: '#E51400', hcDark: null, hcLight: null }, + { dark: '#F14C4C', light: '#E51400', hcDark: null, hcLight: '#F14C4C' }, localize('activityErrorBadge.background', 'Background color of the error activity badge')); From 139cf0e6482ac32aa0b3a37eb9c4f8befa1e5ab0 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 11 Sep 2024 15:04:56 +0200 Subject: [PATCH 269/286] Fix missing closing brace in defaultWorkerFactory.ts (#228231) --- src/vs/base/browser/defaultWorkerFactory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/defaultWorkerFactory.ts b/src/vs/base/browser/defaultWorkerFactory.ts index 8cf67fd4a2e..a8740a589e9 100644 --- a/src/vs/base/browser/defaultWorkerFactory.ts +++ b/src/vs/base/browser/defaultWorkerFactory.ts @@ -98,7 +98,7 @@ function getWorkerBootstrapUrl(label: string, workerScriptUrl: string, workerBas // terminating characters (such as ' or "). const blob = new Blob([coalesce([ `/*${label}*/`, - workerBaseUrl ? `globalThis.MonacoEnvironment = { baseUrl: ${JSON.stringify(workerBaseUrl)};` : undefined, + workerBaseUrl ? `globalThis.MonacoEnvironment = { baseUrl: ${JSON.stringify(workerBaseUrl)} };` : undefined, `globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(getNLSMessages())};`, `globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(getNLSLanguage())};`, `globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(globalThis._VSCODE_FILE_ROOT)};`, From 2991008c4fd8ba016ed5d25765f816f126cba525 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 11 Sep 2024 15:22:26 +0200 Subject: [PATCH 270/286] Block extension installation when signature verification fails (#228230) * #223942 Block extension installation when signature verification fails * fix tests --- .../node/extensionDownloader.ts | 67 ++++++++----------- .../node/extensionManagementService.ts | 23 ++++++- .../test/node/extensionDownloader.test.ts | 31 +++------ .../browser/extensions.contribution.ts | 26 ++++--- .../extensions/browser/extensionsActions.ts | 31 ++++++--- .../remoteExtensionManagementService.ts | 13 ++-- 6 files changed, 101 insertions(+), 90 deletions(-) diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index e997163ed00..8bfb48b8c46 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -9,12 +9,10 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { Schemas } from '../../../base/common/network.js'; import { joinPath } from '../../../base/common/resources.js'; import * as semver from '../../../base/common/semver/semver.js'; -import { isBoolean } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { Promises as FSPromises } from '../../../base/node/pfs.js'; import { buffer, CorruptZipMessage } from '../../../base/node/zip.js'; -import { IConfigurationService } from '../../configuration/common/configuration.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { ExtensionVerificationStatus, toExtensionManagementError } from '../common/abstractExtensionManagementService.js'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from '../common/extensionManagement.js'; @@ -49,7 +47,6 @@ export class ExtensionsDownloader extends Disposable { @INativeEnvironmentService environmentService: INativeEnvironmentService, @IFileService private readonly fileService: IFileService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, @@ -65,59 +62,53 @@ export class ExtensionsDownloader extends Disposable { const location = await this.downloadVSIX(extension, operation); - let verificationStatus: ExtensionVerificationStatus = false; + if (!verifySignature) { + return { location, verificationStatus: false }; + } - if (verifySignature && this.shouldVerifySignature(extension)) { + if (!extension.isSigned) { + return { location, verificationStatus: 'PackageIsUnsigned' }; + } - let signatureArchiveLocation; + let signatureArchiveLocation; + try { + signatureArchiveLocation = await this.downloadSignatureArchive(extension); + } catch (error) { try { - signatureArchiveLocation = await this.downloadSignatureArchive(extension); + // Delete the downloaded VSIX if signature archive download fails + await this.delete(location); } catch (error) { + this.logService.error(error); + } + throw error; + } + + let verificationStatus; + try { + verificationStatus = await this.extensionSignatureVerificationService.verify(extension, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); + } catch (error) { + verificationStatus = (error as ExtensionSignatureVerificationError).code; + if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { try { - // Delete the downloaded VSIX if signature archive download fails + // Delete the downloaded vsix if VSIX or signature archive is invalid await this.delete(location); } catch (error) { this.logService.error(error); } - throw error; + throw new ExtensionManagementError(CorruptZipMessage, ExtensionManagementErrorCode.CorruptZip); } - + } finally { try { - verificationStatus = await this.extensionSignatureVerificationService.verify(extension, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); + // Delete signature archive always + await this.delete(signatureArchiveLocation); } catch (error) { - verificationStatus = (error as ExtensionSignatureVerificationError).code; - if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { - try { - // Delete the downloaded vsix if VSIX or signature archive is invalid - await this.delete(location); - } catch (error) { - this.logService.error(error); - } - throw new ExtensionManagementError(CorruptZipMessage, ExtensionManagementErrorCode.CorruptZip); - } - } finally { - try { - // Delete signature archive always - await this.delete(signatureArchiveLocation); - } catch (error) { - this.logService.error(error); - } + this.logService.error(error); } } return { location, verificationStatus }; } - private shouldVerifySignature(extension: IGalleryExtension): boolean { - if (!extension.isSigned) { - this.logService.info(`Extension is not signed: ${extension.identifier.id}`); - return false; - } - - const value = this.configurationService.getValue('extensions.verifySignature'); - return isBoolean(value) ? value : true; - } - private async downloadVSIX(extension: IGalleryExtension, operation: InstallOperation): Promise { try { const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 7cc41125987..515e10149dd 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -50,6 +50,8 @@ import { IProductService } from '../../product/common/productService.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { isLinux } from '../../../base/common/platform.js'; export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { @@ -75,12 +77,13 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @IExtensionGalleryService galleryService: IExtensionGalleryService, @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, - @INativeEnvironmentService environmentService: INativeEnvironmentService, + @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IDownloadService private downloadService: IDownloadService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService private readonly fileService: IFileService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IProductService productService: IProductService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService @@ -246,7 +249,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } async download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise { - const { location } = await this.extensionsDownloader.download(extension, operation, !donotVerifySignature); + const { location } = await this.downloadExtension(extension, operation, !donotVerifySignature); return location; } @@ -292,7 +295,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } private async downloadAndExtractGalleryExtension(extensionKey: ExtensionKey, gallery: IGalleryExtension, operation: InstallOperation, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { - const { verificationStatus, location } = await this.extensionsDownloader.download(gallery, operation, !options.donotVerifySignature, options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); + const { verificationStatus, location } = await this.downloadExtension(gallery, operation, !options.donotVerifySignature, options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); try { if (token.isCancellationRequested) { @@ -339,6 +342,20 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } + private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { + if (verifySignature) { + const value = this.configurationService.getValue('extensions.verifySignature'); + verifySignature = isBoolean(value) ? value : true; + } + const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform); + + if (verificationStatus !== true && verifySignature && this.environmentService.isBuilt && !isLinux) { + throw new ExtensionManagementError(nls.localize('download failed', "Signature verification failed with '{0}' error.", verificationStatus === false ? 'NotExecuted' : verificationStatus), ExtensionManagementErrorCode.Signature); + } + + return { location, verificationStatus }; + } + private async extractVSIX(extensionKey: ExtensionKey, location: URI, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { const local = await this.extensionsScanner.extractUserExtension( extensionKey, diff --git a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts index 8bce158e6d3..e7450242fc6 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts @@ -13,8 +13,6 @@ import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { mock } from '../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { IConfigurationService } from '../../../configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; import { INativeEnvironmentService } from '../../../environment/common/environment.js'; import { getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, InstallOperation } from '../../common/extensionManagement.js'; import { getGalleryExtensionId } from '../../common/extensionManagementUtil.js'; @@ -76,16 +74,8 @@ suite('ExtensionDownloader Tests', () => { }); }); - test('download completes successfully if verification is disabled by setting set to false', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: false, verificationResult: 'error' }); - - const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); - - assert.strictEqual(actual.verificationStatus, false); - }); - test('download completes successfully if verification is disabled by options', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + const testObject = aTestObject({ verificationResult: 'error' }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, false); @@ -93,7 +83,7 @@ suite('ExtensionDownloader Tests', () => { }); test('download completes successfully if verification is disabled because the module is not loaded', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: false }); + const testObject = aTestObject({ verificationResult: false }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); @@ -102,7 +92,7 @@ suite('ExtensionDownloader Tests', () => { test('download completes successfully if verification fails to execute', async () => { const errorCode = 'ENOENT'; - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + const testObject = aTestObject({ verificationResult: errorCode }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); @@ -111,7 +101,7 @@ suite('ExtensionDownloader Tests', () => { test('download completes successfully if verification fails ', async () => { const errorCode = 'IntegrityCheckFailed'; - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + const testObject = aTestObject({ verificationResult: errorCode }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); @@ -119,7 +109,7 @@ suite('ExtensionDownloader Tests', () => { }); test('download completes successfully if verification succeeds', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + const testObject = aTestObject({ verificationResult: true }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); @@ -127,23 +117,22 @@ suite('ExtensionDownloader Tests', () => { }); test('download completes successfully for unsigned extension', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + const testObject = aTestObject({ verificationResult: true }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); - assert.strictEqual(actual.verificationStatus, false); + assert.strictEqual(actual.verificationStatus, 'PackageIsUnsigned'); }); test('download completes successfully for an unsigned extension even when signature verification throws error', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + const testObject = aTestObject({ verificationResult: 'error' }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); - assert.strictEqual(actual.verificationStatus, false); + assert.strictEqual(actual.verificationStatus, 'PackageIsUnsigned'); }); - function aTestObject(options: { isSignatureVerificationEnabled: boolean; verificationResult: boolean | string }): ExtensionsDownloader { - instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); + function aTestObject(options: { verificationResult: boolean | string }): ExtensionsDownloader { instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult)); return disposables.add(instantiationService.createInstance(TestExtensionDownloader)); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 6e7306a6186..8e68b570a90 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,13 +8,13 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType } from '../common/extensions.js'; -import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType } from '../common/extensions.js'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; import { ExtensionsInput } from '../common/extensionsInput.js'; import { ExtensionEditor } from './extensionEditor.js'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from './extensionsViewlet.js'; @@ -69,7 +69,7 @@ import { ExtensionsCompletionItemsProvider } from './extensionsCompletionItemsPr import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Event } from '../../../../base/common/event.js'; import { UnsupportedExtensionsMigrationContrib } from './unsupportedExtensionsMigrationContribution.js'; -import { isWeb } from '../../../../base/common/platform.js'; +import { isLinux, isNative, isWeb } from '../../../../base/common/platform.js'; import { ExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; @@ -253,6 +253,13 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', description: localize('extensionsInQuickAccess', "When enabled, extensions can be searched for via Quick Access and report issues from there."), default: true + }, + 'extensions.verifySignature': { + type: 'boolean', + description: localize('extensions.verifySignature', "When enabled, extensions are verified to be signed before getting installed."), + default: true, + scope: ConfigurationScope.APPLICATION, + included: isNative && !isLinux } } }); @@ -679,16 +686,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi ], icon: installWorkspaceRecommendedIcon, run: async () => { - const outdated = this.extensionsWorkbenchService.outdated; - const results = await this.extensionsWorkbenchService.updateAll(); - results.forEach((result) => { - if (result.error) { - const extension: IExtension | undefined = outdated.find((extension) => areSameExtensions(extension.identifier, result.identifier)); - if (extension) { - runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, result.error)); - } - } - }); + await this.extensionsWorkbenchService.updateAll(); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 0390170863a..fd381622bfd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -76,6 +76,7 @@ export class PromptExtensionInstallFailureAction extends Action { constructor( private readonly extension: IExtension, + private readonly options: InstallOptions | undefined, private readonly version: string, private readonly installOperation: InstallOperation, private readonly error: Error, @@ -137,19 +138,21 @@ export class PromptExtensionInstallFailureAction extends Action { return; } + if (ExtensionManagementErrorCode.Signature === (this.error.name)) { await this.dialogService.prompt({ type: 'error', - message: localize('signature verification failed', "{0} cannot verify the '{1}' extension. Are you sure you want to install it?", this.productService.nameLong, this.extension.displayName || this.extension.identifier.id), + message: localize('signature verification failed', "Signature of '{0}' extension could not be verified. Are you sure you want to install?", this.extension.displayName), + detail: getErrorMessage(this.error), buttons: [{ label: localize('install anyway', "Install Anyway"), run: () => { - const installAction = this.instantiationService.createInstance(InstallAction, { donotVerifySignature: true }); + const installAction = this.instantiationService.createInstance(InstallAction, { ...this.options, donotVerifySignature: true, }); installAction.extension = this.extension; return installAction.run(); } }], - cancelButton: localize('cancel', "Cancel") + cancelButton: true }); return; } @@ -552,7 +555,7 @@ export class InstallAction extends ExtensionAction { try { return await this.extensionsWorkbenchService.install(extension, this.options); } catch (error) { - await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, error).run(); + await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, this.options, extension.latestVersion, InstallOperation.Install, error).run(); return undefined; } } @@ -945,11 +948,12 @@ export class UpdateAction extends ExtensionAction { } private async install(extension: IExtension): Promise { + const options = extension.local?.preRelease ? { installPreReleaseVersion: true } : undefined; try { - await this.extensionsWorkbenchService.install(extension, extension.local?.preRelease ? { installPreReleaseVersion: true } : undefined); + await this.extensionsWorkbenchService.install(extension, options); alert(localize('updateExtensionComplete', "Updating extension {0} to version {1} completed.", extension.displayName, extension.latestVersion)); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, options, extension.latestVersion, InstallOperation.Update, err).run(); } } } @@ -1501,10 +1505,11 @@ export class InstallAnotherVersionAction extends ExtensionAction { if (this.extension.local?.manifest.version === pick.id) { return; } + const options = { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }; try { - await this.extensionsWorkbenchService.install(this.extension, { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }); + await this.extensionsWorkbenchService.install(this.extension, options); } catch (error) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension, pick.id, InstallOperation.Install, error).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension, options, pick.id, InstallOperation.Install, error).run(); } } return null; @@ -2053,7 +2058,7 @@ export class InstallRecommendedExtensionAction extends Action { try { await this.extensionWorkbenchService.install(extension); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, undefined, extension.latestVersion, InstallOperation.Install, err).run(); } } } @@ -2332,7 +2337,13 @@ export class ExtensionStatusLabelAction extends Action implements IExtensionCont if (currentStatus !== null) { if (currentStatus === ExtensionState.Installing && this.status === ExtensionState.Installed) { - return canAddExtension() ? this.initialStatus === ExtensionState.Installed && this.version !== currentVersion ? localize('updated', "Updated") : localize('installed', "Installed") : null; + if (this.initialStatus === ExtensionState.Uninstalled && canAddExtension()) { + return localize('installed', "Installed"); + } + if (this.initialStatus === ExtensionState.Installed && this.version !== currentVersion && canAddExtension()) { + return localize('updated', "Updated"); + } + return null; } if (currentStatus === ExtensionState.Uninstalling && this.status === ExtensionState.Uninstalled) { this.initialStatus = this.status; diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index dea6f7238a2..1cd69fbf9f0 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -25,6 +25,7 @@ import { IUserDataProfileService } from '../../userDataProfile/common/userDataPr import { IRemoteUserDataProfilesService } from '../../userDataProfile/common/remoteUserDataProfiles.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { areApiProposalsCompatible } from '../../../../platform/extensions/common/extensionValidator.js'; +import { isBoolean, isUndefined } from '../../../../base/common/types.js'; export class NativeRemoteExtensionManagementService extends RemoteExtensionManagementService { @@ -51,15 +52,19 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag return local; } - override async installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { + override async installFromGallery(extension: IGalleryExtension, installOptions: InstallOptions = {}): Promise { + if (isUndefined(installOptions.donotVerifySignature)) { + const value = this.configurationService.getValue('extensions.verifySignature'); + installOptions.donotVerifySignature = isBoolean(value) ? !value : undefined; + } const local = await this.doInstallFromGallery(extension, installOptions); await this.installUIDependenciesAndPackedExtensions(local); return local; } - private async doInstallFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { + private async doInstallFromGallery(extension: IGalleryExtension, installOptions: InstallOptions): Promise { if (this.configurationService.getValue('remote.downloadExtensionsLocally')) { - return this.downloadAndInstall(extension, installOptions || {}); + return this.downloadAndInstall(extension, installOptions); } try { const clientTargetPlatform = await this.localExtensionManagementServer.extensionManagementService.getTargetPlatform(); @@ -73,7 +78,7 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag case ExtensionManagementErrorCode.Unknown: try { this.logService.error(`Error while installing '${extension.identifier.id}' extension in the remote server.`, toErrorMessage(error)); - return await this.downloadAndInstall(extension, installOptions || {}); + return await this.downloadAndInstall(extension, installOptions); } catch (e) { this.logService.error(e); throw e; From 3635f17861a98535e2e14a69b94429680d1215d5 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 11 Sep 2024 15:42:34 +0200 Subject: [PATCH 271/286] Better scope for instanceof with tree-sitter (#228236) --- src/vs/editor/common/languages/highlights/typescript.scm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/languages/highlights/typescript.scm b/src/vs/editor/common/languages/highlights/typescript.scm index 4d3be97c142..56b842837ad 100644 --- a/src/vs/editor/common/languages/highlights/typescript.scm +++ b/src/vs/editor/common/languages/highlights/typescript.scm @@ -17,11 +17,12 @@ ("typeof") @keyword.operator.expression.typeof +(binary_expression "instanceof" @keyword.operator.expression.instanceof) + [ "delete" "in" "infer" - "instanceof" "keyof" "of" ] @keyword.operator.expression From f4b5fd6791a87682c4b80f8eedbe91f13c5c33b7 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 11 Sep 2024 15:44:04 +0200 Subject: [PATCH 272/286] Use 0.0.3 @vscode/tree-sitter-wasm (#228235) --- package-lock.json | 8 ++++---- package.json | 2 +- remote/package-lock.json | 8 ++++---- remote/package.json | 2 +- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2311c494b63..0edf97d784c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", @@ -3205,9 +3205,9 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.2.tgz", - "integrity": "sha512-N57MR/kt4jR0H/TXeDsVYeJmvvUiK7avow0fjy+/EeKcyNBJcM2BFhj4XOAaaMbhGsOcIeSvJFouRWctXI7sKw==" + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.3.tgz", + "integrity": "sha512-K5YmUdohFCavPqEzG2cUPcZ6555KE1qwMDCjkvSUSz+s+8Wro2xfg+syLq90y6Tq0ZSUVvpuX6yq6ukToeGaLg==" }, "node_modules/@vscode/v8-heap-parser": { "version": "0.1.0", diff --git a/package.json b/package.json index d3ce3fd36a8..407ac9a8689 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index 2560f7cdbc1..f11f798759c 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -17,7 +17,7 @@ "@vscode/proxy-agent": "^0.23.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", @@ -223,9 +223,9 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.2.tgz", - "integrity": "sha512-N57MR/kt4jR0H/TXeDsVYeJmvvUiK7avow0fjy+/EeKcyNBJcM2BFhj4XOAaaMbhGsOcIeSvJFouRWctXI7sKw==" + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.3.tgz", + "integrity": "sha512-K5YmUdohFCavPqEzG2cUPcZ6555KE1qwMDCjkvSUSz+s+8Wro2xfg+syLq90y6Tq0ZSUVvpuX6yq6ukToeGaLg==" }, "node_modules/@vscode/vscode-languagedetection": { "version": "1.0.21", diff --git a/remote/package.json b/remote/package.json index 606faaede14..8f29232fea9 100644 --- a/remote/package.json +++ b/remote/package.json @@ -12,7 +12,7 @@ "@vscode/proxy-agent": "^0.23.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index a454672bb4d..332f3ccf36b 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -11,7 +11,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "0.2.0-beta.39", "@xterm/addon-image": "0.9.0-beta.56", @@ -74,9 +74,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.2.tgz", - "integrity": "sha512-N57MR/kt4jR0H/TXeDsVYeJmvvUiK7avow0fjy+/EeKcyNBJcM2BFhj4XOAaaMbhGsOcIeSvJFouRWctXI7sKw==" + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.3.tgz", + "integrity": "sha512-K5YmUdohFCavPqEzG2cUPcZ6555KE1qwMDCjkvSUSz+s+8Wro2xfg+syLq90y6Tq0ZSUVvpuX6yq6ukToeGaLg==" }, "node_modules/@vscode/vscode-languagedetection": { "version": "1.0.21", diff --git a/remote/web/package.json b/remote/web/package.json index 66daa8251e9..c4d2eaeb540 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -6,7 +6,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "0.2.0-beta.39", "@xterm/addon-image": "0.9.0-beta.56", From 17d912dc9162a4f0e6e3ae6ead81ce2fc928ee57 Mon Sep 17 00:00:00 2001 From: Thomas Dubosc Date: Wed, 11 Sep 2024 14:48:42 +0100 Subject: [PATCH 273/286] fix: swap end for flex-end in browser/hover.css (#224102) Swaps end for flex-end in browser/hover.css Fixes justify-content value set in https://github.com/microsoft/vscode/pull/210472 . This resolves an autoprefixer warning while bundling with monaco-editor. ``` txt autoprefixer: end value has mixed support, consider using flex-end instead ``` Co-authored-by: Aiday Marlen Kyzy --- src/vs/editor/contrib/hover/browser/hover.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 2520856e59d..958f1ee9cf4 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -43,7 +43,7 @@ flex-direction: column; padding-left: 5px; padding-right: 5px; - justify-content: end; + justify-content: flex-end; border-right: 1px solid var(--vscode-editorHoverWidget-border); } From 9c9e425876c54127d8277350cfad9cb5df3d798b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:23:56 +0200 Subject: [PATCH 274/286] SCM Graph - remove red/green from default graph theme colors (#228238) --- .../workbench/contrib/scm/browser/scmHistory.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index f87712b51c3..baf6efbc5f8 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -5,8 +5,7 @@ import { localize } from '../../../../nls.js'; import { deepClone } from '../../../../base/common/objects.js'; -import { buttonForeground, foreground } from '../../../../platform/theme/common/colorRegistry.js'; -import { chartsBlue, chartsGreen, chartsOrange, chartsPurple, chartsRed, chartsYellow } from '../../../../platform/theme/common/colors/chartsColors.js'; +import { buttonForeground, editorInfoForeground, foreground } from '../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable, ColorIdentifier, registerColor, transparent } from '../../../../platform/theme/common/colorUtils.js'; import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel } from '../common/history.js'; import { rot } from '../../../../base/common/numbers.js'; @@ -21,9 +20,9 @@ const CIRCLE_STROKE_WIDTH = 2; /** * History item reference colors (local, remote, base) */ -export const historyItemRefColor = registerColor('scmGraph.historyItemRefColor', chartsBlue, localize('scmGraphHistoryItemRefColor', "History item reference color.")); -export const historyItemRemoteRefColor = registerColor('scmGraph.historyItemRemoteRefColor', chartsPurple, localize('scmGraphHistoryItemRemoteRefColor', "History item remote reference color.")); -export const historyItemBaseRefColor = registerColor('scmGraph.historyItemBaseRefColor', chartsOrange, localize('scmGraphHistoryItemBaseRefColor', "History item base reference color.")); +export const historyItemRefColor = registerColor('scmGraph.historyItemRefColor', editorInfoForeground, localize('scmGraphHistoryItemRefColor', "History item reference color.")); +export const historyItemRemoteRefColor = registerColor('scmGraph.historyItemRemoteRefColor', '#B66DFF', localize('scmGraphHistoryItemRemoteRefColor', "History item remote reference color.")); +export const historyItemBaseRefColor = registerColor('scmGraph.historyItemBaseRefColor', '#EA5C00', localize('scmGraphHistoryItemBaseRefColor', "History item base reference color.")); /** * History item hover color @@ -38,9 +37,10 @@ export const historyItemHoverDeletionsForeground = registerColor('scmGraph.histo * History graph color registry */ export const colorRegistry: ColorIdentifier[] = [ - registerColor('scmGraph.foreground1', chartsGreen, localize('scmGraphForeground1', "Source control graph foreground color (1).")), - registerColor('scmGraph.foreground2', chartsRed, localize('scmGraphForeground2', "Source control graph foreground color (2).")), - registerColor('scmGraph.foreground3', chartsYellow, localize('scmGraphForeground3', "Source control graph foreground color (3).")), + registerColor('scmGraph.foreground1', '#FFB000', localize('scmGraphForeground1', "Source control graph foreground color (1).")), + registerColor('scmGraph.foreground2', '#DC267F', localize('scmGraphForeground2', "Source control graph foreground color (2).")), + registerColor('scmGraph.foreground3', '#994F00', localize('scmGraphForeground3', "Source control graph foreground color (3).")), + registerColor('scmGraph.foreground4', '#40B0A6', localize('scmGraphForeground4', "Source control graph foreground color (4).")), ]; function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map): ColorIdentifier | undefined { From 6f56b21aca9e96d82db6de26a30273620eea769e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Sep 2024 07:53:07 -0700 Subject: [PATCH 275/286] fix aria alert not happening when auto synthesize setting is enabled and voice input is not used (#228153) --- src/vs/workbench/contrib/chat/browser/chat.ts | 4 ++-- .../contrib/chat/browser/chatAccessibilityService.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatWidget.ts | 8 ++++---- .../chat/electron-sandbox/actions/voiceChatActions.ts | 4 ++-- .../chat/browser/terminalChatController.ts | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 6273a5e0310..05dca109bb9 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -73,7 +73,7 @@ export const IChatAccessibilityService = createDecorator; + acceptInput(query?: string, isVoiceInput?: boolean): Promise; acceptInputWithPrefix(prefix: string): void; setInputPlaceholder(placeholder: string): void; resetInputPlaceholder(): void; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index cb7fe9da931..763de2b1200 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -37,7 +37,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilityProgressSignalScheduler, CHAT_RESPONSE_PENDING_ALLOWANCE_MS, undefined)); return this._requestId; } - acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { + acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number, isVoiceInput?: boolean): void { this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.toString(); @@ -47,7 +47,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi } const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; const plainTextResponse = renderStringAsPlaintext(new MarkdownString(responseContent)); - if (this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') { + if (!isVoiceInput || this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') { status(plainTextResponse + errorDetails); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 651f68e6f0f..0368011c088 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -762,8 +762,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.logInputHistory(); } - async acceptInput(query?: string): Promise { - return this._acceptInput(query ? { query } : undefined); + async acceptInput(query?: string, isVoiceInput?: boolean): Promise { + return this._acceptInput(query ? { query } : undefined, isVoiceInput); } async acceptInputWithPrefix(prefix: string): Promise { @@ -780,7 +780,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return inputState; } - private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { + private async _acceptInput(opts: { query: string } | { prefix: string } | undefined, isVoiceInput?: boolean): Promise { if (this.viewModel) { this._onDidAcceptInput.fire(); @@ -803,7 +803,7 @@ export class ChatWidget extends Disposable implements IChatWidget { result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; - this.chatAccessibilityService.acceptResponse(lastResponse, requestId); + this.chatAccessibilityService.acceptResponse(lastResponse, requestId, isVoiceInput); if (lastResponse?.result?.nextQuestion) { const { prompt, participant, command } = lastResponse.result.nextQuestion; const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command); diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 0e906a7ded7..7efde1b127d 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -209,7 +209,7 @@ class VoiceChatSessionControllerFactory { onDidAcceptInput: chatWidget.onDidAcceptInput, onDidHideInput: chatWidget.onDidHide, focusInput: () => chatWidget.focusInput(), - acceptInput: () => chatWidget.acceptInput(), + acceptInput: () => chatWidget.acceptInput(undefined, true), updateInput: text => chatWidget.setInput(text), getInput: () => chatWidget.getInput(), setInputPlaceholder: text => chatWidget.setInputPlaceholder(text), @@ -226,7 +226,7 @@ class VoiceChatSessionControllerFactory { onDidAcceptInput: terminalChat.onDidAcceptInput, onDidHideInput: terminalChat.onDidHide, focusInput: () => terminalChat.focus(), - acceptInput: () => terminalChat.acceptInput(), + acceptInput: () => terminalChat.acceptInput(true), updateInput: text => terminalChat.updateInput(text, false), getInput: () => terminalChat.getInput(), setInputPlaceholder: text => terminalChat.setPlaceholder(text), diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index a88fdd95cfb..0fd92493bc3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -204,7 +204,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.value.setValue(undefined); } - async acceptInput(): Promise { + async acceptInput(isVoiceInput?: boolean): Promise { assertType(this._chatWidget); if (!this._model.value) { await this.reveal(); @@ -222,7 +222,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr const store = new DisposableStore(); this._requestActiveContextKey.set(true); let responseContent = ''; - const response = await this._chatWidget.value.inlineChatWidget.chatWidget.acceptInput(lastInput); + const response = await this._chatWidget.value.inlineChatWidget.chatWidget.acceptInput(lastInput, isVoiceInput); this._currentRequestId = response?.requestId; const responsePromise = new DeferredPromise(); try { From 3f2729f11ebebda024a7836cd85a33bf70ce8ef8 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:31:30 +0200 Subject: [PATCH 276/286] Update classifier.json (#228199) --- .github/classifier.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/classifier.json b/.github/classifier.json index d0a2c778997..5f322e25edd 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -124,7 +124,7 @@ "languages-basic": {"assign": ["aeschli"]}, "languages-diagnostics": {"assign": ["jrieken"]}, "languages-guessing": {"assign": ["TylerLeonhardt"]}, - "layout": {"assign": ["sbatten"]}, + "layout": {"assign": ["benibenj"]}, "lcd-text-rendering": {"assign": []}, "list-widget": {"assign": ["joaomoreno"]}, "live-preview": {"assign": ["andreamah"]}, @@ -249,7 +249,7 @@ "timeline": {"assign": ["lramos15"]}, "timeline-git": {"assign": ["lszomoru"]}, "timeline-local-history": {"assign": ["bpasero"]}, - "titlebar": {"assign": ["sbatten"]}, + "titlebar": {"assign": ["benibenj"]}, "tokenization": {"assign": ["alexdima"]}, "touch/pointer": {"assign": []}, "trackpad/scroll": {"assign": []}, @@ -278,7 +278,7 @@ "workbench-cli": {"assign": ["bpasero"]}, "workbench-diagnostics": {"assign": ["Tyriar"]}, "workbench-dnd": {"assign": ["bpasero"]}, - "workbench-editor-grid": {"assign": ["sbatten"]}, + "workbench-editor-grid": {"assign": ["benibenj"]}, "workbench-editor-groups": {"assign": ["bpasero"]}, "workbench-editor-resolver": {"assign": ["lramos15"]}, "workbench-editors": {"assign": ["bpasero"]}, @@ -299,11 +299,11 @@ "workbench-tabs": {"assign": ["benibenj"]}, "workbench-touchbar": {"assign": ["bpasero"]}, "workbench-untitled-editors": {"assign": ["bpasero"]}, - "workbench-views": {"assign": ["sbatten"]}, + "workbench-views": {"assign": ["benibenj"]}, "workbench-welcome": {"assign": ["lramos15"]}, "workbench-window": {"assign": ["bpasero"]}, "workbench-workspace": {"assign": []}, - "workbench-zen": {"assign": ["sbatten"]}, + "workbench-zen": {"assign": ["benibenj"]}, "workspace-edit": {"assign": ["jrieken"]}, "workspace-symbols": {"assign": []}, "workspace-trust": {"assign": ["lszomoru", "sbatten"]}, From e697b033065f75e3c37ba6e3293e60875fa64310 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:33:01 +0200 Subject: [PATCH 277/286] SCM Graph - tweak default colors (#228252) --- src/vs/workbench/contrib/scm/browser/scmHistory.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index baf6efbc5f8..b7c0dc13380 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -5,7 +5,7 @@ import { localize } from '../../../../nls.js'; import { deepClone } from '../../../../base/common/objects.js'; -import { buttonForeground, editorInfoForeground, foreground } from '../../../../platform/theme/common/colorRegistry.js'; +import { buttonForeground, chartsBlue, chartsPurple, foreground } from '../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable, ColorIdentifier, registerColor, transparent } from '../../../../platform/theme/common/colorUtils.js'; import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel } from '../common/history.js'; import { rot } from '../../../../base/common/numbers.js'; @@ -20,8 +20,8 @@ const CIRCLE_STROKE_WIDTH = 2; /** * History item reference colors (local, remote, base) */ -export const historyItemRefColor = registerColor('scmGraph.historyItemRefColor', editorInfoForeground, localize('scmGraphHistoryItemRefColor', "History item reference color.")); -export const historyItemRemoteRefColor = registerColor('scmGraph.historyItemRemoteRefColor', '#B66DFF', localize('scmGraphHistoryItemRemoteRefColor', "History item remote reference color.")); +export const historyItemRefColor = registerColor('scmGraph.historyItemRefColor', chartsBlue, localize('scmGraphHistoryItemRefColor', "History item reference color.")); +export const historyItemRemoteRefColor = registerColor('scmGraph.historyItemRemoteRefColor', chartsPurple, localize('scmGraphHistoryItemRemoteRefColor', "History item remote reference color.")); export const historyItemBaseRefColor = registerColor('scmGraph.historyItemBaseRefColor', '#EA5C00', localize('scmGraphHistoryItemBaseRefColor', "History item base reference color.")); /** @@ -41,6 +41,7 @@ export const colorRegistry: ColorIdentifier[] = [ registerColor('scmGraph.foreground2', '#DC267F', localize('scmGraphForeground2', "Source control graph foreground color (2).")), registerColor('scmGraph.foreground3', '#994F00', localize('scmGraphForeground3', "Source control graph foreground color (3).")), registerColor('scmGraph.foreground4', '#40B0A6', localize('scmGraphForeground4', "Source control graph foreground color (4).")), + registerColor('scmGraph.foreground5', '#B66DFF', localize('scmGraphForeground5', "Source control graph foreground color (5).")), ]; function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map): ColorIdentifier | undefined { From a2230377c8e909775d42b276a31f9f06c8712660 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 11 Sep 2024 19:04:04 +0200 Subject: [PATCH 278/286] use caching for file completions, show workspace relative path (#228257) --- .../browser/contrib/chatInputCompletions.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 231e5d2bff3..95cd5ba9623 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -36,6 +36,7 @@ import { ISearchService } from '../../../../services/search/common/search.js'; import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -374,6 +375,8 @@ class BuiltinDynamicCompletions extends Disposable { this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); } + private cacheKey?: { key: string; time: number }; + private async addFileEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { const makeFileCompletionItem = (resource: URI): CompletionItem => { @@ -382,7 +385,7 @@ class BuiltinDynamicCompletions extends Disposable { const insertText = `${chatVariableLeader}file:${basename} `; return { - label: { label: basename, description: this.labelService.getUriLabel(resource) }, + label: { label: basename, description: this.labelService.getUriLabel(resource, { relative: true }) }, filterText: `${chatVariableLeader}${basename}`, insertText, range: info, @@ -432,10 +435,26 @@ class BuiltinDynamicCompletions extends Disposable { // SEARCH // use file search when having a pattern if (pattern) { + + if (this.cacheKey && Date.now() - this.cacheKey.time > 60000) { + this.searchService.clearCache(this.cacheKey.key); + this.cacheKey = undefined; + } + + if (!this.cacheKey) { + this.cacheKey = { + key: generateUuid(), + time: Date.now() + }; + } + + this.cacheKey.time = Date.now(); + const query = this.queryBuilder.file(this.workspaceContextService.getWorkspace().folders, { filePattern: pattern, sortByScore: true, - maxResults: 100, + maxResults: 250, + cacheKey: this.cacheKey.key }); const data = await this.searchService.fileSearch(query, token); From b040eba7ffe8bbcb3c7352191d32337e12c85c30 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:05:57 -0700 Subject: [PATCH 279/286] Move wgsl into own file Part of #221145 --- .../browser/gpu/fullFileRenderStrategy.ts | 89 +----------------- .../gpu/fullFileRenderStrategy.wgsl.ts | 94 +++++++++++++++++++ 2 files changed, 95 insertions(+), 88 deletions(-) create mode 100644 src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts index 563770d98bc..2771004b423 100644 --- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -14,6 +14,7 @@ import type { ViewContext } from '../../common/viewModel/viewContext.js'; import type { ViewLineOptions } from '../viewParts/lines/viewLineOptions.js'; import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; import type { TextureAtlas } from './atlas/textureAtlas.js'; +import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; import { BindingId, type IGpuRenderStrategy } from './gpu.js'; import { GPULifecycle } from './gpuDisposable.js'; import { quadVertices } from './gpuUtils.js'; @@ -24,94 +25,6 @@ const enum Constants { IndicesPerCell = 6, } -const fullFileRenderStrategyWgsl = /*wgsl*/` -struct GlyphInfo { - position: vec2f, - size: vec2f, - origin: vec2f, -}; - -struct Vertex { - @location(0) position: vec2f, -}; - -struct Cell { - position: vec2f, - unused1: vec2f, - glyphIndex: f32, - textureIndex: f32 -}; - -struct LayoutInfo { - canvasDims: vec2f, - viewportOffset: vec2f, - viewportDims: vec2f, -} - -struct ScrollOffset { - offset: vec2f -} - -struct VSOutput { - @builtin(position) position: vec4f, - @location(1) layerIndex: f32, - @location(0) texcoord: vec2f, -}; - -// Uniforms -@group(0) @binding(${BindingId.ViewportUniform}) var layoutInfo: LayoutInfo; -@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: vec2f; -@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; - -// Storage buffers -@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; -@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; -@group(0) @binding(${BindingId.Cells}) var cells: array; - -@vertex fn vs( - vert: Vertex, - @builtin(instance_index) instanceIndex: u32, - @builtin(vertex_index) vertexIndex : u32 -) -> VSOutput { - let cell = cells[instanceIndex]; - // TODO: Is there a nicer way to init this? - var glyph = glyphInfo0[0]; - let glyphIndex = u32(cell.glyphIndex); - if (u32(cell.textureIndex) == 0) { - glyph = glyphInfo0[glyphIndex]; - } else { - glyph = glyphInfo1[glyphIndex]; - } - - var vsOut: VSOutput; - // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 - vsOut.position = vec4f( - (((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) + (((scrollOffset.offset + layoutInfo.viewportOffset) * 2) / layoutInfo.canvasDims), - 0.0, - 1.0 - ); - - vsOut.layerIndex = cell.textureIndex; - // Textures are flipped from natural direction on the y-axis, so flip it back - vsOut.texcoord = vert.position; - vsOut.texcoord = ( - // Glyph offset (0-1) - (glyph.position / atlasDims) + - // Glyph coordinate (0-1) - (vsOut.texcoord * (glyph.size / atlasDims)) - ); - - return vsOut; -} - -@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; -@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d_array; - -@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { - return textureSample(ourTexture, ourSampler, vsOut.texcoord, u32(vsOut.layerIndex)); -} -`; - const enum CellBufferInfo { FloatsPerEntry = 6, BytesPerEntry = CellBufferInfo.FloatsPerEntry * 4, diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts new file mode 100644 index 00000000000..dbf10934c47 --- /dev/null +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BindingId } from './gpu.js'; + +export const fullFileRenderStrategyWgsl = /*wgsl*/ ` +struct GlyphInfo { + position: vec2f, + size: vec2f, + origin: vec2f, +}; + +struct Vertex { + @location(0) position: vec2f, +}; + +struct Cell { + position: vec2f, + unused1: vec2f, + glyphIndex: f32, + textureIndex: f32 +}; + +struct LayoutInfo { + canvasDims: vec2f, + viewportOffset: vec2f, + viewportDims: vec2f, +} + +struct ScrollOffset { + offset: vec2f +} + +struct VSOutput { + @builtin(position) position: vec4f, + @location(1) layerIndex: f32, + @location(0) texcoord: vec2f, +}; + +// Uniforms +@group(0) @binding(${BindingId.ViewportUniform}) var layoutInfo: LayoutInfo; +@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: vec2f; +@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; + +// Storage buffers +@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; +@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; +@group(0) @binding(${BindingId.Cells}) var cells: array; + +@vertex fn vs( + vert: Vertex, + @builtin(instance_index) instanceIndex: u32, + @builtin(vertex_index) vertexIndex : u32 +) -> VSOutput { + let cell = cells[instanceIndex]; + // TODO: Is there a nicer way to init this? + var glyph = glyphInfo0[0]; + let glyphIndex = u32(cell.glyphIndex); + if (u32(cell.textureIndex) == 0) { + glyph = glyphInfo0[glyphIndex]; + } else { + glyph = glyphInfo1[glyphIndex]; + } + + var vsOut: VSOutput; + // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 + vsOut.position = vec4f( + (((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) + (((scrollOffset.offset + layoutInfo.viewportOffset) * 2) / layoutInfo.canvasDims), + 0.0, + 1.0 + ); + + vsOut.layerIndex = cell.textureIndex; + // Textures are flipped from natural direction on the y-axis, so flip it back + vsOut.texcoord = vert.position; + vsOut.texcoord = ( + // Glyph offset (0-1) + (glyph.position / atlasDims) + + // Glyph coordinate (0-1) + (vsOut.texcoord * (glyph.size / atlasDims)) + ); + + return vsOut; +} + +@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d_array; + +@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { + return textureSample(ourTexture, ourSampler, vsOut.texcoord, u32(vsOut.layerIndex)); +} +`; From 427e8e351d60cbfa5b8260190c9359ef66131250 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 11 Sep 2024 11:07:41 -0700 Subject: [PATCH 280/286] Move executeToolbar to below text input in panel chat (#228262) Not changing quick chat or inline chat, to start. Will also move chat attachments into this space, and possibly split the attachment/other buttons out of the execute toolbar --- .../contrib/chat/browser/chatInputPart.ts | 20 ++++++++++--------- .../contrib/chat/browser/media/chat.css | 10 +++++++--- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 3633aba2768..e703d976f87 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -48,16 +48,16 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { ResourceLabels } from '../../../browser/labels.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; -import { CancelAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, SubmitAction } from './actions/chatExecuteActions.js'; -import { IChatWidget } from './chat.js'; -import { ChatFollowups } from './chatFollowups.js'; +import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_FOCUS, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from '../common/chatContextKeys.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { IChatHistoryEntry, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; -import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js'; +import { CancelAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, SubmitAction } from './actions/chatExecuteActions.js'; +import { IChatWidget } from './chat.js'; +import { ChatFollowups } from './chatFollowups.js'; const $ = dom.$; @@ -598,7 +598,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge get contentHeight(): number { const data = this.getLayoutData(); - return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; + return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight + data.executeToolbarHeight; } layout(height: number, width: number) { @@ -618,10 +618,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const followupsWidth = width - data.inputPartHorizontalPadding; this.followupsContainer.style.width = `${followupsWidth}px`; - this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; + this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight + data.executeToolbarHeight; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); - const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.editorPadding - data.executeToolbarWidth - data.sideToolbarWidth - data.toolbarPadding; + const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.editorPadding - data.executeToolbarWidth - data.sideToolbarWidth; const newDimension = { width: newEditorWidth, height: inputEditorHeight }; if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) { // This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler @@ -637,6 +637,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private getLayoutData() { + const executeToolbarWidth = this.cachedToolbarWidth = this.toolbar.getItemsWidth(); + const executeToolbarPadding = (this.toolbar.getItemsLength() - 1) * 4; return { inputEditorBorder: 2, followupsHeight: this.followupsContainer.offsetHeight, @@ -646,8 +648,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge implicitContextHeight: this.attachedContextContainer.offsetHeight, editorBorder: 2, editorPadding: 12, - toolbarPadding: (this.toolbar.getItemsLength() - 1) * 4, - executeToolbarWidth: this.cachedToolbarWidth = this.toolbar.getItemsWidth(), + executeToolbarWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding : 0, + executeToolbarHeight: this.options.renderStyle === 'compact' ? 0 : 22, sideToolbarWidth: this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) + 4 /*gap*/ : 0, }; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 9a3c450cab8..6b782d5416c 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -427,7 +427,6 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .interactive-input-and-execute-toolbar { - display: flex; box-sizing: border-box; cursor: text; background-color: var(--vscode-input-background); @@ -436,11 +435,12 @@ have to be updated for changes to the rules above, or to support more deeply nes position: relative; padding: 0 6px; margin-bottom: 4px; - align-items: flex-end; - justify-content: space-between; } .interactive-session .interactive-input-part.compact .interactive-input-and-execute-toolbar { + display: flex; + align-items: flex-end; + justify-content: space-between; margin-bottom: 0; border-radius: 2px; } @@ -475,6 +475,10 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-bottom: 7px; } +.interactive-session .interactive-input-part .interactive-execute-toolbar .monaco-action-bar { + float: right; +} + .interactive-session .interactive-input-part .interactive-execute-toolbar .monaco-action-bar .actions-container { display: flex; gap: 4px; From 27a00d021710fcbe65964606f9c425def2936b66 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Sep 2024 12:52:13 -0700 Subject: [PATCH 281/286] tweak help content (#228271) --- src/vs/workbench/browser/actions/helpActions.ts | 4 +--- .../common/gettingStartedContent.ts | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/actions/helpActions.ts b/src/vs/workbench/browser/actions/helpActions.ts index 158dfb1d7de..3a366903e8a 100644 --- a/src/vs/workbench/browser/actions/helpActions.ts +++ b/src/vs/workbench/browser/actions/helpActions.ts @@ -16,7 +16,6 @@ import { ServicesAccessor } from '../../../platform/instantiation/common/instant import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { Categories } from '../../../platform/action/common/actionCommonCategories.js'; import { ICommandService } from '../../../platform/commands/common/commands.js'; -import { IAccessibilityService } from '../../../platform/accessibility/common/accessibility.js'; class KeybindingsReferenceAction extends Action2 { @@ -326,8 +325,7 @@ class GetStartedWithAccessibilityFeatures extends Action2 { } run(accessor: ServicesAccessor): void { const commandService = accessor.get(ICommandService); - const accessibilityService = accessor.get(IAccessibilityService); - commandService.executeCommand('workbench.action.openWalkthrough', accessibilityService.isScreenReaderOptimized() ? 'SetupScreenReader' : 'SetupAccessibility'); + commandService.executeCommand('workbench.action.openWalkthrough', 'SetupAccessibility'); } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index e06b5285451..b6a2fc8f4f0 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -351,9 +351,9 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ } }, { - id: 'SetupScreenReader', - title: localize('gettingStarted.setupScreenReader.title', "Get Started with VS Code using a Screen Reader"), - description: localize('gettingStarted.setupScreenReader.description', "Learn the tools and shortcuts that make VS Code accessible. Note that some actions are not actionable from within the context of the walkthrough."), + id: 'SetupAccessibility', + title: localize('gettingStarted.setupAccessibility.title', "Learn about Accessibility Features in VS Code"), + description: localize('gettingStarted.setupAccessibility.description', "Learn the tools and shortcuts that make VS Code accessible. Note that some actions are not actionable from within the context of the walkthrough."), isFeatured: true, icon: setupIcon, when: CONTEXT_ACCESSIBILITY_MODE_ENABLED.key, @@ -371,7 +371,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, { id: 'accessibleView', - title: localize('gettingStarted.accessibleView.title', "Use the accessible view to inspect content line by line, character by character"), + title: localize('gettingStarted.accessibleView.title', "Screen reader users can inspect content line by line, character by character in the accessible view."), description: localize('gettingStarted.accessibleView.description.interpolated', "The accessible view is available for the terminal, hovers, notifications, comments, notebook output, chat responses, inline completions, and debug console output.\n With focus in any of those features, it can be opened with the Open Accessible View command.\n{0}", Button(localize('openAccessibleView', "Open Accessible View"), 'command:editor.action.accessibleView')), media: { type: 'markdown', path: 'empty' @@ -380,7 +380,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'verbositySettings', title: localize('gettingStarted.verbositySettings.title', "Control the verbosity of aria labels"), - description: localize('gettingStarted.verbositySettings.description.interpolated', "Verbosity settings exist for features around the workbench so that once a user is familiar with a feature, they can avoid hearing hints about how to operate it. For example, features for which an accessibility help dialog exists will indicate how to open the dialog until the verbosity setting for that feature has been disabled.\n These and other accessibility settings can be configured by running the Open Accessibility Settings command.\n{0}", Button(localize('openVerbositySettings', "Open Accessibility Settings"), 'command:workbench.action.openAccessibilitySettings')), + description: localize('gettingStarted.verbositySettings.description.interpolated', "Screen reader verbosity settings exist for features around the workbench so that once a user is familiar with a feature, they can avoid hearing hints about how to operate it. For example, features for which an accessibility help dialog exists will indicate how to open the dialog until the verbosity setting for that feature has been disabled.\n These and other accessibility settings can be configured by running the Open Accessibility Settings command.\n{0}", Button(localize('openVerbositySettings', "Open Accessibility Settings"), 'command:workbench.action.openAccessibilitySettings')), media: { type: 'markdown', path: 'empty' } From 65843d6c663545123c5f1cc5c1df9e0645406aa1 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Wed, 11 Sep 2024 13:16:17 -0700 Subject: [PATCH 282/286] Tools API with a single invokeTool, show spinner for tool call (#228181) * Chat tool confirmations * await --- .../api/browser/mainThreadChatAgents2.ts | 2 +- .../browser/mainThreadLanguageModelTools.ts | 34 +++++++++++++++---- .../api/common/extHostLanguageModelTools.ts | 11 ++++-- .../api/common/extHostTypeConverters.ts | 1 + .../chat/common/languageModelToolsService.ts | 5 +++ .../browser/languageModelToolsService.test.ts | 3 +- ...ode.proposed.chatParticipantAdditions.d.ts | 4 +++ src/vscode-dts/vscode.proposed.lmTools.d.ts | 4 +++ 8 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 83db6b1d91b..a325853c237 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -39,7 +39,7 @@ interface AgentData { hasFollowups?: boolean; } -class MainThreadChatTask implements IChatTask { +export class MainThreadChatTask implements IChatTask { public readonly kind = 'progressTask'; public readonly deferred = new DeferredPromise(); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 4dd68ff5ffe..c7dd92cd7de 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -4,10 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../base/common/htmlContent.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; -import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; +import { ChatModel } from '../../contrib/chat/common/chatModel.js'; +import { IChatService, IChatTask } from '../../contrib/chat/common/chatService.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; +import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; +import { MainThreadChatTask } from './mainThreadChatAgents2.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageModelTools) export class MainThreadLanguageModelTools extends Disposable implements MainThreadLanguageModelToolsShape { @@ -19,6 +23,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre constructor( extHostContext: IExtHostContext, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, + @IChatService private readonly _chatService: IChatService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageModelTools); @@ -30,12 +35,27 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre return Array.from(this._languageModelToolsService.getTools()); } - $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise { - return this._languageModelToolsService.invokeTool( - dto, - (input, token) => this._proxy.$countTokensForInvocation(dto.callId, input, token), - token, - ); + async $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise { + // Shortcut to write to the model directly here, but could call all the way back to use the real stream. + // TODO move this to the tools service? + let task: IChatTask | undefined; + if (dto.context) { + const model = this._chatService.getSession(dto.context?.sessionId) as ChatModel; + const request = model.getRequests().at(-1)!; + const tool = this._languageModelToolsService.getTool(dto.toolId); + task = new MainThreadChatTask(new MarkdownString(`Using ${tool?.displayName ?? dto.toolId}`)); + model.acceptResponseProgress(request, task); + } + + try { + return await this._languageModelToolsService.invokeTool( + dto, + (input, token) => this._proxy.$countTokensForInvocation(dto.callId, input, token), + token, + ); + } finally { + task?.complete(); + } } $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 351a68134a9..56725b94d09 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -12,7 +12,7 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; -import { IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import type * as vscode from 'vscode'; export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { @@ -55,6 +55,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape callId, parameters: options.parameters, tokenBudget: options.tokenOptions?.tokenBudget, + context: options.toolInvocationToken as IToolInvocationContext | undefined, }, token); return typeConvert.LanguageModelToolResult.to(result); } finally { @@ -80,7 +81,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Unknown tool ${dto.toolId}`); } - const options: vscode.LanguageModelToolInvocationOptions = { parameters: dto.parameters }; + const options: vscode.LanguageModelToolInvocationOptions = { parameters: dto.parameters, toolInvocationToken: dto.context }; if (dto.tokenBudget !== undefined) { options.tokenOptions = { tokenBudget: dto.tokenBudget, @@ -89,6 +90,12 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } + // Some participant in extHostChatAgents calls invokeTool, goes to extHostLMTools + // mainThreadLMTools invokes the tool, which calls back to extHostLMTools + // The tool requests permission + // The tool in extHostLMTools calls for permission back to mainThreadLMTools + // And back to extHostLMTools, and back to the participant in extHostChatAgents + // Is there a tool call ID to identify the call? const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token)), token); if (!extensionResult) { throw new CancellationError(); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 2f411ced17e..c00c6918af9 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2747,6 +2747,7 @@ export namespace ChatAgentRequest { acceptedConfirmationData: request.acceptedConfirmationData, rejectedConfirmationData: request.rejectedConfirmationData, location2, + toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) }; } } diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index dee8c8a8c9a..a71895b523d 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -37,6 +37,11 @@ export interface IToolInvocation { toolId: string; parameters: any; tokenBudget?: number; + context: IToolInvocationContext | undefined; +} + +export interface IToolInvocationContext { + sessionId: string; } export interface IToolResult { diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 22ecd7ebe4d..1159effdb95 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -106,7 +106,8 @@ suite('LanguageModelToolsService', () => { tokenBudget: 100, parameters: { a: 1 - } + }, + context: { sessionId: 'a' } }; const result = await service.invokeTool(dto, async () => 0, CancellationToken.None); diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index ab40805bb9b..1c500f53e69 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -248,6 +248,10 @@ declare module 'vscode' { export type ChatExtendedRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; + export interface ChatRequest { + toolInvocationToken: ChatParticipantToolToken; + } + export interface ChatResult { nextQuestion?: { prompt: string; diff --git a/src/vscode-dts/vscode.proposed.lmTools.d.ts b/src/vscode-dts/vscode.proposed.lmTools.d.ts index 3fff1acd8dd..e4b045a4a46 100644 --- a/src/vscode-dts/vscode.proposed.lmTools.d.ts +++ b/src/vscode-dts/vscode.proposed.lmTools.d.ts @@ -103,7 +103,11 @@ declare module 'vscode' { export function invokeTool(id: string, options: LanguageModelToolInvocationOptions, token: CancellationToken): Thenable; } + export type ChatParticipantToolToken = unknown; + export interface LanguageModelToolInvocationOptions { + toolInvocationToken: ChatParticipantToolToken | undefined; + /** * Parameters with which to invoke the tool. */ From 298e7037f027bf5e6add0e0eecaeda10e0195411 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 11 Sep 2024 22:17:37 +0200 Subject: [PATCH 283/286] undo commenting (#228272) --- .../extensionManagement/common/extensionsScannerService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index aaa82639b30..8f238a84029 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -689,8 +689,8 @@ class ExtensionsScanner extends Disposable { validate(extension: IRelaxedScannedExtension, input: ExtensionScannerInput): IRelaxedScannedExtension { let isValid = true; - // const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); - const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, true); + const validateApiVersion = this.environmentService.isBuilt && this.extensionsEnabledWithApiProposalVersion.includes(extension.identifier.id.toLowerCase()); + const validations = validateExtensionManifest(input.productVersion, input.productDate, input.location, extension.manifest, extension.isBuiltin, validateApiVersion); for (const [severity, message] of validations) { if (severity === Severity.Error) { isValid = false; From 03cfbbc769fb70a8663b0297b3da334a215defd2 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 11 Sep 2024 13:48:55 -0700 Subject: [PATCH 284/286] tweak wording of accessibility walkthrough title (#228275) --- .../welcomeGettingStarted/common/gettingStartedContent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index b6a2fc8f4f0..ddbe5e662c6 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -352,7 +352,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, { id: 'SetupAccessibility', - title: localize('gettingStarted.setupAccessibility.title', "Learn about Accessibility Features in VS Code"), + title: localize('gettingStarted.setupAccessibility.title', "Get Started with Accessibility Features"), description: localize('gettingStarted.setupAccessibility.description', "Learn the tools and shortcuts that make VS Code accessible. Note that some actions are not actionable from within the context of the walkthrough."), isFeatured: true, icon: setupIcon, From 88dab8d224076daf01b70644d71c8a7649b06898 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 11 Sep 2024 22:51:22 +0200 Subject: [PATCH 285/286] fix #228076 - do not disable extension for missing dependencies (#228273) A remote extension can depend on built in descriptive extension that runs locally --- .../extensionManagement/browser/extensionEnablementService.ts | 2 +- .../test/browser/extensionEnablementService.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 8bd57b0d460..efdf60b8565 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -474,7 +474,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench : []; if (!dependencyExtensions.length) { - return !!extensions.length && !!extension.manifest.extensionDependencies?.length; + return false; } const hasEnablementState = computedEnablementStates.has(extension); diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 351dc1729f0..fbd716c5fcf 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -979,13 +979,13 @@ suite('ExtensionEnablementService Test', () => { assert.deepStrictEqual(testObject.getEnablementStates(installed), [EnablementState.DisabledGlobally, EnablementState.DisabledByExtensionDependency]); }); - test('test extension is disabled by dependency when it has a missing dependency', async () => { + test('test extension is not disabled when it has a missing dependency', async () => { const target = aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] }); installed.push(target); testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); await (testObject).waitUntilInitialized(); - assert.strictEqual(testObject.getEnablementState(target), EnablementState.DisabledByExtensionDependency); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); }); test('test extension is disabled by invalidity', async () => { From fa97128308b4d10d46a908666353b11efb10ba79 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 11 Sep 2024 23:01:06 +0200 Subject: [PATCH 286/286] remove warning for extensions disabled due to disabled extension dependencies (#228278) --- .../browser/extensionsWorkbenchService.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index cf4ad497858..1b07ed56148 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -62,7 +62,6 @@ import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; interface IExtensionStateProvider { (extension: Extension): T; @@ -954,7 +953,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IUpdateService private readonly updateService: IUpdateService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IViewsService private readonly viewsService: IViewsService, ) { super(); @@ -1380,17 +1378,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } } - if (this.workspaceTrustManagementService.isWorkspaceTrusted()) { - const disabledExtensions = this.local.filter(e => e.enablementState === EnablementState.DisabledByExtensionDependency); - if (disabledExtensions.length) { - return { - message: nls.localize('missingDependencies', "Some extensions are disabled due to missing or disabled dependencies. Review them."), - severity: Severity.Warning, - extensions: disabledExtensions, - }; - } - } - const deprecatedExtensions = this.local.filter(e => !!e.deprecationInfo); if (deprecatedExtensions.length) { return {