From cb4076e563719dc2b29761f8239cbcf4de585d49 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Mon, 18 Nov 2024 10:37:26 -0800
Subject: [PATCH 01/20] Decorations wip
Part of #227095
---
.../browser/gpu/fullFileRenderStrategy.ts | 49 ++++++++++++++++++-
.../browser/gpu/raster/glyphRasterizer.ts | 6 +++
src/vs/editor/browser/gpu/viewGpuContext.ts | 9 +++-
3 files changed, 61 insertions(+), 3 deletions(-)
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index a6df706d2b89e..a42148b222561 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -7,9 +7,12 @@ import { getActiveWindow } from '../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { CursorColumns } from '../../common/core/cursorColumns.js';
+import { Range } from '../../common/core/range.js';
+import { MetadataConsts } from '../../common/encodedTokenAttributes.js';
+import { ClassName } from '../../common/model/intervalTree.js';
import type { IViewLineTokens } from '../../common/tokens/lineTokens.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
-import { ViewEventType, type ViewConfigurationChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js';
+import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js';
import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';
import type { ViewLineRenderingData } from '../../common/viewModel.js';
import type { ViewContext } from '../../common/viewModel/viewContext.js';
@@ -115,6 +118,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
return true;
}
+ public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean {
+ // TODO: Don't clear all lines
+ this._upToDateLines[0].clear();
+ this._upToDateLines[1].clear();
+ return true;
+ }
+
public override onTokensChanged(e: ViewTokensChangedEvent): boolean {
// TODO: This currently fires for the entire viewport whenever scrolling stops
// https://github.com/microsoft/vscode/issues/233942
@@ -274,6 +284,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
}
}
+ const decorations = viewportData.getDecorationsInViewport();
+
for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {
// Only attempt to render lines that the GPU renderer can handle
@@ -291,6 +303,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
dirtyLineStart = Math.min(dirtyLineStart, y);
dirtyLineEnd = Math.max(dirtyLineEnd, y);
+ const inlineDecorations = decorations.filter(e => (
+ e.range.startLineNumber <= y && e.range.endLineNumber >= y &&
+ e.options.inlineClassName
+ ));
+ if (inlineDecorations.length > 0) {
+ console.log('decoration!', inlineDecorations);
+ }
+
lineData = viewportData.getViewLineRenderingData(y);
content = lineData.content;
xOffset = 0;
@@ -313,6 +333,33 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
break;
}
chars = content.charAt(x);
+
+ // TODO: We'd want to optimize pulling the decorations in order
+ // HACK: Temporary replace char to demonstrate inline decorations
+ const cellDecorations = decorations.filter(decoration => {
+ // TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here?
+ if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) {
+ return false;
+ }
+ if (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) {
+ return false;
+ }
+ if (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) {
+ return false;
+ }
+ return true;
+ });
+ for (const decoration of cellDecorations) {
+ switch (decoration.options.inlineClassName) {
+ case (ClassName.EditorDeprecatedInlineDecoration): {
+ // HACK: We probably shouldn't override tokenMetadata
+ tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK;
+ // chars = '-';
+ break;
+ }
+ }
+ }
+
if (chars === ' ' || chars === '\t') {
// Zero out glyph to ensure it doesn't get rendered
cellIndex = ((y - 1) * this._viewGpuContext.maxGpuCols + x) * Constants.IndicesPerCell;
diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
index 26010b7e38690..07d343bb677c4 100644
--- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
+++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
@@ -120,6 +120,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
this._ctx.textBaseline = 'top';
this._ctx.fillText(chars, originX, originY);
+ // TODO: Don't draw beyond glyph - how to handle monospace, wide and proportional?
+ // TODO: Support strikethrough color
+ if (fontStyle & FontStyle.Strikethrough) {
+ this._ctx.fillRect(originX, originY + Math.round(devicePixelFontSize / 2), devicePixelFontSize, Math.max(Math.floor(getActiveWindow().devicePixelRatio), 1));
+ }
+
const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox);
// const offset = {
diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts
index 307636b37e1fe..310a6f97f5d1e 100644
--- a/src/vs/editor/browser/gpu/viewGpuContext.ts
+++ b/src/vs/editor/browser/gpu/viewGpuContext.ts
@@ -19,6 +19,7 @@ import { GPULifecycle } from './gpuDisposable.js';
import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js';
import { RectangleRenderer } from './rectangleRenderer.js';
import type { ViewContext } from '../../common/viewModel/viewContext.js';
+import { ClassName } from '../../common/model/intervalTree.js';
const enum GpuRenderLimits {
maxGpuLines = 3000,
@@ -131,7 +132,8 @@ export class ViewGpuContext extends Disposable {
data.containsRTL ||
data.maxColumn > GpuRenderLimits.maxGpuCols ||
data.continuesWithWrappedLine ||
- data.inlineDecorations.length > 0 ||
+ // HACK: ...
+ data.inlineDecorations.length > 0 && data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration ||
lineNumber >= GpuRenderLimits.maxGpuLines
) {
return false;
@@ -155,7 +157,10 @@ export class ViewGpuContext extends Disposable {
reasons.push('continuesWithWrappedLine');
}
if (data.inlineDecorations.length > 0) {
- reasons.push('inlineDecorations > 0');
+ // HACK: ...
+ if (data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration) {
+ reasons.push('inlineDecorations > 0');
+ }
}
if (lineNumber >= GpuRenderLimits.maxGpuLines) {
reasons.push('lineNumber >= maxGpuLines');
From 8354641acf0adb64e6b881bbdc91345bd9dc05d5 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Tue, 19 Nov 2024 07:58:57 -0800
Subject: [PATCH 02/20] Prototype for extracting inline decoration styles
---
src/vs/editor/browser/gpu/cssRuleExtractor.ts | 77 +++++++++++++++++++
.../browser/gpu/fullFileRenderStrategy.ts | 27 ++++++-
2 files changed, 101 insertions(+), 3 deletions(-)
create mode 100644 src/vs/editor/browser/gpu/cssRuleExtractor.ts
diff --git a/src/vs/editor/browser/gpu/cssRuleExtractor.ts b/src/vs/editor/browser/gpu/cssRuleExtractor.ts
new file mode 100644
index 0000000000000..d8bd41ac385d7
--- /dev/null
+++ b/src/vs/editor/browser/gpu/cssRuleExtractor.ts
@@ -0,0 +1,77 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { $, getActiveDocument } from '../../../base/browser/dom.js';
+import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
+import type { ViewGpuContext } from './viewGpuContext.js';
+
+export class CssRuleExtractor extends Disposable {
+ private _container: HTMLElement;
+
+ private _ruleCache: Map* className */string, CSSStyleRule[]> = new Map();
+
+ constructor(
+ private readonly _viewGpuContext: ViewGpuContext,
+ ) {
+ super();
+
+ this._container = $('div.monaco-css-rule-extractor');
+ this._container.style.visibility = 'hidden';
+ const parentElement = this._viewGpuContext.canvas.domNode.parentElement;
+ if (!parentElement) {
+ throw new Error('No parent element found for the canvas');
+ }
+ parentElement.appendChild(this._container);
+ this._register(toDisposable(() => this._container.remove()));
+ }
+
+ getStyleRules(className: string): CSSStyleRule[] {
+ const existing = this._ruleCache.get(className);
+ if (existing) {
+ return existing;
+ }
+ const dummyElement = $(`span.${className}`);
+ this._container.appendChild(dummyElement);
+ const rules = this._getStyleRules(dummyElement, className);
+ this._ruleCache.set(className, rules);
+ return rules;
+ }
+
+ private _getStyleRules(element: HTMLElement, className: string) {
+ const matchedRules = [];
+
+ // Iterate through all stylesheets
+ const doc = getActiveDocument();
+ for (const stylesheet of doc.styleSheets) {
+ try {
+ // Iterate through all CSS rules in the stylesheet
+ for (const rule of stylesheet.cssRules) {
+ if (rule instanceof CSSImportRule) {
+ // Recursively process the import rule
+ if (rule.styleSheet?.cssRules) {
+ for (const innerRule of rule.styleSheet.cssRules) {
+ if (innerRule instanceof CSSStyleRule) {
+ if (element.matches(innerRule.selectorText) && innerRule.selectorText.includes(className)) {
+ matchedRules.push(innerRule);
+ }
+ }
+ }
+ }
+ } else if (rule instanceof CSSStyleRule) {
+ // Check if the element matches the selector
+ if (element.matches(rule.selectorText) && rule.selectorText.includes(className)) {
+ matchedRules.push(rule);
+ }
+ }
+ }
+ } catch (e) {
+ // Some stylesheets may not be accessible due to CORS restrictions
+ console.warn('Could not access stylesheet:', stylesheet.href);
+ }
+ }
+
+ return matchedRules;
+ }
+}
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index a42148b222561..a57f6c9f085fe 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -3,11 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { getActiveWindow } from '../../../base/browser/dom.js';
+import { getActiveDocument, getActiveWindow } from '../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { CursorColumns } from '../../common/core/cursorColumns.js';
-import { Range } from '../../common/core/range.js';
import { MetadataConsts } from '../../common/encodedTokenAttributes.js';
import { ClassName } from '../../common/model/intervalTree.js';
import type { IViewLineTokens } from '../../common/tokens/lineTokens.js';
@@ -18,6 +17,7 @@ import type { ViewLineRenderingData } from '../../common/viewModel.js';
import type { ViewContext } from '../../common/viewModel/viewContext.js';
import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js';
import type { ITextureAtlasPageGlyph } from './atlas/atlas.js';
+import { CssRuleExtractor } from './cssRuleExtractor.js';
import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js';
import { BindingId, type IGpuRenderStrategy } from './gpu.js';
import { GPULifecycle } from './gpuDisposable.js';
@@ -48,6 +48,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
readonly wgsl: string = fullFileRenderStrategyWgsl;
private readonly _glyphRasterizer: GlyphRasterizer;
+ private readonly _cssRuleExtractor: CssRuleExtractor;
private _cellBindBuffer!: GPUBuffer;
@@ -89,6 +90,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
const fontSize = this._context.configuration.options.get(EditorOption.fontSize);
this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily));
+ this._cssRuleExtractor = this._register(new CssRuleExtractor(this._viewGpuContext));
const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT;
this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
@@ -336,7 +338,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
// TODO: We'd want to optimize pulling the decorations in order
// HACK: Temporary replace char to demonstrate inline decorations
- const cellDecorations = decorations.filter(decoration => {
+ const cellDecorations = inlineDecorations.filter(decoration => {
// TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here?
if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) {
return false;
@@ -350,6 +352,25 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
return true;
});
for (const decoration of cellDecorations) {
+ if (!decoration.options.inlineClassName) {
+ throw new BugIndicatingError('Expected inlineClassName on decoration');
+ }
+ const rules = this._cssRuleExtractor.getStyleRules(decoration.options.inlineClassName);
+ const supportedCssRules = [
+ 'text-decoration-line',
+ 'text-decoration-thickness',
+ 'text-decoration-style',
+ 'text-decoration-color',
+ ];
+ const supported = rules.every(rule => {
+ for (const r of rule.style) {
+ if (!supportedCssRules.includes(r)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ console.log('rules supported?', supported, rules);
switch (decoration.options.inlineClassName) {
case (ClassName.EditorDeprecatedInlineDecoration): {
// HACK: We probably shouldn't override tokenMetadata
From a42e4e9a5f923e10a4af8506502ad8041905e464 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Tue, 19 Nov 2024 12:13:49 -0800
Subject: [PATCH 03/20] Improve rule pulling, report unsupported rules
---
src/vs/editor/browser/gpu/cssRuleExtractor.ts | 77 -------------------
.../browser/gpu/decorationCssRuleExtractor.ts | 57 ++++++++++++++
.../browser/gpu/fullFileRenderStrategy.ts | 29 ++-----
src/vs/editor/browser/gpu/viewGpuContext.ts | 66 ++++++++++++++--
.../browser/viewParts/gpuMark/gpuMark.ts | 4 +-
.../browser/viewParts/viewLines/viewLine.ts | 4 +-
.../viewParts/viewLinesGpu/viewLinesGpu.ts | 4 +-
7 files changed, 128 insertions(+), 113 deletions(-)
delete mode 100644 src/vs/editor/browser/gpu/cssRuleExtractor.ts
create mode 100644 src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
diff --git a/src/vs/editor/browser/gpu/cssRuleExtractor.ts b/src/vs/editor/browser/gpu/cssRuleExtractor.ts
deleted file mode 100644
index d8bd41ac385d7..0000000000000
--- a/src/vs/editor/browser/gpu/cssRuleExtractor.ts
+++ /dev/null
@@ -1,77 +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 { $, getActiveDocument } from '../../../base/browser/dom.js';
-import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
-import type { ViewGpuContext } from './viewGpuContext.js';
-
-export class CssRuleExtractor extends Disposable {
- private _container: HTMLElement;
-
- private _ruleCache: Map* className */string, CSSStyleRule[]> = new Map();
-
- constructor(
- private readonly _viewGpuContext: ViewGpuContext,
- ) {
- super();
-
- this._container = $('div.monaco-css-rule-extractor');
- this._container.style.visibility = 'hidden';
- const parentElement = this._viewGpuContext.canvas.domNode.parentElement;
- if (!parentElement) {
- throw new Error('No parent element found for the canvas');
- }
- parentElement.appendChild(this._container);
- this._register(toDisposable(() => this._container.remove()));
- }
-
- getStyleRules(className: string): CSSStyleRule[] {
- const existing = this._ruleCache.get(className);
- if (existing) {
- return existing;
- }
- const dummyElement = $(`span.${className}`);
- this._container.appendChild(dummyElement);
- const rules = this._getStyleRules(dummyElement, className);
- this._ruleCache.set(className, rules);
- return rules;
- }
-
- private _getStyleRules(element: HTMLElement, className: string) {
- const matchedRules = [];
-
- // Iterate through all stylesheets
- const doc = getActiveDocument();
- for (const stylesheet of doc.styleSheets) {
- try {
- // Iterate through all CSS rules in the stylesheet
- for (const rule of stylesheet.cssRules) {
- if (rule instanceof CSSImportRule) {
- // Recursively process the import rule
- if (rule.styleSheet?.cssRules) {
- for (const innerRule of rule.styleSheet.cssRules) {
- if (innerRule instanceof CSSStyleRule) {
- if (element.matches(innerRule.selectorText) && innerRule.selectorText.includes(className)) {
- matchedRules.push(innerRule);
- }
- }
- }
- }
- } else if (rule instanceof CSSStyleRule) {
- // Check if the element matches the selector
- if (element.matches(rule.selectorText) && rule.selectorText.includes(className)) {
- matchedRules.push(rule);
- }
- }
- }
- } catch (e) {
- // Some stylesheets may not be accessible due to CORS restrictions
- console.warn('Could not access stylesheet:', stylesheet.href);
- }
- }
-
- return matchedRules;
- }
-}
diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
new file mode 100644
index 0000000000000..5959bd36cd3e6
--- /dev/null
+++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
@@ -0,0 +1,57 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { $, getActiveDocument } from '../../../base/browser/dom.js';
+import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
+
+export class DecorationCssRuleExtractor extends Disposable {
+ private _container: HTMLElement;
+
+ private _ruleCache: Map* className */string, CSSStyleRule[]> = new Map();
+
+ constructor() {
+ super();
+ this._container = $('div.monaco-css-rule-extractor');
+ this._container.style.visibility = 'hidden';
+ this._register(toDisposable(() => this._container.remove()));
+ }
+
+ getStyleRules(canvas: HTMLElement, decorationClassName: string): CSSStyleRule[] {
+ const existing = this._ruleCache.get(decorationClassName);
+ if (existing) {
+ return existing;
+ }
+ const dummyElement = $(`span.${decorationClassName}`);
+ this._container.appendChild(dummyElement);
+ const rules = this._getStyleRules(canvas, dummyElement, decorationClassName);
+ this._ruleCache.set(decorationClassName, rules);
+ return rules;
+ }
+
+ private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) {
+ canvas.appendChild(this._container);
+
+ // Iterate through all stylesheets and imported stylesheets to find matching rules
+ const rules = [];
+ const doc = getActiveDocument();
+ const stylesheets = [...doc.styleSheets];
+ for (let i = 0; i < stylesheets.length; i++) {
+ const stylesheet = stylesheets[i];
+ for (const rule of stylesheet.cssRules) {
+ if (rule instanceof CSSImportRule) {
+ if (rule.styleSheet) {
+ stylesheets.push(rule.styleSheet);
+ }
+ } else if (rule instanceof CSSStyleRule) {
+ if (element.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) {
+ rules.push(rule);
+ }
+ }
+ }
+ }
+
+ return rules;
+ }
+}
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index a57f6c9f085fe..473e662a37b64 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { getActiveDocument, getActiveWindow } from '../../../base/browser/dom.js';
+import { getActiveWindow } from '../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { CursorColumns } from '../../common/core/cursorColumns.js';
@@ -17,7 +17,6 @@ import type { ViewLineRenderingData } from '../../common/viewModel.js';
import type { ViewContext } from '../../common/viewModel/viewContext.js';
import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js';
import type { ITextureAtlasPageGlyph } from './atlas/atlas.js';
-import { CssRuleExtractor } from './cssRuleExtractor.js';
import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js';
import { BindingId, type IGpuRenderStrategy } from './gpu.js';
import { GPULifecycle } from './gpuDisposable.js';
@@ -48,7 +47,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
readonly wgsl: string = fullFileRenderStrategyWgsl;
private readonly _glyphRasterizer: GlyphRasterizer;
- private readonly _cssRuleExtractor: CssRuleExtractor;
private _cellBindBuffer!: GPUBuffer;
@@ -90,7 +88,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
const fontSize = this._context.configuration.options.get(EditorOption.fontSize);
this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily));
- this._cssRuleExtractor = this._register(new CssRuleExtractor(this._viewGpuContext));
const bufferSize = this._viewGpuContext.maxGpuLines * this._viewGpuContext.maxGpuCols * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT;
this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
@@ -291,7 +288,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {
// Only attempt to render lines that the GPU renderer can handle
- if (!ViewGpuContext.canRender(viewLineOptions, viewportData, y)) {
+ if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, viewLineOptions, viewportData, y)) {
fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell;
fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell;
cellBuffer.fill(0, fillStartIndex, fillEndIndex);
@@ -351,26 +348,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
}
return true;
});
+
+ // Only lines containing fully supported inline decorations should have made it
+ // this far.
for (const decoration of cellDecorations) {
- if (!decoration.options.inlineClassName) {
- throw new BugIndicatingError('Expected inlineClassName on decoration');
- }
- const rules = this._cssRuleExtractor.getStyleRules(decoration.options.inlineClassName);
- const supportedCssRules = [
- 'text-decoration-line',
- 'text-decoration-thickness',
- 'text-decoration-style',
- 'text-decoration-color',
- ];
- const supported = rules.every(rule => {
- for (const r of rule.style) {
- if (!supportedCssRules.includes(r)) {
- return false;
- }
- }
- return true;
- });
- console.log('rules supported?', supported, rules);
switch (decoration.options.inlineClassName) {
case (ClassName.EditorDeprecatedInlineDecoration): {
// HACK: We probably shouldn't override tokenMetadata
diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts
index 310a6f97f5d1e..70aff96eaaa33 100644
--- a/src/vs/editor/browser/gpu/viewGpuContext.ts
+++ b/src/vs/editor/browser/gpu/viewGpuContext.ts
@@ -19,7 +19,7 @@ import { GPULifecycle } from './gpuDisposable.js';
import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js';
import { RectangleRenderer } from './rectangleRenderer.js';
import type { ViewContext } from '../../common/viewModel/viewContext.js';
-import { ClassName } from '../../common/model/intervalTree.js';
+import { DecorationCssRuleExtractor } from './decorationCssRuleExtractor.js';
const enum GpuRenderLimits {
maxGpuLines = 3000,
@@ -48,6 +48,7 @@ export class ViewGpuContext extends Disposable {
private static _atlas: TextureAtlas | undefined;
+ private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor();
/**
* The shared texture atlas to use across all views.
@@ -126,25 +127,52 @@ export class ViewGpuContext extends Disposable {
* renderer. Eventually this should trend all lines, except maybe exceptional cases like
* decorations that use class names.
*/
- public static canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean {
+ public static canRender(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean {
const data = viewportData.getViewLineRenderingData(lineNumber);
+
+ // Check if the line has simple attributes that aren't supported
if (
data.containsRTL ||
data.maxColumn > GpuRenderLimits.maxGpuCols ||
data.continuesWithWrappedLine ||
- // HACK: ...
- data.inlineDecorations.length > 0 && data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration ||
lineNumber >= GpuRenderLimits.maxGpuLines
) {
return false;
}
+
+ // Check if all inline decorations are supported
+ if (data.inlineDecorations.length > 0) {
+ let supported = true;
+ for (const decoration of data.inlineDecorations) {
+ const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
+ const supportedCssRules = [
+ 'text-decoration-line',
+ 'text-decoration-thickness',
+ 'text-decoration-style',
+ 'text-decoration-color',
+ ];
+ supported &&= styleRules.every(rule => {
+ for (const r of rule.style) {
+ if (!supportedCssRules.includes(r)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ if (!supported) {
+ break;
+ }
+ }
+ return supported;
+ }
+
return true;
}
/**
* Like {@link canRender} but returned detailed information about why the line cannot be rendered.
*/
- public static canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] {
+ public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] {
const data = viewportData.getViewLineRenderingData(lineNumber);
const reasons: string[] = [];
if (data.containsRTL) {
@@ -157,9 +185,31 @@ export class ViewGpuContext extends Disposable {
reasons.push('continuesWithWrappedLine');
}
if (data.inlineDecorations.length > 0) {
- // HACK: ...
- if (data.inlineDecorations[0].inlineClassName !== ClassName.EditorDeprecatedInlineDecoration) {
- reasons.push('inlineDecorations > 0');
+ let supported = true;
+ const problemRules: string[] = [];
+ for (const decoration of data.inlineDecorations) {
+ const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
+ const supportedCssRules = [
+ 'text-decoration-line',
+ 'text-decoration-thickness',
+ 'text-decoration-style',
+ 'text-decoration-color',
+ ];
+ supported &&= styleRules.every(rule => {
+ for (const r of rule.style) {
+ if (!supportedCssRules.includes(r)) {
+ problemRules.push(r);
+ return false;
+ }
+ }
+ return true;
+ });
+ if (!supported) {
+ break;
+ }
+ }
+ if (problemRules.length > 0) {
+ reasons.push(`inlineDecorations with unsupported CSS rules (\`${problemRules.join(', ')}\`)`);
}
}
if (lineNumber >= GpuRenderLimits.maxGpuLines) {
diff --git a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts
index 7019eff5e007e..861ff529cdc47 100644
--- a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts
+++ b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
+import { getActiveDocument } from '../../../../base/browser/dom.js';
import * as viewEvents from '../../../common/viewEvents.js';
import { ViewContext } from '../../../common/viewModel/viewContext.js';
import { ViewGpuContext } from '../../gpu/viewGpuContext.js';
@@ -77,7 +78,8 @@ export class GpuMarkOverlay extends DynamicViewOverlay {
const output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
- const cannotRenderReasons = ViewGpuContext.canRenderDetailed(options, viewportData, lineNumber);
+ // TODO: How to get the container?
+ const cannotRenderReasons = ViewGpuContext.canRenderDetailed(getActiveDocument().querySelector('.view-lines')!, options, viewportData, lineNumber);
output[lineIndex] = cannotRenderReasons.length ? `
` : '';
}
diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts
index 5e555246cea86..d25238bb4cf54 100644
--- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts
+++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts
@@ -19,6 +19,7 @@ import { EditorFontLigatures } from '../../../common/config/editorOptions.js';
import { DomReadingContext } from './domReadingContext.js';
import type { ViewLineOptions } from './viewLineOptions.js';
import { ViewGpuContext } from '../../gpu/viewGpuContext.js';
+import { getActiveDocument } from '../../../../base/browser/dom.js';
const canUseFastRenderedViewLine = (function () {
if (platform.isNative) {
@@ -98,7 +99,8 @@ export class ViewLine implements IVisibleLine {
}
public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean {
- if (this._options.useGpu && ViewGpuContext.canRender(this._options, viewportData, lineNumber)) {
+ // TODO: How to get the container?
+ if (this._options.useGpu && ViewGpuContext.canRender(getActiveDocument().querySelector('.view-lines')!, this._options, viewportData, lineNumber)) {
this._renderedViewLine?.domNode?.domNode.remove();
this._renderedViewLine = null;
return false;
diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts
index c4d8c0400d214..cfe4a974bd78f 100644
--- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts
+++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts
@@ -551,7 +551,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
if (!this._lastViewportData || !this._lastViewLineOptions) {
return undefined;
}
- if (!ViewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
+ if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
return undefined;
}
@@ -569,7 +569,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
if (!this._lastViewportData || !this._lastViewLineOptions) {
return undefined;
}
- if (!ViewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
+ if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
return undefined;
}
const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber);
From f536f763b2ac08658fd0c7473325e42710083776 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Tue, 19 Nov 2024 12:33:24 -0800
Subject: [PATCH 04/20] Apply CSS styles in gpu lines
---
.../browser/gpu/fullFileRenderStrategy.ts | 32 ++++++++++++++++---
src/vs/editor/browser/gpu/viewGpuContext.ts | 31 +++++++++---------
2 files changed, 42 insertions(+), 21 deletions(-)
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index 473e662a37b64..2969cbe23ea53 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -8,7 +8,6 @@ import { BugIndicatingError } from '../../../base/common/errors.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { CursorColumns } from '../../common/core/cursorColumns.js';
import { MetadataConsts } from '../../common/encodedTokenAttributes.js';
-import { ClassName } from '../../common/model/intervalTree.js';
import type { IViewLineTokens } from '../../common/tokens/lineTokens.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js';
@@ -351,14 +350,37 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
// Only lines containing fully supported inline decorations should have made it
// this far.
+ const inlineStyles: Map = new Map();
for (const decoration of cellDecorations) {
- switch (decoration.options.inlineClassName) {
- case (ClassName.EditorDeprecatedInlineDecoration): {
- // HACK: We probably shouldn't override tokenMetadata
+ if (!decoration.options.inlineClassName) {
+ throw new BugIndicatingError('Unexpected inline decoration without class name');
+ }
+ const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.options.inlineClassName);
+ for (const rule of rules) {
+ for (const r of rule.style) {
+ inlineStyles.set(r, rule.styleMap.get(r)?.toString() ?? '');
+ }
+ }
+ }
+
+ for (const [k, v] of inlineStyles.entries()) {
+ switch (k) {
+ case 'text-decoration-line': {
+ // TODO: Don't set tokenMetadata as it applies to more than just this token
tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK;
- // chars = '-';
break;
}
+ case 'text-decoration-thickness':
+ case 'text-decoration-style':
+ case 'text-decoration-color': {
+ // HACK: Ignore for now to avoid throwing
+ break;
+ }
+ // case 'color': {
+ // tokenMetadata |= ...
+ // break;
+ // }
+ default: throw new BugIndicatingError('Unexpected inline decoration style');
}
}
diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts
index 70aff96eaaa33..ae4264cb24438 100644
--- a/src/vs/editor/browser/gpu/viewGpuContext.ts
+++ b/src/vs/editor/browser/gpu/viewGpuContext.ts
@@ -46,9 +46,12 @@ export class ViewGpuContext extends Disposable {
readonly rectangleRenderer: RectangleRenderer;
- private static _atlas: TextureAtlas | undefined;
-
private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor();
+ static get decorationCssRuleExtractor(): DecorationCssRuleExtractor {
+ return ViewGpuContext._decorationCssRuleExtractor;
+ }
+
+ private static _atlas: TextureAtlas | undefined;
/**
* The shared texture atlas to use across all views.
@@ -145,15 +148,9 @@ export class ViewGpuContext extends Disposable {
let supported = true;
for (const decoration of data.inlineDecorations) {
const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
- const supportedCssRules = [
- 'text-decoration-line',
- 'text-decoration-thickness',
- 'text-decoration-style',
- 'text-decoration-color',
- ];
supported &&= styleRules.every(rule => {
for (const r of rule.style) {
- if (!supportedCssRules.includes(r)) {
+ if (!gpuSupportedCssRules.includes(r)) {
return false;
}
}
@@ -189,15 +186,9 @@ export class ViewGpuContext extends Disposable {
const problemRules: string[] = [];
for (const decoration of data.inlineDecorations) {
const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
- const supportedCssRules = [
- 'text-decoration-line',
- 'text-decoration-thickness',
- 'text-decoration-style',
- 'text-decoration-color',
- ];
supported &&= styleRules.every(rule => {
for (const r of rule.style) {
- if (!supportedCssRules.includes(r)) {
+ if (!gpuSupportedCssRules.includes(r)) {
problemRules.push(r);
return false;
}
@@ -218,3 +209,11 @@ export class ViewGpuContext extends Disposable {
return reasons;
}
}
+
+const gpuSupportedCssRules = [
+ // 'color',
+ 'text-decoration-line',
+ 'text-decoration-thickness',
+ 'text-decoration-style',
+ 'text-decoration-color',
+];
From ee5b89187745d866cf849ff5d2e78e1bb39b5b72 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Tue, 19 Nov 2024 13:56:50 -0800
Subject: [PATCH 05/20] Apply style to char metadata, not token
---
.../browser/gpu/fullFileRenderStrategy.ts | 22 +++++++++++--------
src/vs/editor/browser/gpu/viewGpuContext.ts | 10 ++++-----
2 files changed, 18 insertions(+), 14 deletions(-)
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index 2969cbe23ea53..d9f053c3a1a44 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -225,6 +225,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
let tokenEndIndex = 0;
let tokenMetadata = 0;
+ let charMetadata = 0;
+
let lineData: ViewLineRenderingData;
let content: string = '';
let fillStartIndex = 0;
@@ -331,6 +333,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
break;
}
chars = content.charAt(x);
+ charMetadata = tokenMetadata;
// TODO: We'd want to optimize pulling the decorations in order
// HACK: Temporary replace char to demonstrate inline decorations
@@ -363,11 +366,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
}
}
- for (const [k, v] of inlineStyles.entries()) {
- switch (k) {
+ for (const [key, _value] of inlineStyles.entries()) {
+ switch (key) {
case 'text-decoration-line': {
- // TODO: Don't set tokenMetadata as it applies to more than just this token
- tokenMetadata |= MetadataConsts.STRIKETHROUGH_MASK;
+ charMetadata |= MetadataConsts.STRIKETHROUGH_MASK;
break;
}
case 'text-decoration-thickness':
@@ -376,10 +378,12 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
// HACK: Ignore for now to avoid throwing
break;
}
- // case 'color': {
- // tokenMetadata |= ...
- // break;
- // }
+ case 'color': {
+ // HACK: Set color requests to the first token's fg color
+ charMetadata &= ~MetadataConsts.FOREGROUND_MASK;
+ charMetadata |= 0b1 << MetadataConsts.FOREGROUND_OFFSET;
+ break;
+ }
default: throw new BugIndicatingError('Unexpected inline decoration style');
}
}
@@ -395,7 +399,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
continue;
}
- glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata);
+ glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, charMetadata);
// TODO: Support non-standard character widths
absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr);
diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts
index ae4264cb24438..162139b3cb82b 100644
--- a/src/vs/editor/browser/gpu/viewGpuContext.ts
+++ b/src/vs/editor/browser/gpu/viewGpuContext.ts
@@ -211,9 +211,9 @@ export class ViewGpuContext extends Disposable {
}
const gpuSupportedCssRules = [
- // 'color',
- 'text-decoration-line',
- 'text-decoration-thickness',
- 'text-decoration-style',
- 'text-decoration-color',
+ 'color',
+ // 'text-decoration-line',
+ // 'text-decoration-thickness',
+ // 'text-decoration-style',
+ // 'text-decoration-color',
];
From adaa9f2c51f44bc71372d0d258f1268b20b1d01f Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Wed, 20 Nov 2024 07:16:16 -0800
Subject: [PATCH 06/20] Start of gpu character metadata concept
---
src/vs/editor/browser/gpu/atlas/atlas.ts | 4 +--
.../editor/browser/gpu/atlas/textureAtlas.ts | 34 +++++++++----------
.../browser/gpu/atlas/textureAtlasPage.ts | 16 ++++-----
.../browser/gpu/fullFileRenderStrategy.ts | 22 ++++++++----
.../browser/gpu/raster/glyphRasterizer.ts | 19 +++++++----
src/vs/editor/browser/gpu/raster/raster.ts | 16 +++++++--
src/vs/editor/browser/gpu/viewGpuContext.ts | 1 +
7 files changed, 70 insertions(+), 42 deletions(-)
diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts
index e971904270525..a8a2fcee9aac1 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 { ThreeKeyMap } from '../../../../base/common/map.js';
+import type { FourKeyMap } from '../../../../base/common/map.js';
import type { IBoundingBox, IRasterizedGlyph } from '../raster/raster.js';
/**
@@ -92,4 +92,4 @@ export const enum UsagePreviewColors {
Restricted = '#FF000088',
}
-export type GlyphMap = ThreeKeyMap*chars*/string, /*metadata*/number, /*rasterizerCacheKey*/string, T>;
+export type GlyphMap = FourKeyMap*chars*/string, /*tokenMetadata*/number, /*charMetadata*/number, /*rasterizerCacheKey*/string, T>;
diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts
index fb9ac44f4a8ec..2d4365da93bbb 100644
--- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts
+++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts
@@ -8,7 +8,7 @@ import { CharCode } from '../../../../base/common/charCode.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
-import { ThreeKeyMap } from '../../../../base/common/map.js';
+import { FourKeyMap } 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';
@@ -44,7 +44,7 @@ export class TextureAtlas extends Disposable {
* 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: GlyphMap = new ThreeKeyMap();
+ private readonly _glyphPageIndex: GlyphMap = new FourKeyMap();
private readonly _onDidDeleteGlyphs = this._register(new Emitter());
readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event;
@@ -83,7 +83,7 @@ export class TextureAtlas extends Disposable {
// cells end up rendering nothing
// TODO: This currently means the first slab is for 0x0 glyphs and is wasted
const nullRasterizer = new GlyphRasterizer(1, '');
- firstPage.getGlyph(nullRasterizer, '', 0);
+ firstPage.getGlyph(nullRasterizer, '', 0, 0);
nullRasterizer.dispose();
}
@@ -104,10 +104,10 @@ export class TextureAtlas extends Disposable {
this._onDidDeleteGlyphs.fire();
}
- getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly {
+ getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: 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);
+ tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK);
// Warm up common glyphs
if (!this._warmedUpRasterizers.has(rasterizer.id)) {
@@ -116,25 +116,25 @@ export class TextureAtlas extends Disposable {
}
// Try get the glyph, overflowing to a new page if necessary
- return this._tryGetGlyph(this._glyphPageIndex.get(chars, metadata, rasterizer.cacheKey) ?? 0, rasterizer, chars, metadata);
+ return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, charMetadata);
}
- private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly {
- this._glyphPageIndex.set(chars, metadata, rasterizer.cacheKey, pageIndex);
+ private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly {
+ this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, pageIndex);
return (
- this._pages[pageIndex].getGlyph(rasterizer, chars, metadata)
+ this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, charMetadata)
?? (pageIndex + 1 < this._pages.length
- ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, metadata)
+ ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, charMetadata)
: undefined)
- ?? this._getGlyphFromNewPage(rasterizer, chars, metadata)
+ ?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, charMetadata)
);
}
- private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly {
+ private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: 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, rasterizer.cacheKey, this._pages.length - 1);
- return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, metadata)!;
+ this._glyphPageIndex.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, this._pages.length - 1);
+ return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, charMetadata)!;
}
public getUsagePreview(): Promise {
@@ -161,7 +161,7 @@ export class TextureAtlas extends Disposable {
for (let code = CharCode.A; code <= CharCode.Z; code++) {
taskQueue.enqueue(() => {
for (const fgColor of colorMap.keys()) {
- this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK);
+ this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0);
}
});
}
@@ -169,7 +169,7 @@ export class TextureAtlas extends Disposable {
for (let code = CharCode.a; code <= CharCode.z; code++) {
taskQueue.enqueue(() => {
for (const fgColor of colorMap.keys()) {
- this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK);
+ this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0);
}
});
}
@@ -177,7 +177,7 @@ export class TextureAtlas extends Disposable {
for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) {
taskQueue.enqueue(() => {
for (const fgColor of colorMap.keys()) {
- this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK);
+ this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0);
}
});
}
diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts
index 01edf66913051..cbda9352a3bc8 100644
--- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts
+++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
-import { ThreeKeyMap } from '../../../../base/common/map.js';
+import { FourKeyMap } 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';
@@ -31,7 +31,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla
private readonly _canvas: OffscreenCanvas;
get source(): OffscreenCanvas { return this._canvas; }
- private readonly _glyphMap: GlyphMap = new ThreeKeyMap();
+ private readonly _glyphMap: GlyphMap = new FourKeyMap();
private readonly _glyphInOrderSet: Set = new Set();
get glyphs(): IterableIterator {
return this._glyphInOrderSet.values();
@@ -65,20 +65,20 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla
}));
}
- public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined {
+ public getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: number): Readonly | undefined {
// 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, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, metadata);
+ return this._glyphMap.get(chars, tokenMetadata, charMetadata, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, tokenMetadata, charMetadata);
}
- private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined {
+ private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, charMetadata: 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 rasterizedGlyph = rasterizer.rasterizeGlyph(chars, tokenMetadata, charMetadata, this._colorMap);
const glyph = this._allocator.allocate(rasterizedGlyph);
// Ensure the glyph was allocated
@@ -89,7 +89,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla
}
// Save the glyph
- this._glyphMap.set(chars, metadata, rasterizer.cacheKey, glyph);
+ this._glyphMap.set(chars, tokenMetadata, charMetadata, rasterizer.cacheKey, glyph);
this._glyphInOrderSet.add(glyph);
// Update page version and it's tracked used area
@@ -100,7 +100,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla
if (this._logService.getLevel() === LogLevel.Trace) {
this._logService.trace('New glyph', {
chars,
- metadata,
+ metadata: tokenMetadata,
rasterizedGlyph,
glyph
});
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index d9f053c3a1a44..80bfb679b23a7 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -22,6 +22,7 @@ import { GPULifecycle } from './gpuDisposable.js';
import { quadVertices } from './gpuUtils.js';
import { GlyphRasterizer } from './raster/glyphRasterizer.js';
import { ViewGpuContext } from './viewGpuContext.js';
+import { GpuCharMetadata } from './raster/raster.js';
const enum Constants {
@@ -333,7 +334,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
break;
}
chars = content.charAt(x);
- charMetadata = tokenMetadata;
+ charMetadata = 0;
// TODO: We'd want to optimize pulling the decorations in order
// HACK: Temporary replace char to demonstrate inline decorations
@@ -366,7 +367,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
}
}
- for (const [key, _value] of inlineStyles.entries()) {
+ for (const [key, value] of inlineStyles.entries()) {
switch (key) {
case 'text-decoration-line': {
charMetadata |= MetadataConsts.STRIKETHROUGH_MASK;
@@ -379,9 +380,18 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
break;
}
case 'color': {
- // HACK: Set color requests to the first token's fg color
- charMetadata &= ~MetadataConsts.FOREGROUND_MASK;
- charMetadata |= 0b1 << MetadataConsts.FOREGROUND_OFFSET;
+ // TODO: Move to color.ts and make more generic
+ function parseRgb(text: string): number {
+ const color = text.match(/rgb\((\d+), (\d+), (\d+)\)/);
+ if (!color) {
+ throw new Error('Invalid color format');
+ }
+ const r = parseInt(color[1], 10);
+ const g = parseInt(color[2], 10);
+ const b = parseInt(color[3], 10);
+ return r << 16 | g << 8 | b;
+ }
+ charMetadata = ((parseRgb(value) << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0;
break;
}
default: throw new BugIndicatingError('Unexpected inline decoration style');
@@ -399,7 +409,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
continue;
}
- glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, charMetadata);
+ glyph = this._viewGpuContext.atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata, charMetadata);
// TODO: Support non-standard character widths
absoluteOffsetX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr);
diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
index 07d343bb677c4..0c1c5db102e2c 100644
--- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
+++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
@@ -9,7 +9,7 @@ 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';
+import { GpuCharMetadata, type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js';
let nextId = 0;
@@ -61,7 +61,8 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
*/
public rasterizeGlyph(
chars: string,
- metadata: number,
+ tokenMetadata: number,
+ charMetadata: number,
colorMap: string[],
): Readonly {
if (chars === '') {
@@ -74,17 +75,18 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
// 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) {
+ if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === tokenMetadata) {
return this._workGlyph;
}
this._workGlyphConfig.chars = chars;
- this._workGlyphConfig.metadata = metadata;
- return this._rasterizeGlyph(chars, metadata, colorMap);
+ this._workGlyphConfig.metadata = tokenMetadata;
+ return this._rasterizeGlyph(chars, tokenMetadata, charMetadata, colorMap);
}
public _rasterizeGlyph(
chars: string,
metadata: number,
+ charMetadata: number,
colorMap: string[],
): Readonly {
const devicePixelFontSize = Math.ceil(this._fontSize * getActiveWindow().devicePixelRatio);
@@ -114,7 +116,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
const originX = devicePixelFontSize;
const originY = devicePixelFontSize;
- this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)];
+ if (charMetadata) {
+ const fg = (charMetadata & GpuCharMetadata.FOREGROUND_MASK) >> GpuCharMetadata.FOREGROUND_OFFSET;
+ this._ctx.fillStyle = `#${fg.toString(16).padStart(6, '0')}`;
+ } else {
+ this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)];
+ }
// TODO: This might actually be slower
// const textMetrics = this._ctx.measureText(chars);
this._ctx.textBaseline = 'top';
diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts
index 6eb41e680b299..d83b55665dbf5 100644
--- a/src/vs/editor/browser/gpu/raster/raster.ts
+++ b/src/vs/editor/browser/gpu/raster/raster.ts
@@ -21,13 +21,15 @@ export interface IGlyphRasterizer {
* 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 tokenMetadata The token metadata of the glyph to rasterize. See {@link MetadataConsts}
+ * for how this works.
+ * @param charMetadata The chracter metadata of the glyph to rasterize.
* @param colorMap A theme's color map.
*/
rasterizeGlyph(
chars: string,
- metadata: number,
+ tokenMetadata: number,
+ charMetadata: number,
colorMap: string[],
): Readonly;
}
@@ -63,3 +65,11 @@ export interface IRasterizedGlyph {
*/
originOffset: { x: number; y: number };
}
+
+export const enum GpuCharMetadata {
+ FOREGROUND_MASK /* */ = 0b00000000_11111111_11111111_11111111,
+ OPACITY_MASK /* */ = 0b11111111_00000000_00000000_00000000,
+
+ FOREGROUND_OFFSET = 0,
+ OPACITY_OFFSET = 24,
+}
diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts
index 162139b3cb82b..fdd4aa9f5efcf 100644
--- a/src/vs/editor/browser/gpu/viewGpuContext.ts
+++ b/src/vs/editor/browser/gpu/viewGpuContext.ts
@@ -150,6 +150,7 @@ export class ViewGpuContext extends Disposable {
const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
supported &&= styleRules.every(rule => {
for (const r of rule.style) {
+ // TODO: Consider pseudo classes when checking for support
if (!gpuSupportedCssRules.includes(r)) {
return false;
}
From d3c1d2e831afd9ab4636bb0ee2cebe9fd60bf990 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Wed, 20 Nov 2024 09:55:57 -0800
Subject: [PATCH 07/20] Basic handling of alpha channel, ignore for now
---
src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 7 ++-----
src/vs/editor/contrib/gpu/browser/gpuActions.ts | 7 ++++---
.../test/browser/view/gpu/atlas/textureAtlas.test.ts | 7 ++++---
3 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index 80bfb679b23a7..e3de4735d31fc 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -308,9 +308,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
e.range.startLineNumber <= y && e.range.endLineNumber >= y &&
e.options.inlineClassName
));
- if (inlineDecorations.length > 0) {
- console.log('decoration!', inlineDecorations);
- }
lineData = viewportData.getViewLineRenderingData(y);
content = lineData.content;
@@ -382,9 +379,9 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
case 'color': {
// TODO: Move to color.ts and make more generic
function parseRgb(text: string): number {
- const color = text.match(/rgb\((\d+), (\d+), (\d+)\)/);
+ const color = text.match(/rgba?\((\d+), ?(\d+), ?(\d+)(?:, ?\d+(?:.\d+)?)?\)/);
if (!color) {
- throw new Error('Invalid color format');
+ throw new Error('Invalid color format ' + text);
}
const r = parseInt(color[1], 10);
const g = parseInt(color[2], 10);
diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts
index 29de263652703..f64ac1c738047 100644
--- a/src/vs/editor/contrib/gpu/browser/gpuActions.ts
+++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts
@@ -115,8 +115,9 @@ class DebugEditorGpuRendererAction extends EditorAction {
if (codePoint !== undefined) {
chars = String.fromCodePoint(parseInt(codePoint, 16));
}
- const metadata = 0;
- const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, metadata);
+ const tokenMetadata = 0;
+ const charMetadata = 0;
+ const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata);
if (!rasterizedGlyph) {
return;
}
@@ -133,7 +134,7 @@ class DebugEditorGpuRendererAction 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}_${tokenMetadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`);
await fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer())));
});
break;
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 5a4353a1d19b4..3445beee97794 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
@@ -14,15 +14,16 @@ import { assertIsValidGlyph } from './testUtil.js';
import { TextureAtlasSlabAllocator } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js';
const blackInt = 0x000000FF;
+const nullCharMetadata = 0x0;
let lastUniqueGlyph: string | undefined;
-function getUniqueGlyphId(): [chars: string, tokenFg: number] {
+function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number] {
if (!lastUniqueGlyph) {
lastUniqueGlyph = 'a';
} else {
lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1);
}
- return [lastUniqueGlyph, blackInt];
+ return [lastUniqueGlyph, blackInt, nullCharMetadata];
}
class TestGlyphRasterizer implements IGlyphRasterizer {
@@ -30,7 +31,7 @@ class TestGlyphRasterizer implements IGlyphRasterizer {
readonly cacheKey = '';
nextGlyphColor: [number, number, number, number] = [0, 0, 0, 0];
nextGlyphDimensions: [number, number] = [0, 0];
- rasterizeGlyph(chars: string, metadata: number, colorMap: string[]): Readonly {
+ rasterizeGlyph(chars: string, tokenMetadata: number, charMetadata: number, colorMap: string[]): Readonly {
const w = this.nextGlyphDimensions[0];
const h = this.nextGlyphDimensions[1];
if (w === 0 || h === 0) {
From 37e6cb3300061f0b7481011e60a459e917b6dd5a Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Wed, 20 Nov 2024 10:09:52 -0800
Subject: [PATCH 08/20] Clean up DecorationCssRulerExtractor DOM nodes
This was causing mouse event request paths to get messed up
---
src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
index 5959bd36cd3e6..d76761e4c1ec5 100644
--- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
+++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
@@ -25,14 +25,18 @@ export class DecorationCssRuleExtractor extends Disposable {
}
const dummyElement = $(`span.${decorationClassName}`);
this._container.appendChild(dummyElement);
+ canvas.appendChild(this._container);
+
const rules = this._getStyleRules(canvas, dummyElement, decorationClassName);
this._ruleCache.set(decorationClassName, rules);
+
+ canvas.removeChild(this._container);
+ this._container.removeChild(dummyElement);
+
return rules;
}
private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) {
- canvas.appendChild(this._container);
-
// Iterate through all stylesheets and imported stylesheets to find matching rules
const rules = [];
const doc = getActiveDocument();
From 1f900684f6304d1b1e50d8f7f6d2113a29aa152d Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Wed, 20 Nov 2024 10:22:48 -0800
Subject: [PATCH 09/20] Move CSS out and improve lifecycle of
DecorationCssRuleExtractor
---
.../browser/gpu/decorationCssRuleExtractor.ts | 28 +++++++++++++------
.../gpu/media/decorationCssRuleExtractor.css | 9 ++++++
2 files changed, 29 insertions(+), 8 deletions(-)
create mode 100644 src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css
diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
index d76761e4c1ec5..27b52a3e54381 100644
--- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
+++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
@@ -5,38 +5,50 @@
import { $, getActiveDocument } from '../../../base/browser/dom.js';
import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
+import './media/decorationCssRuleExtractor.css';
+/**
+ * Extracts CSS rules that would be applied to certain decoration classes.
+ */
export class DecorationCssRuleExtractor extends Disposable {
private _container: HTMLElement;
+ private _dummyElement: HTMLSpanElement;
private _ruleCache: Map* className */string, CSSStyleRule[]> = new Map();
constructor() {
super();
- this._container = $('div.monaco-css-rule-extractor');
- this._container.style.visibility = 'hidden';
+
+ this._container = $('div.monaco-decoration-css-rule-extractor');
+ this._dummyElement = $('span');
+ this._container.appendChild(this._dummyElement);
+
this._register(toDisposable(() => this._container.remove()));
}
getStyleRules(canvas: HTMLElement, decorationClassName: string): CSSStyleRule[] {
+ // Check cache
const existing = this._ruleCache.get(decorationClassName);
if (existing) {
return existing;
}
- const dummyElement = $(`span.${decorationClassName}`);
- this._container.appendChild(dummyElement);
+
+ // Set up DOM
+ this._dummyElement.classList.add(decorationClassName);
canvas.appendChild(this._container);
- const rules = this._getStyleRules(canvas, dummyElement, decorationClassName);
+ // Get rules
+ const rules = this._getStyleRules(decorationClassName);
this._ruleCache.set(decorationClassName, rules);
+ // Tear down DOM
canvas.removeChild(this._container);
- this._container.removeChild(dummyElement);
+ this._dummyElement.classList.remove(decorationClassName);
return rules;
}
- private _getStyleRules(canvas: HTMLElement, element: HTMLElement, className: string) {
+ private _getStyleRules(className: string) {
// Iterate through all stylesheets and imported stylesheets to find matching rules
const rules = [];
const doc = getActiveDocument();
@@ -49,7 +61,7 @@ export class DecorationCssRuleExtractor extends Disposable {
stylesheets.push(rule.styleSheet);
}
} else if (rule instanceof CSSStyleRule) {
- if (element.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) {
+ if (this._dummyElement.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) {
rules.push(rule);
}
}
diff --git a/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css b/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css
new file mode 100644
index 0000000000000..900154c64fdf8
--- /dev/null
+++ b/src/vs/editor/browser/gpu/media/decorationCssRuleExtractor.css
@@ -0,0 +1,9 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+.monaco-editor .monaco-decoration-css-rule-extractor {
+ visibility: hidden;
+ pointer-events: none;
+}
From 3489890a810a8aee40ac77c31dc913b30a64e2ed Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Wed, 20 Nov 2024 10:24:57 -0800
Subject: [PATCH 10/20] Move gpu/ test folder into correct new place
---
.../test/browser/{view => }/gpu/atlas/testUtil.ts | 8 ++++----
.../{view => }/gpu/atlas/textureAtlas.test.ts | 14 +++++++-------
.../gpu/atlas/textureAtlasAllocator.test.ts | 14 +++++++-------
.../{view => }/gpu/bufferDirtyTracker.test.ts | 4 ++--
.../{view => }/gpu/objectCollectionBuffer.test.ts | 4 ++--
5 files changed, 22 insertions(+), 22 deletions(-)
rename src/vs/editor/test/browser/{view => }/gpu/atlas/testUtil.ts (86%)
rename src/vs/editor/test/browser/{view => }/gpu/atlas/textureAtlas.test.ts (90%)
rename src/vs/editor/test/browser/{view => }/gpu/atlas/textureAtlasAllocator.test.ts (92%)
rename src/vs/editor/test/browser/{view => }/gpu/bufferDirtyTracker.test.ts (91%)
rename src/vs/editor/test/browser/{view => }/gpu/objectCollectionBuffer.test.ts (97%)
diff --git a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts b/src/vs/editor/test/browser/gpu/atlas/testUtil.ts
similarity index 86%
rename from src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts
rename to src/vs/editor/test/browser/gpu/atlas/testUtil.ts
index 73d1e167f1e15..ddc3f1583a064 100644
--- a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts
+++ b/src/vs/editor/test/browser/gpu/atlas/testUtil.ts
@@ -4,10 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { fail, ok } from 'assert';
-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';
+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/gpu/atlas/textureAtlas.test.ts
similarity index 90%
rename from src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts
rename to src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts
index 3445beee97794..0610f84eb3e5e 100644
--- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts
+++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts
@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { strictEqual, throws } from 'assert';
-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 { 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';
+import { TextureAtlasSlabAllocator } from '../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js';
const blackInt = 0x000000FF;
const nullCharMetadata = 0x0;
diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts
similarity index 92%
rename from src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts
rename to src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts
index 92d354a5f789d..377bb752df8a0 100644
--- a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts
+++ b/src/vs/editor/test/browser/gpu/atlas/textureAtlasAllocator.test.ts
@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual, strictEqual, throws } from 'assert';
-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 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';
-import { BugIndicatingError } from '../../../../../../base/common/errors.js';
+import { BugIndicatingError } from '../../../../../base/common/errors.js';
const blackArr = [0x00, 0x00, 0x00, 0xFF];
diff --git a/src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts b/src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts
similarity index 91%
rename from src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts
rename to src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts
index 0ddc7a5befeff..0794961644e0a 100644
--- a/src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts
+++ b/src/vs/editor/test/browser/gpu/bufferDirtyTracker.test.ts
@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { strictEqual } from 'assert';
-import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
-import { BufferDirtyTracker } from '../../../../browser/gpu/bufferDirtyTracker.js';
+import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
+import { BufferDirtyTracker } from '../../../browser/gpu/bufferDirtyTracker.js';
suite('BufferDirtyTracker', () => {
ensureNoDisposablesAreLeakedInTestSuite();
diff --git a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts b/src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts
similarity index 97%
rename from src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts
rename to src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts
index ae8c0670d2b8b..52ca0b7619f39 100644
--- a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts
+++ b/src/vs/editor/test/browser/gpu/objectCollectionBuffer.test.ts
@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual, strictEqual } from 'assert';
-import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
-import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from '../../../../browser/gpu/objectCollectionBuffer.js';
+import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
+import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from '../../../browser/gpu/objectCollectionBuffer.js';
suite('ObjectCollectionBuffer', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();
From 01e1c7c01fb267d5b0a95688dd6b092afe839fcf Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Wed, 20 Nov 2024 10:46:08 -0800
Subject: [PATCH 11/20] Add unit tests for DecorationCssRuleExtractor
---
.../browser/gpu/decorationCssRuleExtractor.ts | 5 +-
.../gpu/decorationCssRulerExtractor.test.ts | 83 +++++++++++++++++++
2 files changed, 87 insertions(+), 1 deletion(-)
create mode 100644 src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts
diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
index 27b52a3e54381..c05f2d63418d4 100644
--- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
+++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
@@ -61,7 +61,10 @@ export class DecorationCssRuleExtractor extends Disposable {
stylesheets.push(rule.styleSheet);
}
} else if (rule instanceof CSSStyleRule) {
- if (this._dummyElement.matches(rule.selectorText) && rule.selectorText.includes(`.${className}`)) {
+ // Note that originally `.matches(rule.selectorText)` was used but this would
+ // not pick up pseudo-classes which are important to determine support of the
+ // returned styles.
+ if (rule.selectorText.includes(`.${className}`)) {
rules.push(rule);
}
}
diff --git a/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts
new file mode 100644
index 0000000000000..ddeffcdb82909
--- /dev/null
+++ b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts
@@ -0,0 +1,83 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { deepStrictEqual } from 'assert';
+import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
+import { DecorationCssRuleExtractor } from '../../../browser/gpu/decorationCssRuleExtractor.js';
+import { generateUuid } from '../../../../base/common/uuid.js';
+import { $, getActiveDocument } from '../../../../base/browser/dom.js';
+
+function randomClass(): string {
+ return 'test-class-' + generateUuid();
+}
+
+suite('DecorationCssRulerExtractor', () => {
+ const store = ensureNoDisposablesAreLeakedInTestSuite();
+
+ let doc: Document;
+ let container: HTMLElement;
+ let extractor: DecorationCssRuleExtractor;
+ let testClassName: string;
+
+ function addStyleElement(content: string): void {
+ const styleElement = $('style');
+ styleElement.textContent = content;
+ container.append(styleElement);
+ }
+
+ function assertStyles(className: string, expectedCssText: string[]): void {
+ deepStrictEqual(extractor.getStyleRules(container, className).map(e => e.cssText), expectedCssText);
+ }
+
+ setup(() => {
+ doc = getActiveDocument();
+ extractor = store.add(new DecorationCssRuleExtractor());
+ testClassName = randomClass();
+ container = $('div');
+ doc.body.append(container);
+ });
+
+ teardown(() => {
+ container.remove();
+ });
+
+ test('unknown class should give no styles', () => {
+ assertStyles(randomClass(), []);
+ });
+
+ test('single style should be picked up', () => {
+ addStyleElement(`.${testClassName} { color: red; }`);
+ assertStyles(testClassName, [
+ `.${testClassName} { color: red; }`
+ ]);
+ });
+
+ test('multiple styles from the same selector should be picked up', () => {
+ addStyleElement(`.${testClassName} { color: red; opacity: 0.5; }`);
+ assertStyles(testClassName, [
+ `.${testClassName} { color: red; opacity: 0.5; }`
+ ]);
+ });
+
+ test('multiple styles from different selectors should be picked up', () => {
+ addStyleElement([
+ `.${testClassName} { color: red; opacity: 0.5; }`,
+ `.${testClassName}:hover { opacity: 1; }`,
+ ].join('\n'));
+ assertStyles(testClassName, [
+ `.${testClassName} { color: red; opacity: 0.5; }`,
+ `.${testClassName}:hover { opacity: 1; }`,
+ ]);
+ });
+
+ test('multiple styles from the different stylesheets should be picked up', () => {
+ addStyleElement(`.${testClassName} { color: red; opacity: 0.5; }`);
+ addStyleElement(`.${testClassName}:hover { opacity: 1; }`);
+ assertStyles(testClassName, [
+ `.${testClassName} { color: red; opacity: 0.5; }`,
+ `.${testClassName}:hover { opacity: 1; }`,
+ ]);
+ });
+});
From 46abc8b541003676268bf477e07576a50d0ad271 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Wed, 20 Nov 2024 10:54:40 -0800
Subject: [PATCH 12/20] Don't support lines with pseudo classes
---
src/vs/editor/browser/gpu/viewGpuContext.ts | 29 ++++++++++++++-------
1 file changed, 20 insertions(+), 9 deletions(-)
diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts
index 2da2c770ffca6..883c75d75bdd6 100644
--- a/src/vs/editor/browser/gpu/viewGpuContext.ts
+++ b/src/vs/editor/browser/gpu/viewGpuContext.ts
@@ -160,9 +160,12 @@ export class ViewGpuContext extends Disposable {
for (const decoration of data.inlineDecorations) {
const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
supported &&= styleRules.every(rule => {
+ // Pseudo classes aren't supported currently
+ if (rule.selectorText.includes(':')) {
+ return false;
+ }
for (const r of rule.style) {
- // TODO: Consider pseudo classes when checking for support
- if (!gpuSupportedCssRules.includes(r)) {
+ if (!gpuSupportedDecorationCssRules.includes(r)) {
return false;
}
}
@@ -179,7 +182,7 @@ export class ViewGpuContext extends Disposable {
}
/**
- * Like {@link canRender} but returned detailed information about why the line cannot be rendered.
+ * Like {@link canRender} but returns detailed information about why the line cannot be rendered.
*/
public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] {
const data = viewportData.getViewLineRenderingData(lineNumber);
@@ -195,12 +198,18 @@ export class ViewGpuContext extends Disposable {
}
if (data.inlineDecorations.length > 0) {
let supported = true;
+ const problemSelectors: string[] = [];
const problemRules: string[] = [];
for (const decoration of data.inlineDecorations) {
const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
supported &&= styleRules.every(rule => {
+ // Pseudo classes aren't supported currently
+ if (rule.selectorText.includes(':')) {
+ problemSelectors.push(rule.selectorText);
+ return false;
+ }
for (const r of rule.style) {
- if (!gpuSupportedCssRules.includes(r)) {
+ if (!gpuSupportedDecorationCssRules.includes(r)) {
problemRules.push(r);
return false;
}
@@ -214,6 +223,9 @@ export class ViewGpuContext extends Disposable {
if (problemRules.length > 0) {
reasons.push(`inlineDecorations with unsupported CSS rules (\`${problemRules.join(', ')}\`)`);
}
+ if (problemSelectors.length > 0) {
+ reasons.push(`inlineDecorations with unsupported CSS selectors (\`${problemSelectors.join(', ')}\`)`);
+ }
}
if (lineNumber >= GpuRenderLimits.maxGpuLines) {
reasons.push('lineNumber >= maxGpuLines');
@@ -222,10 +234,9 @@ export class ViewGpuContext extends Disposable {
}
}
-const gpuSupportedCssRules = [
+/**
+ * A list of fully supported decoration CSS rules that can be used in the GPU renderer.
+ */
+const gpuSupportedDecorationCssRules = [
'color',
- // 'text-decoration-line',
- // 'text-decoration-thickness',
- // 'text-decoration-style',
- // 'text-decoration-color',
];
From f87274f213dec5bd4aaf5872b2302017ae6ca7bd Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Wed, 20 Nov 2024 11:48:55 -0800
Subject: [PATCH 13/20] Move general parse function to color.ts and add some
tests
---
src/vs/base/common/color.ts | 37 +++++
src/vs/base/test/common/color.test.ts | 136 ++++++++++++++++++
.../browser/gpu/fullFileRenderStrategy.ts | 22 +--
3 files changed, 184 insertions(+), 11 deletions(-)
diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts
index caa1d4419f007..a4faedd349956 100644
--- a/src/vs/base/common/color.ts
+++ b/src/vs/base/common/color.ts
@@ -630,6 +630,43 @@ export namespace Color {
return Color.Format.CSS.formatRGBA(color);
}
+ /**
+ * Parse a CSS color and return a {@link Color}.
+ * @param css The CSS color to parse.
+ * @see https://drafts.csswg.org/css-color/#typedef-color
+ */
+ export function parse(css: string): Color | null {
+ if (css === 'transparent') {
+ return Color.transparent;
+ }
+ if (css.startsWith('#')) {
+ return parseHex(css);
+ }
+ if (css.startsWith('rgba(')) {
+ const color = css.match(/rgba\((?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+(\.\d+)?)\)/);
+ if (!color) {
+ throw new Error('Invalid color format ' + css);
+ }
+ const r = parseInt(color.groups?.r ?? '0');
+ const g = parseInt(color.groups?.g ?? '0');
+ const b = parseInt(color.groups?.b ?? '0');
+ const a = parseFloat(color.groups?.a ?? '0');
+ return new Color(new RGBA(r, g, b, a));
+ }
+ if (css.startsWith('rgb(')) {
+ const color = css.match(/rgb\((?(?:\+|-)?\d+), *(?(?:\+|-)?\d+), *(?(?:\+|-)?\d+)\)/);
+ if (!color) {
+ throw new Error('Invalid color format ' + css);
+ }
+ const r = parseInt(color.groups?.r ?? '0');
+ const g = parseInt(color.groups?.g ?? '0');
+ const b = parseInt(color.groups?.b ?? '0');
+ return new Color(new RGBA(r, g, b));
+ }
+ // TODO: Support more formats
+ return null;
+ }
+
/**
* Converts an Hex color value to a Color.
* returns r, g, and b are contained in the set [0, 255]
diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts
index 0f6c1a689cf84..3ae8353bcdff3 100644
--- a/src/vs/base/test/common/color.test.ts
+++ b/src/vs/base/test/common/color.test.ts
@@ -204,6 +204,142 @@ suite('Color', () => {
suite('Format', () => {
suite('CSS', () => {
+ suite('parse', () => {
+ test('invalid', () => {
+ assert.deepStrictEqual(Color.Format.CSS.parse(''), null);
+ assert.deepStrictEqual(Color.Format.CSS.parse('#'), null);
+ assert.deepStrictEqual(Color.Format.CSS.parse('#0102030'), null);
+ });
+ test('transparent', () => {
+ assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0)));
+ });
+ test('hex-color', () => {
+ // somewhat valid
+ assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFG0')!.rgba, new RGBA(255, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFg0')!.rgba, new RGBA(255, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#-FFF00')!.rgba, new RGBA(15, 255, 0, 1));
+
+ // valid
+ assert.deepStrictEqual(Color.Format.CSS.parse('#000000')!.rgba, new RGBA(0, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFFF')!.rgba, new RGBA(255, 255, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('#FF0000')!.rgba, new RGBA(255, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#00FF00')!.rgba, new RGBA(0, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#0000FF')!.rgba, new RGBA(0, 0, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('#FFFF00')!.rgba, new RGBA(255, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#00FFFF')!.rgba, new RGBA(0, 255, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#FF00FF')!.rgba, new RGBA(255, 0, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('#C0C0C0')!.rgba, new RGBA(192, 192, 192, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('#808080')!.rgba, new RGBA(128, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#800000')!.rgba, new RGBA(128, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#808000')!.rgba, new RGBA(128, 128, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#008000')!.rgba, new RGBA(0, 128, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#800080')!.rgba, new RGBA(128, 0, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#008080')!.rgba, new RGBA(0, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#000080')!.rgba, new RGBA(0, 0, 128, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('#010203')!.rgba, new RGBA(1, 2, 3, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#040506')!.rgba, new RGBA(4, 5, 6, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#070809')!.rgba, new RGBA(7, 8, 9, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#0a0A0a')!.rgba, new RGBA(10, 10, 10, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#0b0B0b')!.rgba, new RGBA(11, 11, 11, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#0c0C0c')!.rgba, new RGBA(12, 12, 12, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#0d0D0d')!.rgba, new RGBA(13, 13, 13, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#0e0E0e')!.rgba, new RGBA(14, 14, 14, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#0f0F0f')!.rgba, new RGBA(15, 15, 15, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#a0A0a0')!.rgba, new RGBA(160, 160, 160, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#CFA')!.rgba, new RGBA(204, 255, 170, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('#CFA8')!.rgba, new RGBA(204, 255, 170, 0.533));
+ });
+
+ test('rgb()', () => {
+ // somewhat valid / unusual
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(-255, 0, 0)')!.rgba, new RGBA(0, 0, 0));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(+255, 0, 0)')!.rgba, new RGBA(255, 0, 0));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(800, 0, 0)')!.rgba, new RGBA(255, 0, 0));
+
+ // valid
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 0)')!.rgba, new RGBA(0, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 255, 255)')!.rgba, new RGBA(255, 255, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 0, 0)')!.rgba, new RGBA(255, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 255, 0)')!.rgba, new RGBA(0, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 255)')!.rgba, new RGBA(0, 0, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 255, 0)')!.rgba, new RGBA(255, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 255, 255)')!.rgba, new RGBA(0, 255, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(255, 0, 255)')!.rgba, new RGBA(255, 0, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(192, 192, 192)')!.rgba, new RGBA(192, 192, 192, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 128, 128)')!.rgba, new RGBA(128, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 0, 0)')!.rgba, new RGBA(128, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 128, 0)')!.rgba, new RGBA(128, 128, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 128, 0)')!.rgba, new RGBA(0, 128, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(128, 0, 128)')!.rgba, new RGBA(128, 0, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 128, 128)')!.rgba, new RGBA(0, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(0, 0, 128)')!.rgba, new RGBA(0, 0, 128, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(1, 2, 3)')!.rgba, new RGBA(1, 2, 3, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(4, 5, 6)')!.rgba, new RGBA(4, 5, 6, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(7, 8, 9)')!.rgba, new RGBA(7, 8, 9, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(10, 10, 10)')!.rgba, new RGBA(10, 10, 10, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(11, 11, 11)')!.rgba, new RGBA(11, 11, 11, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(12, 12, 12)')!.rgba, new RGBA(12, 12, 12, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(13, 13, 13)')!.rgba, new RGBA(13, 13, 13, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(14, 14, 14)')!.rgba, new RGBA(14, 14, 14, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgb(15, 15, 15)')!.rgba, new RGBA(15, 15, 15, 1));
+ });
+
+ test('rgba()', () => {
+ // somewhat valid / unusual
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 0, 255)')!.rgba, new RGBA(0, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(-255, 0, 0, 1)')!.rgba, new RGBA(0, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(+255, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(800, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1));
+
+ // alpha values
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.2)')!.rgba, new RGBA(255, 0, 0, 0.2));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.5)')!.rgba, new RGBA(255, 0, 0, 0.5));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 0.75)')!.rgba, new RGBA(255, 0, 0, 0.75));
+
+ // valid
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 0, 1)')!.rgba, new RGBA(0, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 255, 255, 1)')!.rgba, new RGBA(255, 255, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 0, 1)')!.rgba, new RGBA(255, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 255, 0, 1)')!.rgba, new RGBA(0, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 255, 1)')!.rgba, new RGBA(0, 0, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 255, 0, 1)')!.rgba, new RGBA(255, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 255, 255, 1)')!.rgba, new RGBA(0, 255, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(255, 0, 255, 1)')!.rgba, new RGBA(255, 0, 255, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(192, 192, 192, 1)')!.rgba, new RGBA(192, 192, 192, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 128, 128, 1)')!.rgba, new RGBA(128, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 0, 0, 1)')!.rgba, new RGBA(128, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 128, 0, 1)')!.rgba, new RGBA(128, 128, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 128, 0, 1)')!.rgba, new RGBA(0, 128, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(128, 0, 128, 1)')!.rgba, new RGBA(128, 0, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 128, 128, 1)')!.rgba, new RGBA(0, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(0, 0, 128, 1)')!.rgba, new RGBA(0, 0, 128, 1));
+
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(1, 2, 3, 1)')!.rgba, new RGBA(1, 2, 3, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(4, 5, 6, 1)')!.rgba, new RGBA(4, 5, 6, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(7, 8, 9, 1)')!.rgba, new RGBA(7, 8, 9, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(10, 10, 10, 1)')!.rgba, new RGBA(10, 10, 10, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(11, 11, 11, 1)')!.rgba, new RGBA(11, 11, 11, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(12, 12, 12, 1)')!.rgba, new RGBA(12, 12, 12, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(13, 13, 13, 1)')!.rgba, new RGBA(13, 13, 13, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(14, 14, 14, 1)')!.rgba, new RGBA(14, 14, 14, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rgba(15, 15, 15, 1)')!.rgba, new RGBA(15, 15, 15, 1));
+ });
+ });
+
test('parseHex', () => {
// invalid
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index 941599a1143c8..b1c99760b28f5 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -24,6 +24,7 @@ import { quadVertices } from './gpuUtils.js';
import { GlyphRasterizer } from './raster/glyphRasterizer.js';
import { ViewGpuContext } from './viewGpuContext.js';
import { GpuCharMetadata } from './raster/raster.js';
+import { Color } from '../../../base/common/color.js';
const enum Constants {
@@ -410,18 +411,17 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
break;
}
case 'color': {
- // TODO: Move to color.ts and make more generic
- function parseRgb(text: string): number {
- const color = text.match(/rgba?\((\d+), ?(\d+), ?(\d+)(?:, ?\d+(?:.\d+)?)?\)/);
- if (!color) {
- throw new Error('Invalid color format ' + text);
- }
- const r = parseInt(color[1], 10);
- const g = parseInt(color[2], 10);
- const b = parseInt(color[3], 10);
- return r << 16 | g << 8 | b;
+ // TODO: This parsing/error handling should move into canRender so fallback to DOM works
+ const parsedColor = Color.Format.CSS.parse(value);
+ if (!parsedColor) {
+ throw new Error('Invalid color format ' + value);
+ }
+ const rgb = parsedColor.rgba.r << 16 | parsedColor.rgba.g << 8 | parsedColor.rgba.b;
+ charMetadata |= ((rgb << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0;
+ // TODO: _foreground_ opacity should not be applied to regular opacity
+ if (parsedColor.rgba.a < 1) {
+ charMetadata |= ((parsedColor.rgba.a * 0xFF << GpuCharMetadata.OPACITY_OFFSET) & GpuCharMetadata.OPACITY_MASK) >>> 0;
}
- charMetadata = ((parseRgb(value) << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0;
break;
}
default: throw new BugIndicatingError('Unexpected inline decoration style');
From 07d84aae4a81d32cf66954a32564c680f7584d46 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 21 Nov 2024 07:03:04 -0800
Subject: [PATCH 14/20] Add CSS named color support to parse function
---
src/vs/base/common/color.ts | 157 +++++++++++++++++++++++++-
src/vs/base/test/common/color.test.ts | 153 +++++++++++++++++++++++++
2 files changed, 309 insertions(+), 1 deletion(-)
diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts
index a4faedd349956..02d98667fb71d 100644
--- a/src/vs/base/common/color.ts
+++ b/src/vs/base/common/color.ts
@@ -664,7 +664,162 @@ export namespace Color {
return new Color(new RGBA(r, g, b));
}
// TODO: Support more formats
- return null;
+ return parseNamedKeyword(css);
+ }
+
+ function parseNamedKeyword(css: string): Color | null {
+ // https://drafts.csswg.org/css-color/#named-colors
+ switch (css) {
+ case 'aliceblue': return new Color(new RGBA(240, 248, 255, 1));
+ case 'antiquewhite': return new Color(new RGBA(250, 235, 215, 1));
+ case 'aqua': return new Color(new RGBA(0, 255, 255, 1));
+ case 'aquamarine': return new Color(new RGBA(127, 255, 212, 1));
+ case 'azure': return new Color(new RGBA(240, 255, 255, 1));
+ case 'beige': return new Color(new RGBA(245, 245, 220, 1));
+ case 'bisque': return new Color(new RGBA(255, 228, 196, 1));
+ case 'black': return new Color(new RGBA(0, 0, 0, 1));
+ case 'blanchedalmond': return new Color(new RGBA(255, 235, 205, 1));
+ case 'blue': return new Color(new RGBA(0, 0, 255, 1));
+ case 'blueviolet': return new Color(new RGBA(138, 43, 226, 1));
+ case 'brown': return new Color(new RGBA(165, 42, 42, 1));
+ case 'burlywood': return new Color(new RGBA(222, 184, 135, 1));
+ case 'cadetblue': return new Color(new RGBA(95, 158, 160, 1));
+ case 'chartreuse': return new Color(new RGBA(127, 255, 0, 1));
+ case 'chocolate': return new Color(new RGBA(210, 105, 30, 1));
+ case 'coral': return new Color(new RGBA(255, 127, 80, 1));
+ case 'cornflowerblue': return new Color(new RGBA(100, 149, 237, 1));
+ case 'cornsilk': return new Color(new RGBA(255, 248, 220, 1));
+ case 'crimson': return new Color(new RGBA(220, 20, 60, 1));
+ case 'cyan': return new Color(new RGBA(0, 255, 255, 1));
+ case 'darkblue': return new Color(new RGBA(0, 0, 139, 1));
+ case 'darkcyan': return new Color(new RGBA(0, 139, 139, 1));
+ case 'darkgoldenrod': return new Color(new RGBA(184, 134, 11, 1));
+ case 'darkgray': return new Color(new RGBA(169, 169, 169, 1));
+ case 'darkgreen': return new Color(new RGBA(0, 100, 0, 1));
+ case 'darkgrey': return new Color(new RGBA(169, 169, 169, 1));
+ case 'darkkhaki': return new Color(new RGBA(189, 183, 107, 1));
+ case 'darkmagenta': return new Color(new RGBA(139, 0, 139, 1));
+ case 'darkolivegreen': return new Color(new RGBA(85, 107, 47, 1));
+ case 'darkorange': return new Color(new RGBA(255, 140, 0, 1));
+ case 'darkorchid': return new Color(new RGBA(153, 50, 204, 1));
+ case 'darkred': return new Color(new RGBA(139, 0, 0, 1));
+ case 'darksalmon': return new Color(new RGBA(233, 150, 122, 1));
+ case 'darkseagreen': return new Color(new RGBA(143, 188, 143, 1));
+ case 'darkslateblue': return new Color(new RGBA(72, 61, 139, 1));
+ case 'darkslategray': return new Color(new RGBA(47, 79, 79, 1));
+ case 'darkslategrey': return new Color(new RGBA(47, 79, 79, 1));
+ case 'darkturquoise': return new Color(new RGBA(0, 206, 209, 1));
+ case 'darkviolet': return new Color(new RGBA(148, 0, 211, 1));
+ case 'deeppink': return new Color(new RGBA(255, 20, 147, 1));
+ case 'deepskyblue': return new Color(new RGBA(0, 191, 255, 1));
+ case 'dimgray': return new Color(new RGBA(105, 105, 105, 1));
+ case 'dimgrey': return new Color(new RGBA(105, 105, 105, 1));
+ case 'dodgerblue': return new Color(new RGBA(30, 144, 255, 1));
+ case 'firebrick': return new Color(new RGBA(178, 34, 34, 1));
+ case 'floralwhite': return new Color(new RGBA(255, 250, 240, 1));
+ case 'forestgreen': return new Color(new RGBA(34, 139, 34, 1));
+ case 'fuchsia': return new Color(new RGBA(255, 0, 255, 1));
+ case 'gainsboro': return new Color(new RGBA(220, 220, 220, 1));
+ case 'ghostwhite': return new Color(new RGBA(248, 248, 255, 1));
+ case 'gold': return new Color(new RGBA(255, 215, 0, 1));
+ case 'goldenrod': return new Color(new RGBA(218, 165, 32, 1));
+ case 'gray': return new Color(new RGBA(128, 128, 128, 1));
+ case 'green': return new Color(new RGBA(0, 128, 0, 1));
+ case 'greenyellow': return new Color(new RGBA(173, 255, 47, 1));
+ case 'grey': return new Color(new RGBA(128, 128, 128, 1));
+ case 'honeydew': return new Color(new RGBA(240, 255, 240, 1));
+ case 'hotpink': return new Color(new RGBA(255, 105, 180, 1));
+ case 'indianred': return new Color(new RGBA(205, 92, 92, 1));
+ case 'indigo': return new Color(new RGBA(75, 0, 130, 1));
+ case 'ivory': return new Color(new RGBA(255, 255, 240, 1));
+ case 'khaki': return new Color(new RGBA(240, 230, 140, 1));
+ case 'lavender': return new Color(new RGBA(230, 230, 250, 1));
+ case 'lavenderblush': return new Color(new RGBA(255, 240, 245, 1));
+ case 'lawngreen': return new Color(new RGBA(124, 252, 0, 1));
+ case 'lemonchiffon': return new Color(new RGBA(255, 250, 205, 1));
+ case 'lightblue': return new Color(new RGBA(173, 216, 230, 1));
+ case 'lightcoral': return new Color(new RGBA(240, 128, 128, 1));
+ case 'lightcyan': return new Color(new RGBA(224, 255, 255, 1));
+ case 'lightgoldenrodyellow': return new Color(new RGBA(250, 250, 210, 1));
+ case 'lightgray': return new Color(new RGBA(211, 211, 211, 1));
+ case 'lightgreen': return new Color(new RGBA(144, 238, 144, 1));
+ case 'lightgrey': return new Color(new RGBA(211, 211, 211, 1));
+ case 'lightpink': return new Color(new RGBA(255, 182, 193, 1));
+ case 'lightsalmon': return new Color(new RGBA(255, 160, 122, 1));
+ case 'lightseagreen': return new Color(new RGBA(32, 178, 170, 1));
+ case 'lightskyblue': return new Color(new RGBA(135, 206, 250, 1));
+ case 'lightslategray': return new Color(new RGBA(119, 136, 153, 1));
+ case 'lightslategrey': return new Color(new RGBA(119, 136, 153, 1));
+ case 'lightsteelblue': return new Color(new RGBA(176, 196, 222, 1));
+ case 'lightyellow': return new Color(new RGBA(255, 255, 224, 1));
+ case 'lime': return new Color(new RGBA(0, 255, 0, 1));
+ case 'limegreen': return new Color(new RGBA(50, 205, 50, 1));
+ case 'linen': return new Color(new RGBA(250, 240, 230, 1));
+ case 'magenta': return new Color(new RGBA(255, 0, 255, 1));
+ case 'maroon': return new Color(new RGBA(128, 0, 0, 1));
+ case 'mediumaquamarine': return new Color(new RGBA(102, 205, 170, 1));
+ case 'mediumblue': return new Color(new RGBA(0, 0, 205, 1));
+ case 'mediumorchid': return new Color(new RGBA(186, 85, 211, 1));
+ case 'mediumpurple': return new Color(new RGBA(147, 112, 219, 1));
+ case 'mediumseagreen': return new Color(new RGBA(60, 179, 113, 1));
+ case 'mediumslateblue': return new Color(new RGBA(123, 104, 238, 1));
+ case 'mediumspringgreen': return new Color(new RGBA(0, 250, 154, 1));
+ case 'mediumturquoise': return new Color(new RGBA(72, 209, 204, 1));
+ case 'mediumvioletred': return new Color(new RGBA(199, 21, 133, 1));
+ case 'midnightblue': return new Color(new RGBA(25, 25, 112, 1));
+ case 'mintcream': return new Color(new RGBA(245, 255, 250, 1));
+ case 'mistyrose': return new Color(new RGBA(255, 228, 225, 1));
+ case 'moccasin': return new Color(new RGBA(255, 228, 181, 1));
+ case 'navajowhite': return new Color(new RGBA(255, 222, 173, 1));
+ case 'navy': return new Color(new RGBA(0, 0, 128, 1));
+ case 'oldlace': return new Color(new RGBA(253, 245, 230, 1));
+ case 'olive': return new Color(new RGBA(128, 128, 0, 1));
+ case 'olivedrab': return new Color(new RGBA(107, 142, 35, 1));
+ case 'orange': return new Color(new RGBA(255, 165, 0, 1));
+ case 'orangered': return new Color(new RGBA(255, 69, 0, 1));
+ case 'orchid': return new Color(new RGBA(218, 112, 214, 1));
+ case 'palegoldenrod': return new Color(new RGBA(238, 232, 170, 1));
+ case 'palegreen': return new Color(new RGBA(152, 251, 152, 1));
+ case 'paleturquoise': return new Color(new RGBA(175, 238, 238, 1));
+ case 'palevioletred': return new Color(new RGBA(219, 112, 147, 1));
+ case 'papayawhip': return new Color(new RGBA(255, 239, 213, 1));
+ case 'peachpuff': return new Color(new RGBA(255, 218, 185, 1));
+ case 'peru': return new Color(new RGBA(205, 133, 63, 1));
+ case 'pink': return new Color(new RGBA(255, 192, 203, 1));
+ case 'plum': return new Color(new RGBA(221, 160, 221, 1));
+ case 'powderblue': return new Color(new RGBA(176, 224, 230, 1));
+ case 'purple': return new Color(new RGBA(128, 0, 128, 1));
+ case 'rebeccapurple': return new Color(new RGBA(102, 51, 153, 1));
+ case 'red': return new Color(new RGBA(255, 0, 0, 1));
+ case 'rosybrown': return new Color(new RGBA(188, 143, 143, 1));
+ case 'royalblue': return new Color(new RGBA(65, 105, 225, 1));
+ case 'saddlebrown': return new Color(new RGBA(139, 69, 19, 1));
+ case 'salmon': return new Color(new RGBA(250, 128, 114, 1));
+ case 'sandybrown': return new Color(new RGBA(244, 164, 96, 1));
+ case 'seagreen': return new Color(new RGBA(46, 139, 87, 1));
+ case 'seashell': return new Color(new RGBA(255, 245, 238, 1));
+ case 'sienna': return new Color(new RGBA(160, 82, 45, 1));
+ case 'silver': return new Color(new RGBA(192, 192, 192, 1));
+ case 'skyblue': return new Color(new RGBA(135, 206, 235, 1));
+ case 'slateblue': return new Color(new RGBA(106, 90, 205, 1));
+ case 'slategray': return new Color(new RGBA(112, 128, 144, 1));
+ case 'slategrey': return new Color(new RGBA(112, 128, 144, 1));
+ case 'snow': return new Color(new RGBA(255, 250, 250, 1));
+ case 'springgreen': return new Color(new RGBA(0, 255, 127, 1));
+ case 'steelblue': return new Color(new RGBA(70, 130, 180, 1));
+ case 'tan': return new Color(new RGBA(210, 180, 140, 1));
+ case 'teal': return new Color(new RGBA(0, 128, 128, 1));
+ case 'thistle': return new Color(new RGBA(216, 191, 216, 1));
+ case 'tomato': return new Color(new RGBA(255, 99, 71, 1));
+ case 'turquoise': return new Color(new RGBA(64, 224, 208, 1));
+ case 'violet': return new Color(new RGBA(238, 130, 238, 1));
+ case 'wheat': return new Color(new RGBA(245, 222, 179, 1));
+ case 'white': return new Color(new RGBA(255, 255, 255, 1));
+ case 'whitesmoke': return new Color(new RGBA(245, 245, 245, 1));
+ case 'yellow': return new Color(new RGBA(255, 255, 0, 1));
+ case 'yellowgreen': return new Color(new RGBA(154, 205, 50, 1));
+ default: return null;
+ }
}
/**
diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts
index 3ae8353bcdff3..ca5db568ca67f 100644
--- a/src/vs/base/test/common/color.test.ts
+++ b/src/vs/base/test/common/color.test.ts
@@ -210,9 +210,162 @@ suite('Color', () => {
assert.deepStrictEqual(Color.Format.CSS.parse('#'), null);
assert.deepStrictEqual(Color.Format.CSS.parse('#0102030'), null);
});
+
test('transparent', () => {
assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0)));
});
+
+ test('named keyword', () => {
+ assert.deepStrictEqual(Color.Format.CSS.parse('aliceblue')!.rgba, new RGBA(240, 248, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('antiquewhite')!.rgba, new RGBA(250, 235, 215, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('aqua')!.rgba, new RGBA(0, 255, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('aquamarine')!.rgba, new RGBA(127, 255, 212, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('azure')!.rgba, new RGBA(240, 255, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('beige')!.rgba, new RGBA(245, 245, 220, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('bisque')!.rgba, new RGBA(255, 228, 196, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('black')!.rgba, new RGBA(0, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('blanchedalmond')!.rgba, new RGBA(255, 235, 205, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('blue')!.rgba, new RGBA(0, 0, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('blueviolet')!.rgba, new RGBA(138, 43, 226, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('brown')!.rgba, new RGBA(165, 42, 42, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('burlywood')!.rgba, new RGBA(222, 184, 135, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('cadetblue')!.rgba, new RGBA(95, 158, 160, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('chartreuse')!.rgba, new RGBA(127, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('chocolate')!.rgba, new RGBA(210, 105, 30, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('coral')!.rgba, new RGBA(255, 127, 80, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('cornflowerblue')!.rgba, new RGBA(100, 149, 237, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('cornsilk')!.rgba, new RGBA(255, 248, 220, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('crimson')!.rgba, new RGBA(220, 20, 60, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('cyan')!.rgba, new RGBA(0, 255, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkblue')!.rgba, new RGBA(0, 0, 139, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkcyan')!.rgba, new RGBA(0, 139, 139, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkgoldenrod')!.rgba, new RGBA(184, 134, 11, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkgray')!.rgba, new RGBA(169, 169, 169, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkgreen')!.rgba, new RGBA(0, 100, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkgrey')!.rgba, new RGBA(169, 169, 169, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkkhaki')!.rgba, new RGBA(189, 183, 107, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkmagenta')!.rgba, new RGBA(139, 0, 139, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkolivegreen')!.rgba, new RGBA(85, 107, 47, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkorange')!.rgba, new RGBA(255, 140, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkorchid')!.rgba, new RGBA(153, 50, 204, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkred')!.rgba, new RGBA(139, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darksalmon')!.rgba, new RGBA(233, 150, 122, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkseagreen')!.rgba, new RGBA(143, 188, 143, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkslateblue')!.rgba, new RGBA(72, 61, 139, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkslategray')!.rgba, new RGBA(47, 79, 79, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkslategrey')!.rgba, new RGBA(47, 79, 79, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkturquoise')!.rgba, new RGBA(0, 206, 209, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('darkviolet')!.rgba, new RGBA(148, 0, 211, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('deeppink')!.rgba, new RGBA(255, 20, 147, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('deepskyblue')!.rgba, new RGBA(0, 191, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('dimgray')!.rgba, new RGBA(105, 105, 105, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('dimgrey')!.rgba, new RGBA(105, 105, 105, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('dodgerblue')!.rgba, new RGBA(30, 144, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('firebrick')!.rgba, new RGBA(178, 34, 34, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('floralwhite')!.rgba, new RGBA(255, 250, 240, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('forestgreen')!.rgba, new RGBA(34, 139, 34, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('fuchsia')!.rgba, new RGBA(255, 0, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('gainsboro')!.rgba, new RGBA(220, 220, 220, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('ghostwhite')!.rgba, new RGBA(248, 248, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('gold')!.rgba, new RGBA(255, 215, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('goldenrod')!.rgba, new RGBA(218, 165, 32, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('gray')!.rgba, new RGBA(128, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('green')!.rgba, new RGBA(0, 128, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('greenyellow')!.rgba, new RGBA(173, 255, 47, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('grey')!.rgba, new RGBA(128, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('honeydew')!.rgba, new RGBA(240, 255, 240, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('hotpink')!.rgba, new RGBA(255, 105, 180, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('indianred')!.rgba, new RGBA(205, 92, 92, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('indigo')!.rgba, new RGBA(75, 0, 130, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('ivory')!.rgba, new RGBA(255, 255, 240, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('khaki')!.rgba, new RGBA(240, 230, 140, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lavender')!.rgba, new RGBA(230, 230, 250, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lavenderblush')!.rgba, new RGBA(255, 240, 245, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lawngreen')!.rgba, new RGBA(124, 252, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lemonchiffon')!.rgba, new RGBA(255, 250, 205, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightblue')!.rgba, new RGBA(173, 216, 230, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightcoral')!.rgba, new RGBA(240, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightcyan')!.rgba, new RGBA(224, 255, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightgoldenrodyellow')!.rgba, new RGBA(250, 250, 210, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightgray')!.rgba, new RGBA(211, 211, 211, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightgreen')!.rgba, new RGBA(144, 238, 144, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightgrey')!.rgba, new RGBA(211, 211, 211, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightpink')!.rgba, new RGBA(255, 182, 193, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightsalmon')!.rgba, new RGBA(255, 160, 122, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightseagreen')!.rgba, new RGBA(32, 178, 170, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightskyblue')!.rgba, new RGBA(135, 206, 250, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightslategray')!.rgba, new RGBA(119, 136, 153, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightslategrey')!.rgba, new RGBA(119, 136, 153, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightsteelblue')!.rgba, new RGBA(176, 196, 222, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lightyellow')!.rgba, new RGBA(255, 255, 224, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('lime')!.rgba, new RGBA(0, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('limegreen')!.rgba, new RGBA(50, 205, 50, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('linen')!.rgba, new RGBA(250, 240, 230, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('magenta')!.rgba, new RGBA(255, 0, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('maroon')!.rgba, new RGBA(128, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumaquamarine')!.rgba, new RGBA(102, 205, 170, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumblue')!.rgba, new RGBA(0, 0, 205, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumorchid')!.rgba, new RGBA(186, 85, 211, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumpurple')!.rgba, new RGBA(147, 112, 219, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumseagreen')!.rgba, new RGBA(60, 179, 113, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumslateblue')!.rgba, new RGBA(123, 104, 238, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumspringgreen')!.rgba, new RGBA(0, 250, 154, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumturquoise')!.rgba, new RGBA(72, 209, 204, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mediumvioletred')!.rgba, new RGBA(199, 21, 133, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('midnightblue')!.rgba, new RGBA(25, 25, 112, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mintcream')!.rgba, new RGBA(245, 255, 250, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('mistyrose')!.rgba, new RGBA(255, 228, 225, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('moccasin')!.rgba, new RGBA(255, 228, 181, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('navajowhite')!.rgba, new RGBA(255, 222, 173, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('navy')!.rgba, new RGBA(0, 0, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('oldlace')!.rgba, new RGBA(253, 245, 230, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('olive')!.rgba, new RGBA(128, 128, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('olivedrab')!.rgba, new RGBA(107, 142, 35, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('orange')!.rgba, new RGBA(255, 165, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('orangered')!.rgba, new RGBA(255, 69, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('orchid')!.rgba, new RGBA(218, 112, 214, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('palegoldenrod')!.rgba, new RGBA(238, 232, 170, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('palegreen')!.rgba, new RGBA(152, 251, 152, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('paleturquoise')!.rgba, new RGBA(175, 238, 238, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('palevioletred')!.rgba, new RGBA(219, 112, 147, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('papayawhip')!.rgba, new RGBA(255, 239, 213, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('peachpuff')!.rgba, new RGBA(255, 218, 185, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('peru')!.rgba, new RGBA(205, 133, 63, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('pink')!.rgba, new RGBA(255, 192, 203, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('plum')!.rgba, new RGBA(221, 160, 221, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('powderblue')!.rgba, new RGBA(176, 224, 230, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('purple')!.rgba, new RGBA(128, 0, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rebeccapurple')!.rgba, new RGBA(102, 51, 153, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('red')!.rgba, new RGBA(255, 0, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('rosybrown')!.rgba, new RGBA(188, 143, 143, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('royalblue')!.rgba, new RGBA(65, 105, 225, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('saddlebrown')!.rgba, new RGBA(139, 69, 19, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('salmon')!.rgba, new RGBA(250, 128, 114, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('sandybrown')!.rgba, new RGBA(244, 164, 96, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('seagreen')!.rgba, new RGBA(46, 139, 87, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('seashell')!.rgba, new RGBA(255, 245, 238, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('sienna')!.rgba, new RGBA(160, 82, 45, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('silver')!.rgba, new RGBA(192, 192, 192, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('skyblue')!.rgba, new RGBA(135, 206, 235, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('slateblue')!.rgba, new RGBA(106, 90, 205, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('slategray')!.rgba, new RGBA(112, 128, 144, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('slategrey')!.rgba, new RGBA(112, 128, 144, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('snow')!.rgba, new RGBA(255, 250, 250, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('springgreen')!.rgba, new RGBA(0, 255, 127, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('steelblue')!.rgba, new RGBA(70, 130, 180, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('tan')!.rgba, new RGBA(210, 180, 140, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('teal')!.rgba, new RGBA(0, 128, 128, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('thistle')!.rgba, new RGBA(216, 191, 216, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('tomato')!.rgba, new RGBA(255, 99, 71, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('turquoise')!.rgba, new RGBA(64, 224, 208, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('violet')!.rgba, new RGBA(238, 130, 238, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('wheat')!.rgba, new RGBA(245, 222, 179, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('white')!.rgba, new RGBA(255, 255, 255, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('whitesmoke')!.rgba, new RGBA(245, 245, 245, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('yellow')!.rgba, new RGBA(255, 255, 0, 1));
+ assert.deepStrictEqual(Color.Format.CSS.parse('yellowgreen')!.rgba, new RGBA(154, 205, 50, 1));
+ });
+
test('hex-color', () => {
// somewhat valid
assert.deepStrictEqual(Color.Format.CSS.parse('#FFFFG0')!.rgba, new RGBA(255, 255, 0, 1));
From b5c3a7f91362dba398912e4ac5dc1750864e0c89 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Thu, 21 Nov 2024 11:40:45 -0800
Subject: [PATCH 15/20] Fix transparent color test
---
src/vs/base/test/common/color.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts
index ca5db568ca67f..5410575cc3304 100644
--- a/src/vs/base/test/common/color.test.ts
+++ b/src/vs/base/test/common/color.test.ts
@@ -212,7 +212,7 @@ suite('Color', () => {
});
test('transparent', () => {
- assert.deepStrictEqual(Color.Format.CSS.parse('transparent'), new Color(new RGBA(0, 0, 0, 0)));
+ assert.deepStrictEqual(Color.Format.CSS.parse('transparent')!.rgba, new RGBA(0, 0, 0, 0));
});
test('named keyword', () => {
From 8f65102dbf76b8cf35d1143e21350481e13395ca Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Fri, 22 Nov 2024 06:55:35 -0800
Subject: [PATCH 16/20] Fix issue with caching ignoring charMetadata
---
src/vs/base/common/color.ts | 13 ++++++++
.../browser/gpu/atlas/textureAtlasPage.ts | 3 +-
.../browser/gpu/fullFileRenderStrategy.ts | 32 ++++---------------
.../browser/gpu/raster/glyphRasterizer.ts | 12 +++----
src/vs/editor/browser/gpu/raster/raster.ts | 8 -----
5 files changed, 27 insertions(+), 41 deletions(-)
diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts
index 02d98667fb71d..67eeeec757a3e 100644
--- a/src/vs/base/common/color.ts
+++ b/src/vs/base/common/color.ts
@@ -535,6 +535,19 @@ export class Color {
return this._toString;
}
+ private _toNumber24Bit?: number;
+ toNumber24Bit(): number {
+ if (!this._toNumber24Bit) {
+ this._toNumber24Bit = (
+ this.rgba.r /* */ << 24 |
+ this.rgba.g /* */ << 16 |
+ this.rgba.b /* */ << 8 |
+ this.rgba.a * 0xFF << 0
+ ) >>> 0;
+ }
+ return this._toNumber24Bit;
+ }
+
static getLighterColor(of: Color, relative: Color, factor?: number): Color {
if (of.isLighterThan(relative)) {
return of;
diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts
index cbda9352a3bc8..9c09181751a7a 100644
--- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts
+++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts
@@ -100,7 +100,8 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla
if (this._logService.getLevel() === LogLevel.Trace) {
this._logService.trace('New glyph', {
chars,
- metadata: tokenMetadata,
+ tokenMetadata,
+ charMetadata,
rasterizedGlyph,
glyph
});
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index b1c99760b28f5..bfb27a81d332b 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -8,7 +8,6 @@ import { BugIndicatingError } from '../../../base/common/errors.js';
import { MandatoryMutableDisposable } from '../../../base/common/lifecycle.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { CursorColumns } from '../../common/core/cursorColumns.js';
-import { MetadataConsts } from '../../common/encodedTokenAttributes.js';
import type { IViewLineTokens } from '../../common/tokens/lineTokens.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js';
@@ -23,10 +22,8 @@ import { GPULifecycle } from './gpuDisposable.js';
import { quadVertices } from './gpuUtils.js';
import { GlyphRasterizer } from './raster/glyphRasterizer.js';
import { ViewGpuContext } from './viewGpuContext.js';
-import { GpuCharMetadata } from './raster/raster.js';
import { Color } from '../../../base/common/color.js';
-
const enum Constants {
IndicesPerCell = 6,
}
@@ -141,7 +138,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
}
public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean {
- // TODO: Don't clear all lines
+ // TODO: Don't clear all cells if we can avoid it
this._invalidateAllLines();
return true;
}
@@ -225,14 +222,14 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
}
reset() {
+ this._invalidateAllLines();
for (const bufferIndex of [0, 1]) {
// Zero out buffer and upload to GPU to prevent stale rows from rendering
const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]);
buffer.fill(0, 0, buffer.length);
this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength);
- this._upToDateLines[bufferIndex].clear();
}
- this._visibleObjectCount = 0;
+ this._finalRenderedLine = 0;
}
update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number {
@@ -279,7 +276,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
const queuedBufferUpdates = this._queuedBufferUpdates[this._activeDoubleBufferIndex];
while (queuedBufferUpdates.length) {
const e = queuedBufferUpdates.shift()!;
-
switch (e.type) {
case ViewEventType.ViewConfigurationChanged: {
// TODO: Refine the cases for when we throw away all the data
@@ -368,9 +364,8 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
charMetadata = 0;
// TODO: We'd want to optimize pulling the decorations in order
- // HACK: Temporary replace char to demonstrate inline decorations
const cellDecorations = inlineDecorations.filter(decoration => {
- // TODO: Why does Range.containsPosition and Range.strictContainsPosition not work here?
+ // This is Range.strictContainsPosition except it's working at the cell level.
if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) {
return false;
}
@@ -400,28 +395,13 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
for (const [key, value] of inlineStyles.entries()) {
switch (key) {
- case 'text-decoration-line': {
- charMetadata |= MetadataConsts.STRIKETHROUGH_MASK;
- break;
- }
- case 'text-decoration-thickness':
- case 'text-decoration-style':
- case 'text-decoration-color': {
- // HACK: Ignore for now to avoid throwing
- break;
- }
case 'color': {
// TODO: This parsing/error handling should move into canRender so fallback to DOM works
const parsedColor = Color.Format.CSS.parse(value);
if (!parsedColor) {
- throw new Error('Invalid color format ' + value);
- }
- const rgb = parsedColor.rgba.r << 16 | parsedColor.rgba.g << 8 | parsedColor.rgba.b;
- charMetadata |= ((rgb << GpuCharMetadata.FOREGROUND_OFFSET) & GpuCharMetadata.FOREGROUND_MASK) >>> 0;
- // TODO: _foreground_ opacity should not be applied to regular opacity
- if (parsedColor.rgba.a < 1) {
- charMetadata |= ((parsedColor.rgba.a * 0xFF << GpuCharMetadata.OPACITY_OFFSET) & GpuCharMetadata.OPACITY_MASK) >>> 0;
+ throw new BugIndicatingError('Invalid color format ' + value);
}
+ charMetadata = parsedColor.toNumber24Bit();
break;
}
default: throw new BugIndicatingError('Unexpected inline decoration style');
diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
index efec3211c9a7c..a0ae8927b832d 100644
--- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
+++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
@@ -9,7 +9,7 @@ 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 { GpuCharMetadata, type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js';
+import { type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js';
let nextId = 0;
@@ -37,7 +37,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; tokenMetadata: number; charMetadata: number } = { chars: undefined, tokenMetadata: 0, charMetadata: 0 };
constructor(
readonly fontSize: number,
@@ -75,11 +75,12 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
// 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 === tokenMetadata) {
+ if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.tokenMetadata === tokenMetadata && this._workGlyphConfig.charMetadata === charMetadata) {
return this._workGlyph;
}
this._workGlyphConfig.chars = chars;
- this._workGlyphConfig.metadata = tokenMetadata;
+ this._workGlyphConfig.tokenMetadata = tokenMetadata;
+ this._workGlyphConfig.charMetadata = charMetadata;
return this._rasterizeGlyph(chars, tokenMetadata, charMetadata, colorMap);
}
@@ -117,8 +118,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
const originX = devicePixelFontSize;
const originY = devicePixelFontSize;
if (charMetadata) {
- const fg = (charMetadata & GpuCharMetadata.FOREGROUND_MASK) >> GpuCharMetadata.FOREGROUND_OFFSET;
- this._ctx.fillStyle = `#${fg.toString(16).padStart(6, '0')}`;
+ this._ctx.fillStyle = `#${charMetadata.toString(16).padStart(8, '0')}`;
} else {
this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)];
}
diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts
index d83b55665dbf5..c86b1649e34b7 100644
--- a/src/vs/editor/browser/gpu/raster/raster.ts
+++ b/src/vs/editor/browser/gpu/raster/raster.ts
@@ -65,11 +65,3 @@ export interface IRasterizedGlyph {
*/
originOffset: { x: number; y: number };
}
-
-export const enum GpuCharMetadata {
- FOREGROUND_MASK /* */ = 0b00000000_11111111_11111111_11111111,
- OPACITY_MASK /* */ = 0b11111111_00000000_00000000_00000000,
-
- FOREGROUND_OFFSET = 0,
- OPACITY_OFFSET = 24,
-}
From 9b4c43bf852c0af2accfe60257862cded58b064a Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Fri, 22 Nov 2024 06:56:49 -0800
Subject: [PATCH 17/20] Add tests for toString and toNumber24Bit
---
src/vs/base/test/common/color.test.ts | 74 +++++++++++++++++++++++++++
1 file changed, 74 insertions(+)
diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts
index 5410575cc3304..c0f439d744ed4 100644
--- a/src/vs/base/test/common/color.test.ts
+++ b/src/vs/base/test/common/color.test.ts
@@ -81,6 +81,80 @@ suite('Color', () => {
assert.deepStrictEqual(new Color(new RGBA(0, 0, 0, 0.58)).blend(new Color(new RGBA(255, 255, 255, 0.33))), new Color(new RGBA(49, 49, 49, 0.719)));
});
+ suite('toString', () => {
+ test('alpha channel', () => {
+ assert.deepStrictEqual(Color.fromHex('#00000000').toString(), 'rgba(0, 0, 0, 0)');
+ assert.deepStrictEqual(Color.fromHex('#00000080').toString(), 'rgba(0, 0, 0, 0.5)');
+ assert.deepStrictEqual(Color.fromHex('#000000FF').toString(), '#000000');
+ });
+
+ test('opaque', () => {
+ assert.deepStrictEqual(Color.fromHex('#000000').toString().toUpperCase(), '#000000'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#FFFFFF').toString().toUpperCase(), '#FFFFFF'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#FF0000').toString().toUpperCase(), '#FF0000'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#00FF00').toString().toUpperCase(), '#00FF00'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#0000FF').toString().toUpperCase(), '#0000FF'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#FFFF00').toString().toUpperCase(), '#FFFF00'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#00FFFF').toString().toUpperCase(), '#00FFFF'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#FF00FF').toString().toUpperCase(), '#FF00FF'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#C0C0C0').toString().toUpperCase(), '#C0C0C0'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#808080').toString().toUpperCase(), '#808080'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#800000').toString().toUpperCase(), '#800000'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#808000').toString().toUpperCase(), '#808000'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#008000').toString().toUpperCase(), '#008000'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#800080').toString().toUpperCase(), '#800080'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#008080').toString().toUpperCase(), '#008080'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#000080').toString().toUpperCase(), '#000080'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#010203').toString().toUpperCase(), '#010203'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#040506').toString().toUpperCase(), '#040506'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#070809').toString().toUpperCase(), '#070809'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#0a0A0a').toString().toUpperCase(), '#0a0A0a'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#0b0B0b').toString().toUpperCase(), '#0b0B0b'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#0c0C0c').toString().toUpperCase(), '#0c0C0c'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#0d0D0d').toString().toUpperCase(), '#0d0D0d'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#0e0E0e').toString().toUpperCase(), '#0e0E0e'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#0f0F0f').toString().toUpperCase(), '#0f0F0f'.toUpperCase());
+ assert.deepStrictEqual(Color.fromHex('#a0A0a0').toString().toUpperCase(), '#a0A0a0'.toUpperCase());
+ });
+ });
+
+ suite('toNumber24Bit', () => {
+ test('alpha channel', () => {
+ assert.deepStrictEqual(Color.fromHex('#00000000').toNumber24Bit(), 0x00000000);
+ assert.deepStrictEqual(Color.fromHex('#00000080').toNumber24Bit(), 0x00000080);
+ assert.deepStrictEqual(Color.fromHex('#000000FF').toNumber24Bit(), 0x000000FF);
+ });
+
+ test('opaque', () => {
+ assert.deepStrictEqual(Color.fromHex('#000000').toNumber24Bit(), 0x000000FF);
+ assert.deepStrictEqual(Color.fromHex('#FFFFFF').toNumber24Bit(), 0xFFFFFFFF);
+ assert.deepStrictEqual(Color.fromHex('#FF0000').toNumber24Bit(), 0xFF0000FF);
+ assert.deepStrictEqual(Color.fromHex('#00FF00').toNumber24Bit(), 0x00FF00FF);
+ assert.deepStrictEqual(Color.fromHex('#0000FF').toNumber24Bit(), 0x0000FFFF);
+ assert.deepStrictEqual(Color.fromHex('#FFFF00').toNumber24Bit(), 0xFFFF00FF);
+ assert.deepStrictEqual(Color.fromHex('#00FFFF').toNumber24Bit(), 0x00FFFFFF);
+ assert.deepStrictEqual(Color.fromHex('#FF00FF').toNumber24Bit(), 0xFF00FFFF);
+ assert.deepStrictEqual(Color.fromHex('#C0C0C0').toNumber24Bit(), 0xC0C0C0FF);
+ assert.deepStrictEqual(Color.fromHex('#808080').toNumber24Bit(), 0x808080FF);
+ assert.deepStrictEqual(Color.fromHex('#800000').toNumber24Bit(), 0x800000FF);
+ assert.deepStrictEqual(Color.fromHex('#808000').toNumber24Bit(), 0x808000FF);
+ assert.deepStrictEqual(Color.fromHex('#008000').toNumber24Bit(), 0x008000FF);
+ assert.deepStrictEqual(Color.fromHex('#800080').toNumber24Bit(), 0x800080FF);
+ assert.deepStrictEqual(Color.fromHex('#008080').toNumber24Bit(), 0x008080FF);
+ assert.deepStrictEqual(Color.fromHex('#000080').toNumber24Bit(), 0x000080FF);
+ assert.deepStrictEqual(Color.fromHex('#010203').toNumber24Bit(), 0x010203FF);
+ assert.deepStrictEqual(Color.fromHex('#040506').toNumber24Bit(), 0x040506FF);
+ assert.deepStrictEqual(Color.fromHex('#070809').toNumber24Bit(), 0x070809FF);
+ assert.deepStrictEqual(Color.fromHex('#0a0A0a').toNumber24Bit(), 0x0a0A0aFF);
+ assert.deepStrictEqual(Color.fromHex('#0b0B0b').toNumber24Bit(), 0x0b0B0bFF);
+ assert.deepStrictEqual(Color.fromHex('#0c0C0c').toNumber24Bit(), 0x0c0C0cFF);
+ assert.deepStrictEqual(Color.fromHex('#0d0D0d').toNumber24Bit(), 0x0d0D0dFF);
+ assert.deepStrictEqual(Color.fromHex('#0e0E0e').toNumber24Bit(), 0x0e0E0eFF);
+ assert.deepStrictEqual(Color.fromHex('#0f0F0f').toNumber24Bit(), 0x0f0F0fFF);
+ assert.deepStrictEqual(Color.fromHex('#a0A0a0').toNumber24Bit(), 0xa0A0a0FF);
+ });
+ });
+
suite('HSLA', () => {
test('HSLA.toRGBA', () => {
assert.deepStrictEqual(HSLA.toRGBA(new HSLA(0, 0, 0, 0)), new RGBA(0, 0, 0, 0));
From 6de7763887dffc0e87e153842bc038b54ae0dbeb Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Fri, 22 Nov 2024 07:23:11 -0800
Subject: [PATCH 18/20] Speed up and simplify handling of inline decorations
---
src/vs/base/common/color.ts | 2 +-
.../browser/gpu/fullFileRenderStrategy.ts | 80 ++++++++-----------
2 files changed, 34 insertions(+), 48 deletions(-)
diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts
index 67eeeec757a3e..8b1a68294a507 100644
--- a/src/vs/base/common/color.ts
+++ b/src/vs/base/common/color.ts
@@ -676,7 +676,7 @@ export namespace Color {
const b = parseInt(color.groups?.b ?? '0');
return new Color(new RGBA(r, g, b));
}
- // TODO: Support more formats
+ // TODO: Support more formats as needed
return parseNamedKeyword(css);
}
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index bfb27a81d332b..4b3b3c26c0901 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -12,7 +12,7 @@ import type { IViewLineTokens } from '../../common/tokens/lineTokens.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../common/viewEvents.js';
import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';
-import type { ViewLineRenderingData } from '../../common/viewModel.js';
+import type { InlineDecoration, ViewLineRenderingData } from '../../common/viewModel.js';
import type { ViewContext } from '../../common/viewModel/viewContext.js';
import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js';
import type { ITextureAtlasPageGlyph } from './atlas/atlas.js';
@@ -233,8 +233,11 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
}
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
+ // IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the
+ // loop. This is done so we don't need to trust the JIT compiler to do this optimization to
+ // avoid potential additional blocking time in garbage collector which is a common cause of
+ // dropped frames.
+
let chars = '';
let y = 0;
let x = 0;
@@ -251,6 +254,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
let charMetadata = 0;
let lineData: ViewLineRenderingData;
+ let decoration: InlineDecoration;
let content: string = '';
let fillStartIndex = 0;
let fillEndIndex = 0;
@@ -315,8 +319,6 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
}
}
- const decorations = viewportData.getDecorationsInViewport();
-
for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {
// Only attempt to render lines that the GPU renderer can handle
@@ -331,14 +333,10 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
if (upToDateLines.has(y)) {
continue;
}
+
dirtyLineStart = Math.min(dirtyLineStart, y);
dirtyLineEnd = Math.max(dirtyLineEnd, y);
- const inlineDecorations = decorations.filter(e => (
- e.range.startLineNumber <= y && e.range.endLineNumber >= y &&
- e.options.inlineClassName
- ));
-
lineData = viewportData.getViewLineRenderingData(y);
content = lineData.content;
xOffset = 0;
@@ -363,48 +361,36 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
chars = content.charAt(x);
charMetadata = 0;
- // TODO: We'd want to optimize pulling the decorations in order
- const cellDecorations = inlineDecorations.filter(decoration => {
- // This is Range.strictContainsPosition except it's working at the cell level.
- if (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) {
- return false;
- }
- if (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) {
- return false;
- }
- if (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1) {
- return false;
+ // Apply supported inline decoration styles to the cell metadata
+ for (decoration of lineData.inlineDecorations) {
+ // This is Range.strictContainsPosition except it works at the cell level,
+ // it's also inlined to avoid overhead.
+ if (
+ (y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) ||
+ (y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) ||
+ (y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1)
+ ) {
+ continue;
}
- return true;
- });
-
- // Only lines containing fully supported inline decorations should have made it
- // this far.
- const inlineStyles: Map = new Map();
- for (const decoration of cellDecorations) {
- if (!decoration.options.inlineClassName) {
- throw new BugIndicatingError('Unexpected inline decoration without class name');
- }
- const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.options.inlineClassName);
+
+ const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName);
for (const rule of rules) {
for (const r of rule.style) {
- inlineStyles.set(r, rule.styleMap.get(r)?.toString() ?? '');
- }
- }
- }
-
- for (const [key, value] of inlineStyles.entries()) {
- switch (key) {
- case 'color': {
- // TODO: This parsing/error handling should move into canRender so fallback to DOM works
- const parsedColor = Color.Format.CSS.parse(value);
- if (!parsedColor) {
- throw new BugIndicatingError('Invalid color format ' + value);
+ const value = rule.styleMap.get(r)?.toString() ?? '';
+ switch (r) {
+ case 'color': {
+ // TODO: This parsing and error handling should move into canRender so fallback
+ // to DOM works
+ const parsedColor = Color.Format.CSS.parse(value);
+ if (!parsedColor) {
+ throw new BugIndicatingError('Invalid color format ' + value);
+ }
+ charMetadata = parsedColor.toNumber24Bit();
+ break;
+ }
+ default: throw new BugIndicatingError('Unexpected inline decoration style');
}
- charMetadata = parsedColor.toNumber24Bit();
- break;
}
- default: throw new BugIndicatingError('Unexpected inline decoration style');
}
}
From 27687b2229467b1409b51a30f9bd024758feec7e Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Fri, 22 Nov 2024 07:33:44 -0800
Subject: [PATCH 19/20] Make canRender call non-static
Since this now depends on the DOM, it's too difficult and inconsistent to
pass in a DOM node to do the inline decoration test on. It's simplest to
pass it in to whatever view parts need it.
---
src/vs/editor/browser/gpu/fullFileRenderStrategy.ts | 2 +-
src/vs/editor/browser/gpu/raster/glyphRasterizer.ts | 6 ------
src/vs/editor/browser/gpu/viewGpuContext.ts | 8 ++++----
src/vs/editor/browser/view.ts | 4 ++--
src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts | 6 ++----
src/vs/editor/browser/viewParts/viewLines/viewLine.ts | 6 ++----
src/vs/editor/browser/viewParts/viewLines/viewLines.ts | 5 +++--
.../editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts | 4 ++--
8 files changed, 16 insertions(+), 25 deletions(-)
diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
index 4b3b3c26c0901..7ebc66d68e006 100644
--- a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
+++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
@@ -322,7 +322,7 @@ export class FullFileRenderStrategy extends ViewEventHandler implements IGpuRend
for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {
// Only attempt to render lines that the GPU renderer can handle
- if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, viewLineOptions, viewportData, y)) {
+ if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) {
fillStartIndex = ((y - 1) * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell;
fillEndIndex = (y * this._viewGpuContext.maxGpuCols) * Constants.IndicesPerCell;
cellBuffer.fill(0, fillStartIndex, fillEndIndex);
diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
index a0ae8927b832d..5df78bc465e49 100644
--- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
+++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
@@ -127,12 +127,6 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
this._ctx.textBaseline = 'top';
this._ctx.fillText(chars, originX, originY);
- // TODO: Don't draw beyond glyph - how to handle monospace, wide and proportional?
- // TODO: Support strikethrough color
- if (fontStyle & FontStyle.Strikethrough) {
- this._ctx.fillRect(originX, originY + Math.round(devicePixelFontSize / 2), devicePixelFontSize, Math.max(Math.floor(getActiveWindow().devicePixelRatio), 1));
- }
-
const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox);
// const offset = {
diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts
index 883c75d75bdd6..b7c2bb9cbe82d 100644
--- a/src/vs/editor/browser/gpu/viewGpuContext.ts
+++ b/src/vs/editor/browser/gpu/viewGpuContext.ts
@@ -141,7 +141,7 @@ export class ViewGpuContext extends Disposable {
* renderer. Eventually this should trend all lines, except maybe exceptional cases like
* decorations that use class names.
*/
- public static canRender(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean {
+ public canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean {
const data = viewportData.getViewLineRenderingData(lineNumber);
// Check if the line has simple attributes that aren't supported
@@ -158,7 +158,7 @@ export class ViewGpuContext extends Disposable {
if (data.inlineDecorations.length > 0) {
let supported = true;
for (const decoration of data.inlineDecorations) {
- const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
+ const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);
supported &&= styleRules.every(rule => {
// Pseudo classes aren't supported currently
if (rule.selectorText.includes(':')) {
@@ -184,7 +184,7 @@ export class ViewGpuContext extends Disposable {
/**
* Like {@link canRender} but returns detailed information about why the line cannot be rendered.
*/
- public static canRenderDetailed(container: HTMLElement, options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] {
+ public canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] {
const data = viewportData.getViewLineRenderingData(lineNumber);
const reasons: string[] = [];
if (data.containsRTL) {
@@ -201,7 +201,7 @@ export class ViewGpuContext extends Disposable {
const problemSelectors: string[] = [];
const problemRules: string[] = [];
for (const decoration of data.inlineDecorations) {
- const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(container, decoration.inlineClassName);
+ const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);
supported &&= styleRules.every(rule => {
// Pseudo classes aren't supported currently
if (rule.selectorText.includes(':')) {
diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts
index 0b90dab63ed16..10ecd41bb1663 100644
--- a/src/vs/editor/browser/view.ts
+++ b/src/vs/editor/browser/view.ts
@@ -167,7 +167,7 @@ export class View extends ViewEventHandler {
this._viewParts.push(this._scrollbar);
// View Lines
- this._viewLines = new ViewLines(this._context, this._linesContent);
+ this._viewLines = new ViewLines(this._context, this._viewGpuContext, this._linesContent);
if (this._viewGpuContext) {
this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext);
}
@@ -199,7 +199,7 @@ export class View extends ViewEventHandler {
marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context));
if (this._viewGpuContext) {
- marginViewOverlays.addDynamicOverlay(new GpuMarkOverlay(this._context));
+ marginViewOverlays.addDynamicOverlay(new GpuMarkOverlay(this._context, this._viewGpuContext));
}
// Glyph margin widgets
diff --git a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts
index 861ff529cdc47..733cd8e681a0c 100644
--- a/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts
+++ b/src/vs/editor/browser/viewParts/gpuMark/gpuMark.ts
@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { getActiveDocument } from '../../../../base/browser/dom.js';
import * as viewEvents from '../../../common/viewEvents.js';
import { ViewContext } from '../../../common/viewModel/viewContext.js';
import { ViewGpuContext } from '../../gpu/viewGpuContext.js';
@@ -23,7 +22,7 @@ export class GpuMarkOverlay extends DynamicViewOverlay {
private _renderResult: string[] | null;
- constructor(context: ViewContext) {
+ constructor(context: ViewContext, private readonly _viewGpuContext: ViewGpuContext) {
super();
this._context = context;
this._renderResult = null;
@@ -78,8 +77,7 @@ export class GpuMarkOverlay extends DynamicViewOverlay {
const output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
- // TODO: How to get the container?
- const cannotRenderReasons = ViewGpuContext.canRenderDetailed(getActiveDocument().querySelector('.view-lines')!, options, viewportData, lineNumber);
+ const cannotRenderReasons = this._viewGpuContext.canRenderDetailed(options, viewportData, lineNumber);
output[lineIndex] = cannotRenderReasons.length ? `` : '';
}
diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts
index d25238bb4cf54..16229cd928a9a 100644
--- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts
+++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts
@@ -19,7 +19,6 @@ import { EditorFontLigatures } from '../../../common/config/editorOptions.js';
import { DomReadingContext } from './domReadingContext.js';
import type { ViewLineOptions } from './viewLineOptions.js';
import { ViewGpuContext } from '../../gpu/viewGpuContext.js';
-import { getActiveDocument } from '../../../../base/browser/dom.js';
const canUseFastRenderedViewLine = (function () {
if (platform.isNative) {
@@ -55,7 +54,7 @@ export class ViewLine implements IVisibleLine {
private _isMaybeInvalid: boolean;
private _renderedViewLine: IRenderedViewLine | null;
- constructor(options: ViewLineOptions) {
+ constructor(private readonly _viewGpuContext: ViewGpuContext | undefined, options: ViewLineOptions) {
this._options = options;
this._isMaybeInvalid = true;
this._renderedViewLine = null;
@@ -99,8 +98,7 @@ export class ViewLine implements IVisibleLine {
}
public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean {
- // TODO: How to get the container?
- if (this._options.useGpu && ViewGpuContext.canRender(getActiveDocument().querySelector('.view-lines')!, this._options, viewportData, lineNumber)) {
+ if (this._options.useGpu && this._viewGpuContext?.canRender(this._options, viewportData, lineNumber)) {
this._renderedViewLine?.domNode?.domNode.remove();
this._renderedViewLine = null;
return false;
diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts
index b93a0028b0bbf..6de14aedb6aeb 100644
--- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts
+++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts
@@ -25,6 +25,7 @@ import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.j
import { Viewport } from '../../../common/viewModel.js';
import { ViewContext } from '../../../common/viewModel/viewContext.js';
import { ViewLineOptions } from './viewLineOptions.js';
+import type { ViewGpuContext } from '../../gpu/viewGpuContext.js';
class LastRenderedData {
@@ -125,7 +126,7 @@ export class ViewLines extends ViewPart implements IViewLines {
private _stickyScrollEnabled: boolean;
private _maxNumberStickyLines: number;
- constructor(context: ViewContext, linesContent: FastDomNode) {
+ constructor(context: ViewContext, viewGpuContext: ViewGpuContext | undefined, linesContent: FastDomNode) {
super(context);
const conf = this._context.configuration;
@@ -145,7 +146,7 @@ export class ViewLines extends ViewPart implements IViewLines {
this._linesContent = linesContent;
this._textRangeRestingSpot = document.createElement('div');
this._visibleLines = new VisibleLinesCollection({
- createLine: () => new ViewLine(this._viewLineOptions),
+ createLine: () => new ViewLine(viewGpuContext, this._viewLineOptions),
});
this.domNode = this._visibleLines.domNode;
diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts
index cdaa217d5e5df..f483e0eda95e6 100644
--- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts
+++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts
@@ -555,7 +555,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
if (!this._lastViewportData || !this._lastViewLineOptions) {
return undefined;
}
- if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
+ if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
return undefined;
}
@@ -573,7 +573,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
if (!this._lastViewportData || !this._lastViewLineOptions) {
return undefined;
}
- if (!ViewGpuContext.canRender(this._viewGpuContext.canvas.domNode, this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
+ if (!this._viewGpuContext.canRender(this._lastViewLineOptions, this._lastViewportData, lineNumber)) {
return undefined;
}
const lineData = this._lastViewportData.getViewLineRenderingData(lineNumber);
From e6bc2ee20f657fbc8e2ed295da0c8f52bca311f7 Mon Sep 17 00:00:00 2001
From: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Date: Fri, 22 Nov 2024 07:48:05 -0800
Subject: [PATCH 20/20] Get style extraction working for decorations with
multiple class names
---
src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
index c05f2d63418d4..9490d2e1e9aeb 100644
--- a/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
+++ b/src/vs/editor/browser/gpu/decorationCssRuleExtractor.ts
@@ -34,7 +34,7 @@ export class DecorationCssRuleExtractor extends Disposable {
}
// Set up DOM
- this._dummyElement.classList.add(decorationClassName);
+ this._dummyElement.className = decorationClassName;
canvas.appendChild(this._container);
// Get rules
@@ -43,7 +43,6 @@ export class DecorationCssRuleExtractor extends Disposable {
// Tear down DOM
canvas.removeChild(this._container);
- this._dummyElement.classList.remove(decorationClassName);
return rules;
}