From a7cad9d53bab71441bbe4f7259d9e2d9b981ddaa Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 14:58:15 +0200 Subject: [PATCH 01/41] Add injected text decoration fixtures Establish stable dark and light visual baselines for color decorators and inline progress before changing injected text width handling. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../editor/injectedTextDecorations.fixture.ts | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts new file mode 100644 index 00000000000000..b19bee2bf69837 --- /dev/null +++ b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { timeout } from '../../../../../base/common/async.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ICodeEditorWidgetOptions, CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { EditorContributionInstantiation, IEditorContributionDescription } from '../../../../../editor/browser/editorExtensions.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { DocumentColorProvider } from '../../../../../editor/common/languages.js'; +import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; +import { ColorDetector } from '../../../../../editor/contrib/colorPicker/browser/colorDetector.js'; +import '../../../../../editor/contrib/colorPicker/browser/colorPicker.css'; +import { InlineProgressManager } from '../../../../../editor/contrib/inlineProgress/browser/inlineProgress.js'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; + +const colorDetectorContribution: IEditorContributionDescription = { + id: ColorDetector.ID, + ctor: ColorDetector, + instantiation: EditorContributionInstantiation.AfterFirstRender, +}; + +async function renderColorDecorators(context: ComponentFixtureContext): Promise { + const { editor } = createEditor( + context, + '.red { color: #ff0000; }\n.green { color: #00ff00; }\n.blue { color: #0000ff; }', + 'css', + [colorDetectorContribution], + { colorDecorators: true }, + languageFeaturesService => context.disposableStore.add(languageFeaturesService.colorProvider.register('*', new class implements DocumentColorProvider { + provideDocumentColors() { + return [ + { range: new Range(1, 15, 1, 22), color: { red: 1, green: 0, blue: 0, alpha: 1 } }, + { range: new Range(2, 17, 2, 24), color: { red: 0, green: 1, blue: 0, alpha: 1 } }, + { range: new Range(3, 16, 3, 23), color: { red: 0, green: 0, blue: 1, alpha: 1 } }, + ]; + } + + provideColorPresentations() { + return []; + } + })) + ); + editor.getContribution(ColorDetector.ID); + await timeout(0); +} + +async function renderInlineProgress(context: ComponentFixtureContext): Promise { + const { editor, instantiationService } = createEditor(context, 'const result = await work();', 'typescript'); + const progress = context.disposableStore.add(instantiationService.createInstance(InlineProgressManager, 'fixture', editor)); + void progress.showWhile( + { lineNumber: 1, column: 15 }, + 'Computing result', + new Promise(() => { }), + { cancel() { } }, + 0 + ); + await timeout(0); +} + +function createEditor( + context: ComponentFixtureContext, + content: string, + languageId: string, + contributions: IEditorContributionDescription[] = [], + options: ICodeEditorWidgetOptions = {}, + registerLanguageFeatures?: (languageFeaturesService: ILanguageFeaturesService) => void +) { + const { container, disposableStore, theme } = context; + container.style.width = '420px'; + container.style.height = '120px'; + container.style.border = '1px solid var(--vscode-editorWidget-border)'; + + const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + const languageFeaturesService = instantiationService.get(ILanguageFeaturesService); + registerLanguageFeatures?.(languageFeaturesService); + const model = disposableStore.add(createTextModel( + instantiationService, + content, + URI.parse(`inmemory://injected-text/${languageId}`), + languageId + )); + const editor = disposableStore.add(instantiationService.createInstance( + CodeEditorWidget, + container, + { + automaticLayout: true, + fontFamily: 'Consolas, "Courier New", monospace', + fontSize: 14, + glyphMargin: false, + lineNumbers: 'off', + minimap: { enabled: false }, + scrollBeyondLastLine: false, + scrollbar: { horizontal: 'hidden', vertical: 'hidden' }, + wordWrap: 'off', + ...options, + }, + { contributions } + )); + editor.setModel(model); + + return { editor, instantiationService, languageFeaturesService }; +} + +export default defineThemedFixtureGroup({ path: 'editor/' }, { + ColorDecorators: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + render: renderColorDecorators, + }), + InlineProgress: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + render: renderInlineProgress, + }), +}); From 2c853b882ca79db0d5eb94b5cbdd6e1c08ab7c62 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 15:00:32 +0200 Subject: [PATCH 02/41] Refactor line break test helper for injected text Allow focused wrapping tests to supply injected text without changing existing test behavior. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../common/viewModel/monospaceLineBreaksComputer.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 70d687d30e4427..dba9b0681e589b 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -7,6 +7,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { EditorOptions, WrappingIndent } from '../../../common/config/editorOptions.js'; import { FontInfo } from '../../../common/config/fontInfo.js'; import { ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../../common/modelLineProjectionData.js'; +import { LineInjectedText } from '../../../common/textModelEvents.js'; import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js'; function parseAnnotatedText(annotatedText: string): { text: string; indices: number[] } { @@ -44,7 +45,7 @@ function toAnnotatedText(text: string, lineBreakData: ModelLineProjectionData | return actualAnnotatedText; } -function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean, text: string, previousLineBreakData: ModelLineProjectionData | null): ModelLineProjectionData | null { +function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean, text: string, previousLineBreakData: ModelLineProjectionData | null, injectedText: LineInjectedText[] | null = null): ModelLineProjectionData | null { const fontInfo = new FontInfo({ pixelRatio: 1, fontFamily: 'testFontFamily', @@ -68,7 +69,7 @@ function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, return text; }, getLineInjectedText(lineNumber) { - return null; + return injectedText; } }; const lineBreaksComputer = factory.createLineBreaksComputer(context, fontInfo, tabSize, breakAfter, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds); From 691e46209774f7e1feaa6fa482e0b9d4155bf156 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 15:06:35 +0200 Subject: [PATCH 03/41] Stabilize injected text decoration fixtures Remove focus-dependent current-line highlighting so fixture hashes remain stable across clean explorer sessions. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../componentFixtures/editor/injectedTextDecorations.fixture.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts index b19bee2bf69837..a3d8b8618538bc 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts @@ -91,6 +91,7 @@ function createEditor( glyphMargin: false, lineNumbers: 'off', minimap: { enabled: false }, + renderLineHighlight: 'none', scrollBeyondLastLine: false, scrollbar: { horizontal: 'hidden', vertical: 'hidden' }, wordWrap: 'off', From 32f3b94471df105d00466a7538a877c359b5bdf5 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 15:12:23 +0200 Subject: [PATCH 04/41] Carry fixed width metadata through editor rendering Thread optional injected-text width metadata through decoration normalization and line parts without changing wrapping or rendered output. The ordinary decoration path avoids allocating width storage when no fixed width is present. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/editor/common/model.ts | 6 +++ src/vs/editor/common/model/textModel.ts | 2 + .../common/viewLayout/lineDecorations.ts | 44 +++++++++++++++---- src/vs/editor/common/viewLayout/linePart.ts | 5 ++- .../common/viewLayout/viewLineRenderer.ts | 18 ++++---- .../common/viewModel/inlineDecorations.ts | 10 ++++- 6 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 2fc027b34a231c..2793e8f16ba57d 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -347,6 +347,12 @@ export interface InjectedTextOptions { */ readonly inlineClassNameAffectsLetterSpacing?: boolean; + /** + * Overrides the rendered width of this injected text, measured in em. + * @internal + */ + readonly widthInEm?: number; + /** * This field allows to attach data to this injected text. * The data can be read when injected texts at a given position are queried. diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 6d7e5a6be55037..bc81bcf1156755 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2463,6 +2463,7 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt public readonly tokens: TokenArray | null; readonly inlineClassName: string | null; readonly inlineClassNameAffectsLetterSpacing: boolean; + readonly widthInEm: number | undefined; readonly attachedData: unknown | null; readonly cursorStops: model.InjectedTextCursorStops | null; @@ -2471,6 +2472,7 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt this.tokens = options.tokens ?? null; this.inlineClassName = options.inlineClassName || null; this.inlineClassNameAffectsLetterSpacing = options.inlineClassNameAffectsLetterSpacing || false; + this.widthInEm = options.widthInEm; this.attachedData = options.attachedData || null; this.cursorStops = options.cursorStops || null; } diff --git a/src/vs/editor/common/viewLayout/lineDecorations.ts b/src/vs/editor/common/viewLayout/lineDecorations.ts index 3439b945aacc27..4ec815b69dba96 100644 --- a/src/vs/editor/common/viewLayout/lineDecorations.ts +++ b/src/vs/editor/common/viewLayout/lineDecorations.ts @@ -5,7 +5,7 @@ import * as strings from '../../../base/common/strings.js'; import { Constants } from '../../../base/common/uint.js'; -import { InlineDecoration, InlineDecorationType } from '../viewModel/inlineDecorations.js'; +import { InlineDecoration, type InlineDecorationFixedWidth, InlineDecorationType } from '../viewModel/inlineDecorations.js'; import { LinePartMetadata } from './linePart.js'; export class LineDecoration { @@ -15,7 +15,8 @@ export class LineDecoration { public readonly startColumn: number, public readonly endColumn: number, public readonly className: string, - public readonly type: InlineDecorationType + public readonly type: InlineDecorationType, + public readonly fixedWidth: InlineDecorationFixedWidth | undefined = undefined ) { } @@ -25,6 +26,7 @@ export class LineDecoration { && a.endColumn === b.endColumn && a.className === b.className && a.type === b.type + && a.fixedWidth?.widthInEm === b.fixedWidth?.widthInEm ); } @@ -55,7 +57,7 @@ export class LineDecoration { if (dec.endColumn <= startColumn || dec.startColumn >= endColumn) { continue; } - r[rLength++] = new LineDecoration(Math.max(1, dec.startColumn - startColumn + 1), Math.min(lineLength + 1, dec.endColumn - startColumn + 1), dec.className, dec.type); + r[rLength++] = new LineDecoration(Math.max(1, dec.startColumn - startColumn + 1), Math.min(lineLength + 1, dec.endColumn - startColumn + 1), dec.className, dec.type, dec.fixedWidth); } return r; } @@ -85,7 +87,7 @@ export class LineDecoration { const startColumn = (range.startLineNumber === lineNumber ? range.startColumn : minLineColumn); const endColumn = (range.endLineNumber === lineNumber ? range.endColumn : maxLineColumn); - result[resultLen++] = new LineDecoration(startColumn, endColumn, d.inlineClassName, d.type); + result[resultLen++] = new LineDecoration(startColumn, endColumn, d.inlineClassName, d.type, d.fixedWidth); } return result; @@ -123,12 +125,14 @@ export class DecorationSegment { endOffset: number; className: string; metadata: number; + fixedWidth: InlineDecorationFixedWidth | undefined; - constructor(startOffset: number, endOffset: number, className: string, metadata: number) { + constructor(startOffset: number, endOffset: number, className: string, metadata: number, fixedWidth: InlineDecorationFixedWidth | undefined = undefined) { this.startOffset = startOffset; this.endOffset = endOffset; this.className = className; this.metadata = metadata; + this.fixedWidth = fixedWidth; } } @@ -137,6 +141,7 @@ class Stack { private readonly stopOffsets: number[]; private readonly classNames: string[]; private readonly metadata: number[]; + private fixedWidths: (InlineDecorationFixedWidth | undefined)[] | undefined; constructor() { this.stopOffsets = []; @@ -153,6 +158,18 @@ class Stack { return result; } + private _fixedWidth(): InlineDecorationFixedWidth | undefined { + if (!this.fixedWidths) { + return undefined; + } + for (const fixedWidth of this.fixedWidths) { + if (fixedWidth !== undefined) { + return fixedWidth; + } + } + return undefined; + } + public consumeLowerThan(maxStopOffset: number, nextStartOffset: number, result: DecorationSegment[]): number { while (this.count > 0 && this.stopOffsets[0] < maxStopOffset) { @@ -164,30 +181,35 @@ class Stack { } // Basically we are consuming the first i + 1 elements of the stack - result.push(new DecorationSegment(nextStartOffset, this.stopOffsets[i], this.classNames.join(' '), Stack._metadata(this.metadata))); + result.push(new DecorationSegment(nextStartOffset, this.stopOffsets[i], this.classNames.join(' '), Stack._metadata(this.metadata), this._fixedWidth())); nextStartOffset = this.stopOffsets[i] + 1; // Consume them this.stopOffsets.splice(0, i + 1); this.classNames.splice(0, i + 1); this.metadata.splice(0, i + 1); + this.fixedWidths?.splice(0, i + 1); this.count -= (i + 1); } if (this.count > 0 && nextStartOffset < maxStopOffset) { - result.push(new DecorationSegment(nextStartOffset, maxStopOffset - 1, this.classNames.join(' '), Stack._metadata(this.metadata))); + result.push(new DecorationSegment(nextStartOffset, maxStopOffset - 1, this.classNames.join(' '), Stack._metadata(this.metadata), this._fixedWidth())); nextStartOffset = maxStopOffset; } return nextStartOffset; } - public insert(stopOffset: number, className: string, metadata: number): void { + public insert(stopOffset: number, className: string, metadata: number, fixedWidth: InlineDecorationFixedWidth | undefined): void { if (this.count === 0 || this.stopOffsets[this.count - 1] <= stopOffset) { // Insert at the end this.stopOffsets.push(stopOffset); this.classNames.push(className); this.metadata.push(metadata); + if (fixedWidth !== undefined && !this.fixedWidths) { + this.fixedWidths = new Array(this.count); + } + this.fixedWidths?.push(fixedWidth); } else { // Find the insertion position for `stopOffset` for (let i = 0; i < this.count; i++) { @@ -195,6 +217,10 @@ class Stack { this.stopOffsets.splice(i, 0, stopOffset); this.classNames.splice(i, 0, className); this.metadata.splice(i, 0, metadata); + if (fixedWidth !== undefined && !this.fixedWidths) { + this.fixedWidths = new Array(this.count); + } + this.fixedWidths?.splice(i, 0, fixedWidth); break; } } @@ -254,7 +280,7 @@ export class LineDecorationsNormalizer { if (stack.count === 0) { nextStartOffset = currentStartOffset; } - stack.insert(currentEndOffset, className, metadata); + stack.insert(currentEndOffset, className, metadata, d.fixedWidth); } stack.consumeLowerThan(Constants.MAX_SAFE_SMALL_INTEGER, nextStartOffset, result); diff --git a/src/vs/editor/common/viewLayout/linePart.ts b/src/vs/editor/common/viewLayout/linePart.ts index a179915fca60b0..f90223fc6367db 100644 --- a/src/vs/editor/common/viewLayout/linePart.ts +++ b/src/vs/editor/common/viewLayout/linePart.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { InlineDecorationFixedWidth } from '../viewModel/inlineDecorations.js'; + export const enum LinePartMetadata { IS_WHITESPACE = 1, PSEUDO_BEFORE = 2, @@ -23,7 +25,8 @@ export class LinePart { public readonly endIndex: number, public readonly type: string, public readonly metadata: number, - public readonly containsRTL: boolean + public readonly containsRTL: boolean, + public readonly fixedWidth: InlineDecorationFixedWidth | undefined = undefined ) { } public isWhitespace(): boolean { diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 11aca36bbb9a7c..8db001814349fe 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -601,13 +601,13 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: } if (lastSpaceOffset !== -1 && j - currTokenStart >= Constants.LongToken) { // Split at `lastSpaceOffset` + 1 - result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType, tokenMetadata, tokenContainsRTL); + result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType, tokenMetadata, tokenContainsRTL, token.fixedWidth); currTokenStart = lastSpaceOffset + 1; lastSpaceOffset = -1; } } if (currTokenStart !== tokenEndIndex) { - result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL); + result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL, token.fixedWidth); } } else { result[resultLen++] = token; @@ -628,9 +628,9 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: const piecesCount = Math.ceil(diff / Constants.LongToken); for (let j = 1; j < piecesCount; j++) { const pieceEndIndex = lastTokenEndIndex + (j * Constants.LongToken); - result[resultLen++] = new LinePart(pieceEndIndex, tokenType, tokenMetadata, tokenContainsRTL); + result[resultLen++] = new LinePart(pieceEndIndex, tokenType, tokenMetadata, tokenContainsRTL, token.fixedWidth); } - result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL); + result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL, token.fixedWidth); } else { result[resultLen++] = token; } @@ -672,8 +672,8 @@ function splitLeadingWhitespaceFromRTL(lineContent: string, tokens: LinePart[]): // Split the first token into leading whitespace and the rest const result: LinePart[] = []; - result.push(new LinePart(firstNonWhitespaceIndex, firstToken.type, firstToken.metadata, false)); - result.push(new LinePart(firstTokenEndIndex, firstToken.type, firstToken.metadata, firstToken.containsRTL)); + result.push(new LinePart(firstNonWhitespaceIndex, firstToken.type, firstToken.metadata, false, firstToken.fixedWidth)); + result.push(new LinePart(firstTokenEndIndex, firstToken.type, firstToken.metadata, firstToken.containsRTL, firstToken.fixedWidth)); // Add remaining tokens for (let i = 1; i < tokens.length; i++) { @@ -944,12 +944,12 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP if (lineDecoration.endOffset + 1 <= tokenEndIndex) { // This line decoration ends before this token ends lastResultEndIndex = lineDecoration.endOffset + 1; - result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL); + result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL, lineDecoration.fixedWidth); lineDecorationIndex++; } else { // This line decoration continues on to the next token lastResultEndIndex = tokenEndIndex; - result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL); + result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL, lineDecoration.fixedWidth); break; } } @@ -964,7 +964,7 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP if (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) { while (lineDecorationIndex < lineDecorationsLen && lineDecorations[lineDecorationIndex].startOffset === lastTokenEndIndex) { const lineDecoration = lineDecorations[lineDecorationIndex]; - result[resultLen++] = new LinePart(lastResultEndIndex, lineDecoration.className, lineDecoration.metadata, false); + result[resultLen++] = new LinePart(lastResultEndIndex, lineDecoration.className, lineDecoration.metadata, false, lineDecoration.fixedWidth); lineDecorationIndex++; } } diff --git a/src/vs/editor/common/viewModel/inlineDecorations.ts b/src/vs/editor/common/viewModel/inlineDecorations.ts index f6869c57dd334e..59d6e9fdeb4a30 100644 --- a/src/vs/editor/common/viewModel/inlineDecorations.ts +++ b/src/vs/editor/common/viewModel/inlineDecorations.ts @@ -16,11 +16,16 @@ export const enum InlineDecorationType { RegularAffectingLetterSpacing = 3 } +export interface InlineDecorationFixedWidth { + readonly widthInEm: number; +} + export class InlineDecoration { constructor( public readonly range: Range, public readonly inlineClassName: string, - public readonly type: InlineDecorationType + public readonly type: InlineDecorationType, + public readonly fixedWidth: InlineDecorationFixedWidth | undefined = undefined ) { } } @@ -251,7 +256,8 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations const viewLineNumber = this.context.getBaseViewLineNumber(modelLineNumber) + outputLineIndex; const range = new Range(viewLineNumber, start + 1, viewLineNumber, end + 1); const type: InlineDecorationType = options.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular; - inlineDecorations.push(new InlineDecoration(range, options.inlineClassName, type)); + const fixedWidth = options.widthInEm === undefined ? undefined : { widthInEm: options.widthInEm }; + inlineDecorations.push(new InlineDecoration(range, options.inlineClassName, type, fixedWidth)); } } } From 85e97b6573efb9939ab81707bcca00b70a063036 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 15:15:52 +0200 Subject: [PATCH 05/41] Add failing tests for fixed-width injected text Cover wrapping width, atomic multi-character injections, and rendering through a width-enforcing wrapper that preserves decorated token spans. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../viewLayout/viewLineRenderer.test.ts | 11 +++++++ .../monospaceLineBreaksComputer.test.ts | 30 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 475ba0463ffde8..22c5fd624ced6e 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -191,6 +191,17 @@ suite('renderViewLine', () => { assertParts('xyz', 4, [createPart(2, 1), createPart(3, 2)], 'xyz', [[0, [0, 0]], [1, [0, 1]], [2, [1, 0]], [3, [1, 1]]]); }); + test('enforces fixed injected text width without changing decorated spans', () => { + const fixedWidth = { widthInEm: 1 }; + const actual = renderViewLine(createRenderLineInput({ + lineContent: 'hello', + lineTokens: createViewLineTokens([createPart(2, 0), createPart(5, 1)]), + lineDecorations: [new LineDecoration(1, 6, 'injected', InlineDecorationType.RegularAffectingLetterSpacing, fixedWidth)] + })); + + assert.strictEqual(actual.html, 'hello'); + }); + // overflow test('overflow', async () => { const _actual = renderViewLine(createRenderLineInput({ diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index dba9b0681e589b..8a3934236bcdaf 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -135,6 +135,36 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { assertLineBreaks(factory, 4, 5, 'aa.|(.).|aaa'); }); + test('accounts for fixed injected text width when wrapping', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 5, 2, WrappingIndent.None, 'normal', false, 'abcdef', null, [ + new LineInjectedText(0, 1, 5, { content: '\xa0', widthInEm: 1 }, 0) + ]); + + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [4, 7], + breakOffsetsVisibleColumn: [4, 8] + }); + }); + + test('treats multi-character fixed-width injected text as an atomic span', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 5, 2, WrappingIndent.None, 'normal', false, 'abcdef', null, [ + new LineInjectedText(0, 1, 4, { content: 'hello', widthInEm: 1 }, 0) + ]); + + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [8, 11], + breakOffsetsVisibleColumn: [5, 8] + }); + }); + function assertLineBreakDataEqual(a: ModelLineProjectionData | null, b: ModelLineProjectionData | null): void { if (!a || !b) { assert.deepStrictEqual(a, b); From a0b9cd9a3465e0dabcca608d6f249d746a392ba1 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 16:10:17 +0200 Subject: [PATCH 06/41] Add proportional-font injected text fixture Exercise fixed-width wrapping through the real DOM line-break computer and use registered editor contribution metadata in the existing visual fixtures. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../editor/injectedTextDecorations.fixture.ts | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts index a3d8b8618538bc..eef22e0479e647 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts @@ -4,22 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { timeout } from '../../../../../base/common/async.js'; +import { toDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; -import { ICodeEditorWidgetOptions, CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { EditorContributionInstantiation, IEditorContributionDescription } from '../../../../../editor/browser/editorExtensions.js'; +import { IEditorConstructionOptions } from '../../../../../editor/browser/config/editorConfiguration.js'; +import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { EditorExtensionsRegistry, IEditorContributionDescription } from '../../../../../editor/browser/editorExtensions.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { DocumentColorProvider } from '../../../../../editor/common/languages.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { ColorDetector } from '../../../../../editor/contrib/colorPicker/browser/colorDetector.js'; +import '../../../../../editor/contrib/colorPicker/browser/colorPickerContribution.js'; import '../../../../../editor/contrib/colorPicker/browser/colorPicker.css'; import { InlineProgressManager } from '../../../../../editor/contrib/inlineProgress/browser/inlineProgress.js'; import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; -const colorDetectorContribution: IEditorContributionDescription = { - id: ColorDetector.ID, - ctor: ColorDetector, - instantiation: EditorContributionInstantiation.AfterFirstRender, -}; +const colorDetectorContribution = EditorExtensionsRegistry.getSomeEditorContributions([ColorDetector.ID])[0]; async function renderColorDecorators(context: ComponentFixtureContext): Promise { const { editor } = createEditor( @@ -59,12 +58,41 @@ async function renderInlineProgress(context: ComponentFixtureContext): Promise decorations.clear())); +} + function createEditor( context: ComponentFixtureContext, content: string, languageId: string, contributions: IEditorContributionDescription[] = [], - options: ICodeEditorWidgetOptions = {}, + options: IEditorConstructionOptions = {}, registerLanguageFeatures?: (languageFeaturesService: ILanguageFeaturesService) => void ) { const { container, disposableStore, theme } = context; @@ -113,4 +141,8 @@ export default defineThemedFixtureGroup({ path: 'editor/' }, { labels: { kind: 'screenshot', blocksCi: true }, render: renderInlineProgress, }), + FixedWidthWrapping: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + render: renderFixedWidthWrapping, + }), }); From fd06444a586d28172d59be00207c693e302b1eec Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 16:16:23 +0200 Subject: [PATCH 07/41] Exercise advanced injected text wrapping Force the proportional-font fixture through the DOM line-break computer so it validates atomic fixed-width wrapping rather than the monospace fallback. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../componentFixtures/editor/injectedTextDecorations.fixture.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts index eef22e0479e647..04e870f07a4a9d 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts @@ -68,6 +68,7 @@ function renderFixedWidthWrapping(context: ComponentFixtureContext): void { fontFamily: 'Arial, sans-serif', wordWrap: 'wordWrapColumn', wordWrapColumn: 12, + wrappingStrategy: 'advanced', wrappingIndent: 'none', } ); From 9f31f53ca9d4c57a618e6979af29b3924b30269b Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 16:17:43 +0200 Subject: [PATCH 08/41] Support fixed-width injected text wrapping Treat widthInEm injections as atomic in both monospace and DOM line-break computation and enforce the width on the existing flat renderer span. Reject custom tokens with fixed widths and retain the unchanged fast path for ordinary lines. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/view/domLineBreaksComputer.ts | 200 ++++++++++++++++-- src/vs/editor/common/model.ts | 2 +- src/vs/editor/common/model/textModel.ts | 3 + src/vs/editor/common/textModelEvents.ts | 24 +++ .../common/viewLayout/viewLineRenderer.ts | 16 +- .../viewModel/monospaceLineBreaksComputer.ts | 51 +++-- .../viewLayout/viewLineRenderer.test.ts | 10 +- .../common/viewModel/lineBreakData.test.ts | 12 ++ 8 files changed, 276 insertions(+), 42 deletions(-) diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 1c88653206fac9..319135a05b8ece 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -12,7 +12,7 @@ import { WrappingIndent } from '../../common/config/editorOptions.js'; import { StringBuilder } from '../../common/core/stringBuilder.js'; import { InjectedTextOptions } from '../../common/model.js'; import { ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js'; -import { LineInjectedText } from '../../common/textModelEvents.js'; +import { LineInjectedText, LineInjectedTextFixedWidth } from '../../common/textModelEvents.js'; import { FontInfo } from '../../common/config/fontInfo.js'; const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value }); @@ -78,10 +78,13 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont const wrappedTextIndentLengths: number[] = []; const renderLineContents: string[] = []; const allCharOffsets: number[][] = []; + const allSpanStartOffsets: (number[] | null)[] = []; const allVisibleColumns: number[][] = []; for (let i = 0; i < lineNumbers.length; i++) { const lineNumber = lineNumbers[i]; - const lineContent = LineInjectedText.applyInjectedText(context.getLineContent(lineNumber), context.getLineInjectedText(lineNumber)); + const injectedTexts = context.getLineInjectedText(lineNumber); + const lineContent = LineInjectedText.applyInjectedText(context.getLineContent(lineNumber), injectedTexts); + const fixedWidthRanges = LineInjectedText.getFixedWidthRanges(injectedTexts); let firstNonWhitespaceIndex = 0; let wrappedTextIndentLength = 0; @@ -96,13 +99,20 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont } else { // Track existing indent + let fixedWidthRangeIndex = 0; for (let i = 0; i < firstNonWhitespaceIndex; i++) { - const charWidth = ( - lineContent.charCodeAt(i) === CharCode.Tab + const fixedWidthRange = fixedWidthRanges?.[fixedWidthRangeIndex]; + const isFixedWidthStart = fixedWidthRange?.startOffset === i; + const charWidth = isFixedWidthStart + ? fixedWidthRange.widthInEm * fontInfo.fontSize / fontInfo.spaceWidth + : lineContent.charCodeAt(i) === CharCode.Tab ? (tabSize - (wrappedTextIndentLength % tabSize)) - : 1 - ); + : 1; wrappedTextIndentLength += charWidth; + if (isFixedWidthStart) { + i = fixedWidthRange.endOffset - 1; + fixedWidthRangeIndex++; + } } const indentWidth = Math.ceil(fontInfo.spaceWidth * wrappedTextIndentLength); @@ -118,11 +128,22 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont } const renderLineContent = lineContent.substr(firstNonWhitespaceIndex); - const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength); + const shiftedFixedWidthRanges = firstNonWhitespaceIndex === 0 + ? fixedWidthRanges + : fixedWidthRanges?.map(range => ({ + startOffset: Math.max(0, range.startOffset - firstNonWhitespaceIndex), + endOffset: range.endOffset - firstNonWhitespaceIndex, + widthInEm: range.widthInEm + })).filter(range => range.endOffset > 0) ?? null; + const renderLineFixedWidthRanges = shiftedFixedWidthRanges?.length ? shiftedFixedWidthRanges : null; + const tmp = renderLineFixedWidthRanges + ? renderLineWithFixedWidths(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength, renderLineFixedWidthRanges, fontInfo.fontSize / fontInfo.typicalHalfwidthCharacterWidth) + : renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength); firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex; wrappedTextIndentLengths[i] = wrappedTextIndentLength; renderLineContents[i] = renderLineContent; allCharOffsets[i] = tmp[0]; + allSpanStartOffsets[i] = tmp[2]; allVisibleColumns[i] = tmp[1]; } const html = sb.build(); @@ -149,7 +170,7 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont for (let i = 0; i < lineNumbers.length; i++) { const lineNumber = lineNumbers[i]; const lineDomNode = lineDomNodes[i]; - const breakOffsets: number[] | null = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i]); + const breakOffsets: number[] | null = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i], allSpanStartOffsets[i]); if (breakOffsets === null) { result[i] = createEmptyLineBreakWithPossiblyInjectedText(lineNumber); continue; @@ -193,7 +214,7 @@ const enum Constants { SPAN_MODULO_LIMIT = 16384 } -function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number): [number[], number[]] { +function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number): [number[], number[], null] { if (wrappingIndentLength !== 0) { const hangingOffset = String(wrappingIndentLength); @@ -294,10 +315,126 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: sb.appendString(''); - return [charOffsets, visibleColumns]; + return [charOffsets, visibleColumns, null]; } -function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[]): number[] | null { +function renderLineWithFixedWidths(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly LineInjectedTextFixedWidth[], columnsPerEm: number): [number[], number[], number[]] { + if (wrappingIndentLength !== 0) { + const hangingOffset = String(wrappingIndentLength); + sb.appendString('
'); + + const len = lineContent.length; + let visibleColumn = initialVisibleColumn; + let charOffset = 0; + let fixedWidthRangeIndex = 0; + const charOffsets: number[] = []; + const spanStartOffsets: number[] = [0]; + const visibleColumns: number[] = []; + let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : CharCode.Null); + + sb.appendString(''); + for (let charIndex = 0; charIndex < len; charIndex++) { + const fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex]; + const startsFixedWidth = fixedWidthRange?.startOffset === charIndex; + if (startsFixedWidth) { + sb.appendString(''); + spanStartOffsets.push(charOffset); + } else if (!fixedWidthRange && charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) { + sb.appendString(''); + spanStartOffsets.push(charOffset); + } + + charOffsets[charIndex] = charOffset; + visibleColumns[charIndex] = visibleColumn; + const charCode = nextCharCode; + nextCharCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null); + let producedCharacters = 1; + let charWidth = 1; + switch (charCode) { + case CharCode.Tab: + producedCharacters = (tabSize - (visibleColumn % tabSize)); + charWidth = producedCharacters; + for (let space = 1; space <= producedCharacters; space++) { + if (space < producedCharacters) { + sb.appendCharCode(0xA0); + } else { + sb.appendASCIICharCode(CharCode.Space); + } + } + break; + case CharCode.Space: + if (nextCharCode === CharCode.Space) { + sb.appendCharCode(0xA0); + } else { + sb.appendASCIICharCode(CharCode.Space); + } + break; + case CharCode.LessThan: + sb.appendString('<'); + break; + case CharCode.GreaterThan: + sb.appendString('>'); + break; + case CharCode.Ampersand: + sb.appendString('&'); + break; + case CharCode.Null: + sb.appendString('�'); + break; + case CharCode.UTF8_BOM: + case CharCode.LINE_SEPARATOR: + case CharCode.PARAGRAPH_SEPARATOR: + case CharCode.NEXT_LINE: + sb.appendCharCode(0xFFFD); + break; + default: + if (strings.isFullWidthCharacter(charCode)) { + charWidth++; + } + if (charCode < 32) { + sb.appendCharCode(9216 + charCode); + } else { + sb.appendCharCode(charCode); + } + } + charOffset += producedCharacters; + if (startsFixedWidth) { + charWidth = fixedWidthRange.widthInEm * columnsPerEm; + } else if (fixedWidthRange && charIndex > fixedWidthRange.startOffset && charIndex < fixedWidthRange.endOffset) { + charWidth = 0; + } + visibleColumn += charWidth; + + if (fixedWidthRange && charIndex + 1 === fixedWidthRange.endOffset) { + sb.appendString(''); + if (fixedWidthRange.endOffset < len) { + sb.appendString(''); + spanStartOffsets.push(charOffset); + } + fixedWidthRangeIndex++; + } + } + if (fixedWidthRanges[fixedWidthRanges.length - 1].endOffset < len) { + sb.appendString(''); + } + charOffsets[lineContent.length] = charOffset; + visibleColumns[lineContent.length] = visibleColumn; + sb.appendString('
'); + return [charOffsets, visibleColumns, spanStartOffsets]; +} + +function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[], spanStartOffsets: number[] | null = null): number[] | null { if (lineContent.length <= 1) { return null; } @@ -305,7 +442,7 @@ function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: const breakOffsets: number[] = []; try { - discoverBreaks(range, spans, charOffsets, 0, null, lineContent.length - 1, null, breakOffsets); + discoverBreaks(range, spans, charOffsets, spanStartOffsets, 0, null, lineContent.length - 1, null, breakOffsets); } catch (err) { console.error(err); return null; @@ -319,13 +456,13 @@ function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: return breakOffsets; } -function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: number[], low: number, lowRects: DOMRectList | null, high: number, highRects: DOMRectList | null, result: number[]): void { +function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: number[], spanStartOffsets: number[] | null, low: number, lowRects: DOMRectList | null, high: number, highRects: DOMRectList | null, result: number[]): void { if (low === high) { return; } - lowRects = lowRects || readClientRect(range, spans, charOffsets[low], charOffsets[low + 1]); - highRects = highRects || readClientRect(range, spans, charOffsets[high], charOffsets[high + 1]); + lowRects = lowRects || readClientRect(range, spans, charOffsets[low], charOffsets[low + 1], spanStartOffsets); + highRects = highRects || readClientRect(range, spans, charOffsets[high], charOffsets[high + 1], spanStartOffsets); if (Math.abs(lowRects[0].top - highRects[0].top) <= 0.1) { // same line @@ -340,13 +477,34 @@ function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: num } const mid = low + ((high - low) / 2) | 0; - const midRects = readClientRect(range, spans, charOffsets[mid], charOffsets[mid + 1]); - discoverBreaks(range, spans, charOffsets, low, lowRects, mid, midRects, result); - discoverBreaks(range, spans, charOffsets, mid, midRects, high, highRects, result); + const midRects = readClientRect(range, spans, charOffsets[mid], charOffsets[mid + 1], spanStartOffsets); + discoverBreaks(range, spans, charOffsets, spanStartOffsets, low, lowRects, mid, midRects, result); + discoverBreaks(range, spans, charOffsets, spanStartOffsets, mid, midRects, high, highRects, result); } -function readClientRect(range: Range, spans: HTMLSpanElement[], startOffset: number, endOffset: number): DOMRectList { - range.setStart(spans[(startOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, startOffset % Constants.SPAN_MODULO_LIMIT); - range.setEnd(spans[(endOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, endOffset % Constants.SPAN_MODULO_LIMIT); +function readClientRect(range: Range, spans: HTMLSpanElement[], startOffset: number, endOffset: number, spanStartOffsets: number[] | null): DOMRectList { + if (!spanStartOffsets) { + range.setStart(spans[(startOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, startOffset % Constants.SPAN_MODULO_LIMIT); + range.setEnd(spans[(endOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, endOffset % Constants.SPAN_MODULO_LIMIT); + return range.getClientRects(); + } + const startSpanIndex = findSpanIndex(spanStartOffsets, startOffset); + const endSpanIndex = findSpanIndex(spanStartOffsets, endOffset); + range.setStart(spans[startSpanIndex].firstChild!, startOffset - spanStartOffsets[startSpanIndex]); + range.setEnd(spans[endSpanIndex].firstChild!, endOffset - spanStartOffsets[endSpanIndex]); return range.getClientRects(); } + +function findSpanIndex(spanStartOffsets: readonly number[], offset: number): number { + let low = 0; + let high = spanStartOffsets.length; + while (low < high) { + const mid = (low + high) >>> 1; + if (spanStartOffsets[mid] <= offset) { + low = mid + 1; + } else { + high = mid; + } + } + return low - 1; +} diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 2793e8f16ba57d..457c5704bef283 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -348,7 +348,7 @@ export interface InjectedTextOptions { readonly inlineClassNameAffectsLetterSpacing?: boolean; /** - * Overrides the rendered width of this injected text, measured in em. + * Overrides the rendered width of this injected text, measured in em. Cannot be combined with {@link tokens}. * @internal */ readonly widthInEm?: number; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index bc81bcf1156755..62d1579f571260 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2468,6 +2468,9 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt readonly cursorStops: model.InjectedTextCursorStops | null; private constructor(options: model.InjectedTextOptions) { + if (options.widthInEm !== undefined && options.tokens) { + throw new BugIndicatingError('Injected text cannot define both tokens and widthInEm'); + } this.content = options.content || ''; this.tokens = options.tokens ?? null; this.inlineClassName = options.inlineClassName || null; diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 1f504db5852e61..d4548299265447 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -254,6 +254,24 @@ export class LineInjectedText { return result; } + public static getFixedWidthRanges(injectedTexts: LineInjectedText[] | null): LineInjectedTextFixedWidth[] | null { + let result: LineInjectedTextFixedWidth[] | null = null; + let injectedTextLength = 0; + for (const injectedText of injectedTexts ?? []) { + const startOffset = injectedText.column - 1 + injectedTextLength; + const endOffset = startOffset + injectedText.options.content.length; + if (injectedText.options.widthInEm !== undefined) { + (result ??= []).push({ + startOffset, + endOffset, + widthInEm: injectedText.options.widthInEm + }); + } + injectedTextLength += injectedText.options.content.length; + } + return result; + } + public static fromDecorations(decorations: IModelDecoration[]): LineInjectedText[] { const result: LineInjectedText[] = []; for (const decoration of decorations) { @@ -301,6 +319,12 @@ export class LineInjectedText { } } +export interface LineInjectedTextFixedWidth { + readonly startOffset: number; + readonly endOffset: number; + readonly widthInEm: number; +} + /** * An event describing that a line has changed in a model. * @internal diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 8db001814349fe..aa3d160b12a641 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -588,7 +588,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: for (let i = 0, len = tokens.length; i < len; i++) { const token = tokens[i]; const tokenEndIndex = token.endIndex; - if (lastTokenEndIndex + Constants.LongToken < tokenEndIndex) { + if (!token.fixedWidth && lastTokenEndIndex + Constants.LongToken < tokenEndIndex) { const tokenType = token.type; const tokenMetadata = token.metadata; const tokenContainsRTL = token.containsRTL; @@ -621,7 +621,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: const token = tokens[i]; const tokenEndIndex = token.endIndex; const diff = (tokenEndIndex - lastTokenEndIndex); - if (diff > Constants.LongToken) { + if (!token.fixedWidth && diff > Constants.LongToken) { const tokenType = token.type; const tokenMetadata = token.metadata; const tokenContainsRTL = token.containsRTL; @@ -1011,13 +1011,23 @@ function _renderLine(input: ResolvedRenderLineInput, sb: StringBuilder): RenderL const partEndIndex = part.endIndex; const partType = part.type; const partContainsRTL = part.containsRTL; + const partWidthInEm = part.fixedWidth?.widthInEm; const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && part.isWhitespace()); const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements); const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.isPseudoAfter()); charOffsetInPart = 0; sb.appendString('xyz', [[0, [0, 0]], [1, [0, 1]], [2, [1, 0]], [3, [1, 1]]]); }); - test('enforces fixed injected text width without changing decorated spans', () => { + test('enforces fixed injected text width on a flat span', () => { const fixedWidth = { widthInEm: 1 }; const actual = renderViewLine(createRenderLineInput({ - lineContent: 'hello', - lineTokens: createViewLineTokens([createPart(2, 0), createPart(5, 1)]), - lineDecorations: [new LineDecoration(1, 6, 'injected', InlineDecorationType.RegularAffectingLetterSpacing, fixedWidth)] + lineContent: '\xa0', + lineTokens: createViewLineTokens([createPart(1, 0)]), + lineDecorations: [new LineDecoration(1, 2, 'injected', InlineDecorationType.RegularAffectingLetterSpacing, fixedWidth)] })); - assert.strictEqual(actual.html, 'hello'); + assert.strictEqual(actual.html, '\xa0'); }); // overflow diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index b621632fe1d39a..b2b6835327f9f7 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -8,6 +8,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { PositionAffinity } from '../../../common/model.js'; import { ModelDecorationInjectedTextOptions } from '../../../common/model/textModel.js'; import { ModelLineProjectionData } from '../../../common/modelLineProjectionData.js'; +import { TokenArray, TokenInfo } from '../../../common/tokens/lineTokens.js'; suite('Editor ViewModel - LineBreakData', () => { @@ -20,6 +21,17 @@ suite('Editor ViewModel - LineBreakData', () => { assert.strictEqual(data.translateToInputOffset(1, 60), 150); }); + test('fixed width cannot be combined with tokens', () => { + assert.throws( + () => ModelDecorationInjectedTextOptions.from({ + content: 'text', + tokens: TokenArray.create([new TokenInfo(4, 0)]), + widthInEm: 1 + }), + /Injected text cannot define both tokens and widthInEm/ + ); + }); + function sequence(length: number, start = 0): number[] { const result = new Array(); for (let i = 0; i < length; i++) { From ab76b1a14f5ca0f0824e1371bf5ba47cfba65455 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 16:21:22 +0200 Subject: [PATCH 09/41] Account for inline progress decoration width Declare the existing 1em inline progress placeholder width for wrapping. Its computed geometry and component fixture screenshot remain unchanged. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts index 5b238c214768d7..e111516e10f821 100644 --- a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts +++ b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts @@ -27,6 +27,7 @@ const inlineProgressDecoration = ModelDecorationOptions.register({ content: noBreakWhitespace, inlineClassName: 'inline-editor-progress-decoration', inlineClassNameAffectsLetterSpacing: true, + widthInEm: 1, } }); From be9931518d86a62a230f3ba10bfcdc5e47faad42 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 16:23:51 +0200 Subject: [PATCH 10/41] Account for inlay hint spacer width Express the existing rounded pixel spacer width relative to the editor font so wrapping reserves the same width that CSS already renders. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../inlayHints/browser/inlayHintsController.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 6c1c58c4f8ac14..6707ffde0d05d3 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -532,14 +532,17 @@ export class InlayHintsController implements IEditorContribution { } // utils to collect/create injected text decorations + const { fontSize, fontFamily, padding, isUniform } = this._getLayoutInfo(); + const editorFontSize = this._editor.getOption(EditorOption.fontSize); const newDecorationsData: InlayHintDecorationRenderInfo[] = []; - const addInjectedText = (item: InlayHintItem, ref: ClassNameReference, content: string, cursorStops: InjectedTextCursorStops, attachedData?: RenderedInlayHintLabelPart | object): void => { + const addInjectedText = (item: InlayHintItem, ref: ClassNameReference, content: string, cursorStops: InjectedTextCursorStops, attachedData?: RenderedInlayHintLabelPart | object, widthInEm?: number): void => { const opts: InjectedTextOptions = { content, inlineClassNameAffectsLetterSpacing: true, inlineClassName: ref.className, cursorStops, - attachedData + attachedData, + widthInEm }; newDecorationsData.push({ item, @@ -559,16 +562,16 @@ export class InlayHintsController implements IEditorContribution { }; const addInjectedWhitespace = (item: InlayHintItem, isLast: boolean): void => { + const widthInPixels = (fontSize / 3) | 0; const marginRule = this._ruleFactory.createClassNameRef({ - width: `${(fontSize / 3) | 0}px`, + width: `${widthInPixels}px`, display: 'inline-block' }); - addInjectedText(item, marginRule, '\u200a', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None, InlayHintsController._whitespaceData); + addInjectedText(item, marginRule, '\u200a', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None, InlayHintsController._whitespaceData, widthInPixels / editorFontSize); }; // - const { fontSize, fontFamily, padding, isUniform } = this._getLayoutInfo(); const maxLength = this._editor.getOption(EditorOption.inlayHints).maximumLength; const fontFamilyVar = '--code-editorInlayHintsFontFamily'; this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily); From e3f5e3b2913de04efea0fffdc931cb1992c869d7 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 16:30:34 +0200 Subject: [PATCH 11/41] Account for color decorator width when wrapping Reserve the existing 1.2em occupied width while retaining the original 0.8em square and margins. Dark and light component fixture hashes remain identical. Fixes #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/vs/editor/contrib/colorPicker/browser/colorDetector.ts | 1 + src/vs/editor/contrib/colorPicker/browser/colorPicker.css | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index 506be419193df5..9200278b6bbdfa 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -231,6 +231,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { content: noBreakWhitespace, inlineClassName: `${ref.className} colorpicker-color-decoration`, inlineClassNameAffectsLetterSpacing: true, + widthInEm: 1.2, attachedData: ColorDecorationInjectedTextMarker } } diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css index f484517caba972..e3e9b7d05fb0da 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css +++ b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css @@ -16,7 +16,7 @@ border: solid 0.1em #000; box-sizing: border-box; margin: 0.1em 0.2em 0 0.2em; - width: 0.8em; + width: 0.8em !important; height: 0.8em; line-height: 0.8em; display: inline-block; From 4d66db54c8dd0c9db3dd00fb979541cd9cf23c38 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 16:54:08 +0200 Subject: [PATCH 12/41] Add fixed-width injected text coverage Cover adjacent and oversized atomic injections, render the real padded inlay hint contribution in component fixtures, and add approved dark/light visual expectations for all fixed-width scenarios. Refs #32856 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/inlayHintsController.ts | 11 ++-- .../monospaceLineBreaksComputer.test.ts | 31 +++++++++++ .../editor/injectedTextDecorations.fixture.ts | 53 +++++++++++++++++-- .../browser/componentFixtures/fixtureUtils.ts | 2 + 4 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 6707ffde0d05d3..3e2b2f06b222c7 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -40,7 +40,13 @@ import { Position } from '../../../common/core/position.js'; // --- hint caching service (per session) -class InlayHintsCache { +export interface IInlayHintsCache { + readonly _serviceBrand: undefined; + get(model: ITextModel): InlayHintItem[] | undefined; + set(model: ITextModel, value: InlayHintItem[]): void; +} + +class InlayHintsCache implements IInlayHintsCache { declare readonly _serviceBrand: undefined; @@ -61,8 +67,7 @@ class InlayHintsCache { } } -interface IInlayHintsCache extends InlayHintsCache { } -const IInlayHintsCache = createDecorator('IInlayHintsCache'); +export const IInlayHintsCache = createDecorator('IInlayHintsCache'); registerSingleton(IInlayHintsCache, InlayHintsCache, InstantiationType.Delayed); // --- rendered label diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 8a3934236bcdaf..108421300977fd 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -165,6 +165,37 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { }); }); + test('treats adjacent fixed-width injected texts as separate atomic spans', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 5, 2, WrappingIndent.None, 'normal', false, 'abcdef', null, [ + new LineInjectedText(0, 1, 4, { content: 'x', widthInEm: 0.5 }, 0), + new LineInjectedText(0, 1, 4, { content: 'y', widthInEm: 1 }, 1) + ]); + + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [4, 8], + breakOffsetsVisibleColumn: [4, 9] + }); + }); + + test('keeps fixed-width injected text wider than the wrap column atomic', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 5, 2, WrappingIndent.None, 'normal', false, 'abcdef', null, [ + new LineInjectedText(0, 1, 4, { content: 'x', widthInEm: 3 }, 0) + ]); + + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [3, 4, 7], + breakOffsetsVisibleColumn: [3, 9, 12] + }); + }); + function assertLineBreakDataEqual(a: ModelLineProjectionData | null, b: ModelLineProjectionData | null): void { if (!a || !b) { assert.deepStrictEqual(a, b); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts index 04e870f07a4a9d..d6567b21f59da8 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts @@ -10,15 +10,18 @@ import { IEditorConstructionOptions } from '../../../../../editor/browser/config import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; import { EditorExtensionsRegistry, IEditorContributionDescription } from '../../../../../editor/browser/editorExtensions.js'; import { Range } from '../../../../../editor/common/core/range.js'; -import { DocumentColorProvider } from '../../../../../editor/common/languages.js'; +import { DocumentColorProvider, InlayHintsProvider } from '../../../../../editor/common/languages.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { ColorDetector } from '../../../../../editor/contrib/colorPicker/browser/colorDetector.js'; import '../../../../../editor/contrib/colorPicker/browser/colorPickerContribution.js'; import '../../../../../editor/contrib/colorPicker/browser/colorPicker.css'; +import { IInlayHintsCache, InlayHintsController } from '../../../../../editor/contrib/inlayHints/browser/inlayHintsController.js'; +import '../../../../../editor/contrib/inlayHints/browser/inlayHintsContribution.js'; import { InlineProgressManager } from '../../../../../editor/contrib/inlineProgress/browser/inlineProgress.js'; -import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; +import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup, ServiceRegistration } from '../fixtureUtils.js'; const colorDetectorContribution = EditorExtensionsRegistry.getSomeEditorContributions([ColorDetector.ID])[0]; +const inlayHintsContribution = EditorExtensionsRegistry.getSomeEditorContributions([InlayHintsController.ID])[0]; async function renderColorDecorators(context: ComponentFixtureContext): Promise { const { editor } = createEditor( @@ -58,6 +61,36 @@ async function renderInlineProgress(context: ComponentFixtureContext): Promise { + const { editor } = createEditor( + context, + 'const value = computeResult();', + 'typescript', + [inlayHintsContribution], + { inlayHints: { enabled: 'on', fontSize: 12 } }, + languageFeaturesService => context.disposableStore.add(languageFeaturesService.inlayHintsProvider.register('*', new class implements InlayHintsProvider { + provideInlayHints() { + return { + hints: [{ + label: ': number', + position: { lineNumber: 1, column: 12 }, + paddingLeft: true, + paddingRight: true, + }], + dispose() { } + }; + } + })), + registration => registration.defineInstance(IInlayHintsCache, { + _serviceBrand: undefined, + get: () => undefined, + set: () => { }, + }) + ); + editor.getContribution(InlayHintsController.ID); + await timeout(50); +} + function renderFixedWidthWrapping(context: ComponentFixtureContext): void { const { editor } = createEditor( context, @@ -94,14 +127,18 @@ function createEditor( languageId: string, contributions: IEditorContributionDescription[] = [], options: IEditorConstructionOptions = {}, - registerLanguageFeatures?: (languageFeaturesService: ILanguageFeaturesService) => void + registerLanguageFeatures?: (languageFeaturesService: ILanguageFeaturesService) => void, + registerServices?: (registration: ServiceRegistration) => void ) { const { container, disposableStore, theme } = context; container.style.width = '420px'; container.style.height = '120px'; container.style.border = '1px solid var(--vscode-editorWidget-border)'; - const instantiationService = createEditorServices(disposableStore, { colorTheme: theme }); + const instantiationService = createEditorServices(disposableStore, { + colorTheme: theme, + additionalServices: registerServices, + }); const languageFeaturesService = instantiationService.get(ILanguageFeaturesService); registerLanguageFeatures?.(languageFeaturesService); const model = disposableStore.add(createTextModel( @@ -136,14 +173,22 @@ function createEditor( export default defineThemedFixtureGroup({ path: 'editor/' }, { ColorDecorators: defineComponentFixture({ labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['Three CSS declarations appear on separate lines. Each hexadecimal color is preceded by a square swatch whose fill matches the value. Every swatch is the same size, has a visible contrasting border, and is vertically aligned with its line of text.'], render: renderColorDecorators, }), InlineProgress: defineComponentFixture({ labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['A single TypeScript statement appears on one line. A small inline progress placeholder separates the equals sign from await without changing the line height or vertical alignment.'], render: renderInlineProgress, }), + InlayHints: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['A single TypeScript statement contains a muted : number inlay hint after value. Narrow, equal-width spaces separate the hint from the source text on both sides, and all content stays on one baseline.'], + render: renderInlayHints, + }), FixedWidthWrapping: defineComponentFixture({ labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['Proportional-font text wraps to three lines: alpha, beta gamma, and delta epsilon. The invisible fixed-width injection after alpha occupies enough horizontal space to move beta to the second line without creating visible content or horizontal overflow.'], render: renderFixedWidthWrapping, }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts index 25dc8c19af7492..850125fccf9c88 100644 --- a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts @@ -879,6 +879,7 @@ export interface ComponentFixtureOptions { labels?: ThemedFixtureGroupLabels; virtualTime?: { enabled?: boolean; durationMs?: number; teardownDrainMs?: number }; additionalThemes?: readonly ComponentFixtureAdditionalTheme[]; + expectedVisualDescriptions?: readonly string[]; } type ThemedFixtures = ReturnType; @@ -910,6 +911,7 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed isolation: 'none', displayMode: { type: 'component' }, background: themeVariant.background, + expectedVisualDescriptions: options.expectedVisualDescriptions, inputSchema: fixtureInputSchema, inputControls: { reverseStylesheets: { placement: 'toolbar', label: 'Reverse Stylesheets' }, From 02e2de0acabd07ea7bbf75c9a71990580b9b338c Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 17:11:47 +0200 Subject: [PATCH 13/41] Fix fixed-width injected text rendering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/view/domLineBreaksComputer.ts | 2 +- .../common/viewLayout/viewLineRenderer.ts | 52 ++++++++++++++++++- .../common/viewModel/inlineDecorations.ts | 6 +-- .../viewLayout/viewLineRenderer.test.ts | 12 +++++ .../viewModel/inlineDecorations.test.ts | 18 +++++++ 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 319135a05b8ece..522d29d351bbfc 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -350,7 +350,7 @@ function renderLineWithFixedWidths(lineContent: string, initialVisibleColumn: nu sb.appendString(String(fixedWidthRange.widthInEm)); sb.appendString('em;">'); spanStartOffsets.push(charOffset); - } else if (!fixedWidthRange && charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) { + } else if ((!fixedWidthRange || charIndex < fixedWidthRange.startOffset) && charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) { sb.appendString(''); spanStartOffsets.push(charOffset); } diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index aa3d160b12a641..65c8ff98d4ce4a 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -944,12 +944,12 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP if (lineDecoration.endOffset + 1 <= tokenEndIndex) { // This line decoration ends before this token ends lastResultEndIndex = lineDecoration.endOffset + 1; - result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL, lineDecoration.fixedWidth); + result[resultLen++] = new LinePart(lastResultEndIndex, combineClassNames(tokenType, lineDecoration.className), tokenMetadata | lineDecoration.metadata, tokenContainsRTL, lineDecoration.fixedWidth); lineDecorationIndex++; } else { // This line decoration continues on to the next token lastResultEndIndex = tokenEndIndex; - result[resultLen++] = new LinePart(lastResultEndIndex, tokenType + ' ' + lineDecoration.className, tokenMetadata | lineDecoration.metadata, tokenContainsRTL, lineDecoration.fixedWidth); + result[resultLen++] = new LinePart(lastResultEndIndex, combineClassNames(tokenType, lineDecoration.className), tokenMetadata | lineDecoration.metadata, tokenContainsRTL, lineDecoration.fixedWidth); break; } } @@ -969,6 +969,54 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP } } + return coalesceFixedWidthLineParts(lineContent, result); +} + +function combineClassNames(first: string, second: string): string { + if (!first) { + return second; + } + if (!second) { + return first; + } + return first + ' ' + second; +} + +function coalesceFixedWidthLineParts(lineContent: string, parts: LinePart[]): LinePart[] { + const result: LinePart[] = []; + let partStartIndex = 0; + + for (let partIndex = 0; partIndex < parts.length; partIndex++) { + const part = parts[partIndex]; + const fixedWidth = part.fixedWidth; + if (!fixedWidth) { + result.push(part); + partStartIndex = part.endIndex; + continue; + } + + let endIndex = part.endIndex; + let metadata = part.metadata; + let containsRTL = part.containsRTL; + while (partIndex + 1 < parts.length && parts[partIndex + 1].fixedWidth === fixedWidth) { + const nextPart = parts[++partIndex]; + endIndex = nextPart.endIndex; + metadata |= nextPart.metadata; + containsRTL ||= nextPart.containsRTL; + } + + for (let charIndex = partStartIndex; charIndex < endIndex; charIndex++) { + const charCode = lineContent.charCodeAt(charIndex); + if (charCode !== CharCode.Space && charCode !== CharCode.Tab) { + metadata &= ~LinePartMetadata.IS_WHITESPACE_MASK; + break; + } + } + + result.push(new LinePart(endIndex, part.type, metadata, containsRTL, fixedWidth)); + partStartIndex = endIndex; + } + return result; } diff --git a/src/vs/editor/common/viewModel/inlineDecorations.ts b/src/vs/editor/common/viewModel/inlineDecorations.ts index 59d6e9fdeb4a30..c54b09d027a8aa 100644 --- a/src/vs/editor/common/viewModel/inlineDecorations.ts +++ b/src/vs/editor/common/viewModel/inlineDecorations.ts @@ -247,7 +247,7 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations if (lineStartOffsetInInputWithInjections < injectedTextEndOffsetInInputWithInjections) { // Injected text ends after or in this line (but also starts in or before this line). const options = injectionOptions![currentInjectedOffset]; - if (options.inlineClassName) { + if (options.inlineClassName || options.widthInEm !== undefined) { const wrappedTextIndentLength = this.context.getWrappedTextIndentLength(modelLineNumber); const offset = (outputLineIndex > 0 ? wrappedTextIndentLength : 0); const start = offset + Math.max(injectedTextStartOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, 0); @@ -255,9 +255,9 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations if (start !== end) { const viewLineNumber = this.context.getBaseViewLineNumber(modelLineNumber) + outputLineIndex; const range = new Range(viewLineNumber, start + 1, viewLineNumber, end + 1); - const type: InlineDecorationType = options.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular; + const type: InlineDecorationType = options.inlineClassNameAffectsLetterSpacing || options.widthInEm !== undefined ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular; const fixedWidth = options.widthInEm === undefined ? undefined : { widthInEm: options.widthInEm }; - inlineDecorations.push(new InlineDecoration(range, options.inlineClassName, type, fixedWidth)); + inlineDecorations.push(new InlineDecoration(range, options.inlineClassName ?? '', type, fixedWidth)); } } } diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index af59859c19ba2a..870937db445759 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -202,6 +202,18 @@ suite('renderViewLine', () => { assert.strictEqual(actual.html, '\xa0'); }); + test('applies fixed injected text width once across line parts', () => { + const fixedWidth = { widthInEm: 3 }; + const actual = renderViewLine(createRenderLineInput({ + lineContent: 'a b', + lineTokens: createViewLineTokens([createPart(1, 1), createPart(3, 2)]), + lineDecorations: [new LineDecoration(1, 4, 'injected', InlineDecorationType.RegularAffectingLetterSpacing, fixedWidth)], + renderWhitespace: 'all' + })); + + assert.strictEqual(actual.html, 'a\xa0b'); + }); + // overflow test('overflow', async () => { const _actual = renderViewLine(createRenderLineInput({ diff --git a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts index efc948f669e3cc..5f1263f86d0351 100644 --- a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts @@ -316,6 +316,24 @@ suite('InjectedTextInlineDecorationsComputer', () => { ]); }); + test('fixed width injection without inlineClassName affects letter spacing', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '\xa0', widthInEm: 3 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [5], + getBreakOffsets: () => [11], + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + assert.deepStrictEqual(result, [ + [new InlineDecoration(new Range(1, 6, 1, 7), '', InlineDecorationType.RegularAffectingLetterSpacing, { widthInEm: 3 })] + ]); + }); + test('injection with inlineClassNameAffectsLetterSpacing', () => { const injectionOptions: InjectedTextOptions[] = [ { content: 'abc', inlineClassName: 'ls-class', inlineClassNameAffectsLetterSpacing: true } From 9d48d5664c0678607b1217229055da994d459b25 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 21:39:22 +0200 Subject: [PATCH 14/41] wip --- .../native/screenReaderContentRich.ts | 1 + src/vs/editor/browser/gpu/viewGpuContext.ts | 4 + .../browser/view/domLineBreaksComputer.ts | 6 +- .../browser/viewParts/viewLines/viewLine.ts | 1 + .../components/accessibleDiffViewer.ts | 1 + .../diffEditorViewZones/renderLines.ts | 1 + src/vs/editor/common/textModelEvents.ts | 8 +- .../common/viewLayout/lineDecorations.ts | 44 ++----- src/vs/editor/common/viewLayout/linePart.ts | 4 +- .../common/viewLayout/viewLineRenderer.ts | 121 +++++++++--------- src/vs/editor/common/viewModel.ts | 15 ++- .../common/viewModel/injectedTextLinePart.ts | 33 +++++ .../common/viewModel/inlineDecorations.ts | 35 +++-- .../common/viewModel/modelLineProjection.ts | 16 ++- .../viewModel/monospaceLineBreaksComputer.ts | 48 ++++--- .../editor/common/viewModel/viewModelImpl.ts | 1 + .../editor/common/viewModel/viewModelLines.ts | 1 + .../colorPicker/browser/colorDetector.ts | 12 +- .../colorPicker/browser/colorPicker.css | 2 - .../browser/inlayHintsController.ts | 5 +- .../browser/view/ghostText/ghostTextView.ts | 1 + .../browser/inlineProgressWidget.css | 1 - .../browser/stickyScrollWidget.ts | 2 +- src/vs/editor/standalone/browser/colorizer.ts | 3 + .../viewModel/modelLineProjection.test.ts | 22 ++++ .../viewLayout/viewLineRenderer.test.ts | 88 ++++++++++++- .../viewModel/inlineDecorations.test.ts | 12 +- 27 files changed, 332 insertions(+), 156 deletions(-) create mode 100644 src/vs/editor/common/viewModel/injectedTextLinePart.ts diff --git a/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts b/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts index 7a274e27f3bebb..de59b0453e2ab0 100644 --- a/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts +++ b/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts @@ -195,6 +195,7 @@ export class RichScreenReaderContent extends Disposable implements IScreenReader positionLineData.minColumn - 1, positionLineData.tokens, lineDecorations, + positionLineData.injectedTextLineParts, positionLineData.tabSize, positionLineData.startVisibleColumn, fontInfo.spaceWidth, diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 9d520abdb3b077..0691e93d54f893 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -164,6 +164,7 @@ export class ViewGpuContext extends Disposable { // Check if the line has simple attributes that aren't supported if ( data.containsRTL || + data.injectedTextLineParts.length > 0 || data.maxColumn > this.maxGpuCols ) { return false; @@ -209,6 +210,9 @@ export class ViewGpuContext extends Disposable { if (data.containsRTL) { reasons.push('containsRTL'); } + if (data.injectedTextLineParts.length > 0) { + reasons.push('contains fixed-width injected text'); + } if (data.maxColumn > this.maxGpuCols) { reasons.push('maxColumn > maxGpuCols'); } diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 522d29d351bbfc..919329601bd457 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -12,7 +12,7 @@ import { WrappingIndent } from '../../common/config/editorOptions.js'; import { StringBuilder } from '../../common/core/stringBuilder.js'; import { InjectedTextOptions } from '../../common/model.js'; import { ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js'; -import { LineInjectedText, LineInjectedTextFixedWidth } from '../../common/textModelEvents.js'; +import { LineInjectedText, LineInjectedTextWidth } from '../../common/textModelEvents.js'; import { FontInfo } from '../../common/config/fontInfo.js'; const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value }); @@ -84,7 +84,7 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont const lineNumber = lineNumbers[i]; const injectedTexts = context.getLineInjectedText(lineNumber); const lineContent = LineInjectedText.applyInjectedText(context.getLineContent(lineNumber), injectedTexts); - const fixedWidthRanges = LineInjectedText.getFixedWidthRanges(injectedTexts); + const fixedWidthRanges = LineInjectedText.getInjectedTextWidthsInEm(injectedTexts); let firstNonWhitespaceIndex = 0; let wrappedTextIndentLength = 0; @@ -318,7 +318,7 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: return [charOffsets, visibleColumns, null]; } -function renderLineWithFixedWidths(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly LineInjectedTextFixedWidth[], columnsPerEm: number): [number[], number[], number[]] { +function renderLineWithFixedWidths(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly LineInjectedTextWidth[], columnsPerEm: number): [number[], number[], number[]] { if (wrappingIndentLength !== 0) { const hangingOffset = String(wrappingIndentLength); sb.appendString('
\xa0'); }); + test('enforces fixed injected text width without a class name', () => { + const actual = renderViewLine(createRenderLineInput({ + lineContent: '\xa0', + lineTokens: createViewLineTokens([createPart(1, 0)]), + injectedTextLineParts: [new InjectedTextLinePart(1, 2, '', 1)] + })); + + assert.strictEqual(actual.html, '\xa0'); + }); + + test('does not render whitespace markers inside fixed injected text', () => { + const actual = renderViewLine(createRenderLineInput({ + lineContent: ' ', + lineTokens: createViewLineTokens([createPart(1, 0)]), + injectedTextLineParts: [new InjectedTextLinePart(1, 2, 'injected', 1)], + renderWhitespace: 'all' + })); + + assert.strictEqual(actual.html, '\xa0'); + }); + + test('preserves content around fixed injected text', () => { + const actual = renderViewLine(createRenderLineInput({ + lineContent: 'xabcy', + lineTokens: createViewLineTokens([createPart(1, 0), createPart(4, 0), createPart(5, 0)]), + injectedTextLineParts: [new InjectedTextLinePart(2, 5, 'injected', 3)] + })); + + assert.strictEqual(actual.html, 'xabcy'); + }); + test('applies fixed injected text width once across line parts', () => { - const fixedWidth = { widthInEm: 3 }; const actual = renderViewLine(createRenderLineInput({ lineContent: 'a b', lineTokens: createViewLineTokens([createPart(1, 1), createPart(3, 2)]), - lineDecorations: [new LineDecoration(1, 4, 'injected', InlineDecorationType.RegularAffectingLetterSpacing, fixedWidth)], + injectedTextLineParts: [new InjectedTextLinePart(1, 4, 'injected', 3)], renderWhitespace: 'all' })); assert.strictEqual(actual.html, 'a\xa0b'); }); + test('keeps adjacent equal fixed widths separate', () => { + const actual = renderViewLine(createRenderLineInput({ + lineContent: 'ab', + lineTokens: createViewLineTokens([createPart(1, 0), createPart(2, 0)]), + injectedTextLineParts: [ + new InjectedTextLinePart(1, 2, 'injected', 1), + new InjectedTextLinePart(2, 3, 'injected', 1) + ] + })); + + assert.strictEqual(actual.html, 'ab'); + }); + + test('applies decorations covering fixed-width injected text', () => { + const actual = renderViewLine(createRenderLineInput({ + lineContent: 'abc', + lineTokens: createViewLineTokens([createPart(3, 0)]), + lineDecorations: [new LineDecoration(1, 4, 'secondary', InlineDecorationType.Regular)], + injectedTextLineParts: [new InjectedTextLinePart(1, 4, 'injected', 3)] + })); + + assert.strictEqual(actual.html, 'abc'); + }); + + test('keeps fixed-width RTL injected text atomic', () => { + const actual = renderViewLine(createRenderLineInput({ + lineContent: ' אב', + isBasicASCII: false, + containsRTL: true, + lineTokens: createViewLineTokens([createPart(3, 0)]), + injectedTextLineParts: [new InjectedTextLinePart(1, 4, 'injected', 3)] + })); + + assert.strictEqual(actual.html, '\xa0אב'); + }); + + test('clamps fixed injected text to the rendered line length', () => { + const actual = renderViewLine(createRenderLineInput({ + lineContent: 'abcde', + lineTokens: createViewLineTokens([createPart(1, 0), createPart(5, 0)]), + injectedTextLineParts: [new InjectedTextLinePart(2, 6, 'injected', 3)], + stopRenderingLineAfter: 3 + })); + + assert.ok(actual.html.includes('bc')); + }); + // overflow test('overflow', async () => { const _actual = renderViewLine(createRenderLineInput({ @@ -452,6 +531,7 @@ suite('renderViewLine', () => { 0, lineTokens, [], + [], 4, 0, 10, diff --git a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts index 5f1263f86d0351..e85359d076e6f8 100644 --- a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts @@ -8,6 +8,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { Range } from '../../../common/core/range.js'; import { IModelDecoration, IModelDecorationOptions, InjectedTextOptions } from '../../../common/model.js'; import { InlineDecoration, InlineDecorationType, InlineModelDecorationsComputer, IInlineModelDecorationsComputerContext, InjectedTextInlineDecorationsComputer, IInjectedTextInlineDecorationsComputerContext } from '../../../common/viewModel/inlineDecorations.js'; +import { InjectedTextLinePart } from '../../../common/viewModel/injectedTextLinePart.js'; import { createTextModel } from '../testTextModel.js'; import { IdentityCoordinatesConverter } from '../../../common/coordinatesConverter.js'; @@ -316,7 +317,7 @@ suite('InjectedTextInlineDecorationsComputer', () => { ]); }); - test('fixed width injection without inlineClassName affects letter spacing', () => { + test('fixed width injection creates a projected line part', () => { const injectionOptions: InjectedTextOptions[] = [ { content: '\xa0', widthInEm: 3 } ]; @@ -328,10 +329,11 @@ suite('InjectedTextInlineDecorationsComputer', () => { getBaseViewLineNumber: () => 1, }; const computer = new InjectedTextInlineDecorationsComputer(context); - const result = computer.getInlineDecorations(1); - assert.deepStrictEqual(result, [ - [new InlineDecoration(new Range(1, 6, 1, 7), '', InlineDecorationType.RegularAffectingLetterSpacing, { widthInEm: 3 })] - ]); + const result = computer.getDecorations(1); + assert.deepStrictEqual(result, { + inlineDecorations: [[]], + injectedTextLineParts: [[new InjectedTextLinePart(6, 7, '', 3)]] + }); }); test('injection with inlineClassNameAffectsLetterSpacing', () => { From 90f81cb1d352df4a573a049fd593017bd536b8ce Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 21 Aug 2026 22:00:12 +0200 Subject: [PATCH 15/41] wip --- .../browser/view/domLineBreaksComputer.ts | 154 ++++-------------- 1 file changed, 36 insertions(+), 118 deletions(-) diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 919329601bd457..020a5e9da26703 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -101,17 +101,16 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont let fixedWidthRangeIndex = 0; for (let i = 0; i < firstNonWhitespaceIndex; i++) { - const fixedWidthRange = fixedWidthRanges?.[fixedWidthRangeIndex]; + const fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex]; const isFixedWidthStart = fixedWidthRange?.startOffset === i; - const charWidth = isFixedWidthStart - ? fixedWidthRange.widthInEm * fontInfo.fontSize / fontInfo.spaceWidth - : lineContent.charCodeAt(i) === CharCode.Tab - ? (tabSize - (wrappedTextIndentLength % tabSize)) - : 1; - wrappedTextIndentLength += charWidth; if (isFixedWidthStart) { + wrappedTextIndentLength += fixedWidthRange.widthInEm * fontInfo.fontSize / fontInfo.spaceWidth; i = fixedWidthRange.endOffset - 1; fixedWidthRangeIndex++; + } else if (lineContent.charCodeAt(i) === CharCode.Tab) { + wrappedTextIndentLength += tabSize - (wrappedTextIndentLength % tabSize); + } else { + wrappedTextIndentLength++; } } @@ -135,10 +134,17 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont endOffset: range.endOffset - firstNonWhitespaceIndex, widthInEm: range.widthInEm })).filter(range => range.endOffset > 0) ?? null; - const renderLineFixedWidthRanges = shiftedFixedWidthRanges?.length ? shiftedFixedWidthRanges : null; - const tmp = renderLineFixedWidthRanges - ? renderLineWithFixedWidths(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength, renderLineFixedWidthRanges, fontInfo.fontSize / fontInfo.typicalHalfwidthCharacterWidth) - : renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength); + const renderLineFixedWidthRanges = shiftedFixedWidthRanges ?? null; + const tmp = renderLine( + renderLineContent, + wrappedTextIndentLength, + tabSize, + width, + sb, + additionalIndentLength, + renderLineFixedWidthRanges, + fontInfo.fontSize / fontInfo.typicalHalfwidthCharacterWidth + ); firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex; wrappedTextIndentLengths[i] = wrappedTextIndentLength; renderLineContents[i] = renderLineContent; @@ -214,7 +220,7 @@ const enum Constants { SPAN_MODULO_LIMIT = 16384 } -function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number): [number[], number[], null] { +function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly LineInjectedTextWidth[] | null, columnsPerEm: number): [number[], number[], number[] | null] { if (wrappingIndentLength !== 0) { const hangingOffset = String(wrappingIndentLength); @@ -235,13 +241,23 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: const len = lineContent.length; let visibleColumn = initialVisibleColumn; let charOffset = 0; + let fixedWidthRangeIndex = 0; const charOffsets: number[] = []; + const spanStartOffsets: number[] | null = fixedWidthRanges ? [0] : null; const visibleColumns: number[] = []; let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : CharCode.Null); + let spanOpen = true; sb.appendString(''); for (let charIndex = 0; charIndex < len; charIndex++) { - if (charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) { + const fixedWidthRange = fixedWidthRanges?.[fixedWidthRangeIndex]; + const startsFixedWidth = fixedWidthRange?.startOffset === charIndex; + spanStartOffsets!.push(charOffset); + if (startsFixedWidth) { + sb.appendString(''); + } else if ((!fixedWidthRange || charIndex < fixedWidthRange.startOffset) && charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) { sb.appendString(''); } charOffsets[charIndex] = charOffset; @@ -305,109 +321,6 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: } } - charOffset += producedCharacters; - visibleColumn += charWidth; - } - sb.appendString(''); - - charOffsets[lineContent.length] = charOffset; - visibleColumns[lineContent.length] = visibleColumn; - - sb.appendString('
'); - - return [charOffsets, visibleColumns, null]; -} - -function renderLineWithFixedWidths(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly LineInjectedTextWidth[], columnsPerEm: number): [number[], number[], number[]] { - if (wrappingIndentLength !== 0) { - const hangingOffset = String(wrappingIndentLength); - sb.appendString('
'); - - const len = lineContent.length; - let visibleColumn = initialVisibleColumn; - let charOffset = 0; - let fixedWidthRangeIndex = 0; - const charOffsets: number[] = []; - const spanStartOffsets: number[] = [0]; - const visibleColumns: number[] = []; - let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : CharCode.Null); - - sb.appendString(''); - for (let charIndex = 0; charIndex < len; charIndex++) { - const fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex]; - const startsFixedWidth = fixedWidthRange?.startOffset === charIndex; - if (startsFixedWidth) { - sb.appendString(''); - spanStartOffsets.push(charOffset); - } else if ((!fixedWidthRange || charIndex < fixedWidthRange.startOffset) && charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) { - sb.appendString(''); - spanStartOffsets.push(charOffset); - } - - charOffsets[charIndex] = charOffset; - visibleColumns[charIndex] = visibleColumn; - const charCode = nextCharCode; - nextCharCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : CharCode.Null); - let producedCharacters = 1; - let charWidth = 1; - switch (charCode) { - case CharCode.Tab: - producedCharacters = (tabSize - (visibleColumn % tabSize)); - charWidth = producedCharacters; - for (let space = 1; space <= producedCharacters; space++) { - if (space < producedCharacters) { - sb.appendCharCode(0xA0); - } else { - sb.appendASCIICharCode(CharCode.Space); - } - } - break; - case CharCode.Space: - if (nextCharCode === CharCode.Space) { - sb.appendCharCode(0xA0); - } else { - sb.appendASCIICharCode(CharCode.Space); - } - break; - case CharCode.LessThan: - sb.appendString('<'); - break; - case CharCode.GreaterThan: - sb.appendString('>'); - break; - case CharCode.Ampersand: - sb.appendString('&'); - break; - case CharCode.Null: - sb.appendString('�'); - break; - case CharCode.UTF8_BOM: - case CharCode.LINE_SEPARATOR: - case CharCode.PARAGRAPH_SEPARATOR: - case CharCode.NEXT_LINE: - sb.appendCharCode(0xFFFD); - break; - default: - if (strings.isFullWidthCharacter(charCode)) { - charWidth++; - } - if (charCode < 32) { - sb.appendCharCode(9216 + charCode); - } else { - sb.appendCharCode(charCode); - } - } charOffset += producedCharacters; if (startsFixedWidth) { charWidth = fixedWidthRange.widthInEm * columnsPerEm; @@ -418,19 +331,24 @@ function renderLineWithFixedWidths(lineContent: string, initialVisibleColumn: nu if (fixedWidthRange && charIndex + 1 === fixedWidthRange.endOffset) { sb.appendString(''); + spanOpen = false; if (fixedWidthRange.endOffset < len) { sb.appendString(''); - spanStartOffsets.push(charOffset); + spanStartOffsets!.push(charOffset); + spanOpen = true; } fixedWidthRangeIndex++; } } - if (fixedWidthRanges[fixedWidthRanges.length - 1].endOffset < len) { + if (spanOpen) { sb.appendString(''); } + charOffsets[lineContent.length] = charOffset; visibleColumns[lineContent.length] = visibleColumn; + sb.appendString('
'); + return [charOffsets, visibleColumns, spanStartOffsets]; } From 6e9fc0d39e6c00817ea9a5fb744a67db1d182d11 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 24 Aug 2026 12:02:48 +0200 Subject: [PATCH 16/41] review changes --- .../browser/view/domLineBreaksComputer.ts | 34 ++++---- src/vs/editor/common/model.ts | 3 +- src/vs/editor/common/model/textModel.ts | 3 + .../editor/common/modelLineProjectionData.ts | 30 +++++++ src/vs/editor/common/textModelEvents.ts | 24 ------ .../common/viewLayout/viewLineRenderer.ts | 23 +++-- .../viewModel/monospaceLineBreaksComputer.ts | 17 ++-- .../colorPicker/browser/colorDetector.ts | 13 +-- .../colorPicker/browser/colorPicker.css | 17 +++- .../view/domLineBreaksComputer.test.ts | 85 +++++++++++++++++++ .../viewLayout/viewLineRenderer.test.ts | 28 ++++-- .../common/viewModel/lineBreakData.test.ts | 11 +++ .../monospaceLineBreaksComputer.test.ts | 15 ++++ .../browser/breakpointEditorContribution.ts | 6 +- .../browser/media/debug.contribution.css | 1 - .../debug/test/browser/breakpoints.test.ts | 8 +- 16 files changed, 236 insertions(+), 82 deletions(-) create mode 100644 src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 020a5e9da26703..a47f5dae2e5b4d 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -11,8 +11,8 @@ import { applyFontInfo } from '../config/domFontInfo.js'; import { WrappingIndent } from '../../common/config/editorOptions.js'; import { StringBuilder } from '../../common/core/stringBuilder.js'; import { InjectedTextOptions } from '../../common/model.js'; -import { ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js'; -import { LineInjectedText, LineInjectedTextWidth } from '../../common/textModelEvents.js'; +import { FixedWidthInjectedTextRange, getFixedWidthInjectedTextRanges, ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js'; +import { LineInjectedText } from '../../common/textModelEvents.js'; import { FontInfo } from '../../common/config/fontInfo.js'; const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value }); @@ -84,7 +84,7 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont const lineNumber = lineNumbers[i]; const injectedTexts = context.getLineInjectedText(lineNumber); const lineContent = LineInjectedText.applyInjectedText(context.getLineContent(lineNumber), injectedTexts); - const fixedWidthRanges = LineInjectedText.getInjectedTextWidthsInEm(injectedTexts); + const fixedWidthRanges = getFixedWidthInjectedTextRanges(injectedTexts); let firstNonWhitespaceIndex = 0; let wrappedTextIndentLength = 0; @@ -99,14 +99,12 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont } else { // Track existing indent - let fixedWidthRangeIndex = 0; for (let i = 0; i < firstNonWhitespaceIndex; i++) { - const fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex]; + const fixedWidthRange = fixedWidthRanges[0]; const isFixedWidthStart = fixedWidthRange?.startOffset === i; if (isFixedWidthStart) { - wrappedTextIndentLength += fixedWidthRange.widthInEm * fontInfo.fontSize / fontInfo.spaceWidth; - i = fixedWidthRange.endOffset - 1; - fixedWidthRangeIndex++; + firstNonWhitespaceIndex = i; + break; } else if (lineContent.charCodeAt(i) === CharCode.Tab) { wrappedTextIndentLength += tabSize - (wrappedTextIndentLength % tabSize); } else { @@ -129,12 +127,12 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont const renderLineContent = lineContent.substr(firstNonWhitespaceIndex); const shiftedFixedWidthRanges = firstNonWhitespaceIndex === 0 ? fixedWidthRanges - : fixedWidthRanges?.map(range => ({ + : fixedWidthRanges.map(range => ({ startOffset: Math.max(0, range.startOffset - firstNonWhitespaceIndex), endOffset: range.endOffset - firstNonWhitespaceIndex, widthInEm: range.widthInEm - })).filter(range => range.endOffset > 0) ?? null; - const renderLineFixedWidthRanges = shiftedFixedWidthRanges ?? null; + })).filter(range => range.endOffset > 0); + const renderLineFixedWidthRanges = shiftedFixedWidthRanges.length > 0 ? shiftedFixedWidthRanges : null; const tmp = renderLine( renderLineContent, wrappedTextIndentLength, @@ -220,7 +218,7 @@ const enum Constants { SPAN_MODULO_LIMIT = 16384 } -function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly LineInjectedTextWidth[] | null, columnsPerEm: number): [number[], number[], number[] | null] { +function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly FixedWidthInjectedTextRange[] | null, columnsPerEm: number): [number[], number[], number[] | null] { if (wrappingIndentLength !== 0) { const hangingOffset = String(wrappingIndentLength); @@ -252,13 +250,18 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: for (let charIndex = 0; charIndex < len; charIndex++) { const fixedWidthRange = fixedWidthRanges?.[fixedWidthRangeIndex]; const startsFixedWidth = fixedWidthRange?.startOffset === charIndex; - spanStartOffsets!.push(charOffset); if (startsFixedWidth) { - sb.appendString('
'); + spanStartOffsets!.push(charOffset); + spanOpen = true; } else if ((!fixedWidthRange || charIndex < fixedWidthRange.startOffset) && charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) { sb.appendString(''); + spanStartOffsets?.push(charOffset); } charOffsets[charIndex] = charOffset; visibleColumns[charIndex] = visibleColumn; @@ -332,7 +335,8 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: if (fixedWidthRange && charIndex + 1 === fixedWidthRange.endOffset) { sb.appendString(''); spanOpen = false; - if (fixedWidthRange.endOffset < len) { + const nextFixedWidthRange = fixedWidthRanges?.[fixedWidthRangeIndex + 1]; + if (fixedWidthRange.endOffset < len && nextFixedWidthRange?.startOffset !== fixedWidthRange.endOffset) { sb.appendString(''); spanStartOffsets!.push(charOffset); spanOpen = true; diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 457c5704bef283..cfbf2389d9db71 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -348,7 +348,8 @@ export interface InjectedTextOptions { readonly inlineClassNameAffectsLetterSpacing?: boolean; /** - * Overrides the rendered width of this injected text, measured in em. Cannot be combined with {@link tokens}. + * Sets the atomic rendered width and wrapping advance of this injected text in editor-font em units. + * The inline class must not change the font size or add horizontal margins. Cannot be combined with {@link tokens}. * @internal */ readonly widthInEm?: number; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 62d1579f571260..625ce2fc3951c8 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2471,6 +2471,9 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt if (options.widthInEm !== undefined && options.tokens) { throw new BugIndicatingError('Injected text cannot define both tokens and widthInEm'); } + if (options.widthInEm !== undefined && (!Number.isFinite(options.widthInEm) || options.widthInEm < 0)) { + throw new BugIndicatingError('Injected text widthInEm must be a finite non-negative number'); + } this.content = options.content || ''; this.tokens = options.tokens ?? null; this.inlineClassName = options.inlineClassName || null; diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index 948214355ef3f8..c1584d79026ead 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -344,3 +344,33 @@ export interface ILineBreaksComputer { addRequest(lineNumber: number, previousLineBreakData: ModelLineProjectionData | null): void; finalize(): (ModelLineProjectionData | null)[]; } + +/** + * The fixed-width geometry of injected text after all preceding injections have been applied. + */ +export interface FixedWidthInjectedTextRange { + readonly startOffset: number; + readonly endOffset: number; + readonly widthInEm: number; +} + +/** + * Projects fixed-width injected text into offsets in the line with all injections applied. + */ +export function getFixedWidthInjectedTextRanges(injectedTexts: readonly LineInjectedText[] | null): FixedWidthInjectedTextRange[] { + const result: FixedWidthInjectedTextRange[] = []; + let injectedTextLength = 0; + for (const injectedText of injectedTexts ?? []) { + const startOffset = injectedText.column - 1 + injectedTextLength; + const endOffset = startOffset + injectedText.options.content.length; + if (injectedText.options.widthInEm !== undefined) { + result.push({ + startOffset, + endOffset, + widthInEm: injectedText.options.widthInEm + }); + } + injectedTextLength += injectedText.options.content.length; + } + return result; +} diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 8a02164c033d9c..1f504db5852e61 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -254,24 +254,6 @@ export class LineInjectedText { return result; } - public static getInjectedTextWidthsInEm(injectedTexts: LineInjectedText[] | null): LineInjectedTextWidth[] { - const result: LineInjectedTextWidth[] = []; - let injectedTextLength = 0; - for (const injectedText of injectedTexts ?? []) { - const startOffset = injectedText.column - 1 + injectedTextLength; - const endOffset = startOffset + injectedText.options.content.length; - if (injectedText.options.widthInEm !== undefined) { - result.push({ - startOffset, - endOffset, - widthInEm: injectedText.options.widthInEm - }); - } - injectedTextLength += injectedText.options.content.length; - } - return result; - } - public static fromDecorations(decorations: IModelDecoration[]): LineInjectedText[] { const result: LineInjectedText[] = []; for (const decoration of decorations) { @@ -319,12 +301,6 @@ export class LineInjectedText { } } -export interface LineInjectedTextWidth { - readonly startOffset: number; - readonly endOffset: number; - readonly widthInEm: number; -} - /** * An event describing that a line has changed in a model. * @internal diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 4c4e90b9f82e21..e362c235bea18a 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -991,6 +991,7 @@ function createAtomicInjectedTextParts(parts: LinePart[], injectedTextLineParts: const result: LinePart[] = []; let partIndex = 0; + let partStartIndex = 0; const renderedEndIndex = parts[parts.length - 1]?.endIndex ?? 0; for (const injectedTextPart of injectedTextLineParts) { @@ -1000,8 +1001,14 @@ function createAtomicInjectedTextParts(parts: LinePart[], injectedTextLineParts: continue; } - while (partIndex < parts.length && parts[partIndex].endIndex <= injectedTextStartIndex) { - result.push(parts[partIndex++]); + while (partIndex < parts.length && partStartIndex < injectedTextStartIndex) { + const part = parts[partIndex]; + const endIndex = Math.min(part.endIndex, injectedTextStartIndex); + result.push(new LinePart(endIndex, part.type, part.metadata, part.containsRTL, part.widthInEm)); + partStartIndex = endIndex; + if (partStartIndex === part.endIndex) { + partIndex++; + } } const firstPart = parts[partIndex]; @@ -1010,10 +1017,13 @@ function createAtomicInjectedTextParts(parts: LinePart[], injectedTextLineParts: } let containsRTL = false; - while (partIndex < parts.length && parts[partIndex].endIndex <= injectedTextEndIndex) { + while (partIndex < parts.length && partStartIndex < injectedTextEndIndex) { const part = parts[partIndex]; containsRTL ||= part.containsRTL; - partIndex++; + partStartIndex = Math.min(part.endIndex, injectedTextEndIndex); + if (partStartIndex === part.endIndex) { + partIndex++; + } } const type = injectedTextPart.inlineClassName ? firstPart.type + ' ' + injectedTextPart.inlineClassName : firstPart.type; @@ -1021,7 +1031,8 @@ function createAtomicInjectedTextParts(parts: LinePart[], injectedTextLineParts: } while (partIndex < parts.length) { - result.push(parts[partIndex++]); + const part = parts[partIndex++]; + result.push(new LinePart(part.endIndex, part.type, part.metadata, part.containsRTL, part.widthInEm)); } return result; @@ -1078,7 +1089,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: StringBuilder): RenderL if (partContainsRTL) { sb.appendString('unicode-bidi:isolate;'); } - sb.appendString('display:inline-block;width:'); + sb.appendString('display:inline-block;box-sizing:border-box;width:'); sb.appendString(String(partWidthInEm)); sb.appendString('em;'); sb.appendString('" '); diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 6512c52874bbdd..11938145063019 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -8,9 +8,9 @@ import * as strings from '../../../base/common/strings.js'; import { WrappingIndent, IComputedEditorOptions, EditorOption } from '../config/editorOptions.js'; import { CharacterClassifier } from '../core/characterClassifier.js'; import { FontInfo } from '../config/fontInfo.js'; -import { LineInjectedText, LineInjectedTextWidth } from '../textModelEvents.js'; +import { LineInjectedText } from '../textModelEvents.js'; import { InjectedTextOptions } from '../model.js'; -import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; +import { FixedWidthInjectedTextRange, getFixedWidthInjectedTextRanges, ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory { public static create(options: IComputedEditorOptions): MonospaceLineBreaksComputerFactory { @@ -358,7 +358,7 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, columnsPerEm: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ModelLineProjectionData | null { const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts); - const injectedTextWidthsInEm = LineInjectedText.getInjectedTextWidthsInEm(injectedTexts); + const injectedTextWidthsInEm = getFixedWidthInjectedTextRanges(injectedTexts); let injectionOptions: InjectedTextOptions[] | null; let injectionOffsets: number[] | null; @@ -390,7 +390,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st } const isKeepAll = (wordBreak === 'keepAll'); - const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent, injectedTextWidthsInEm, columnsPerEm); + const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent, injectedTextWidthsInEm); const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; const breakingOffsets: number[] = []; @@ -552,21 +552,18 @@ function canBreak(prevCharCode: number, prevCharCodeClass: CharacterClass, charC ); } -function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, fixedWidthRanges: readonly LineInjectedTextWidth[] = [], columnsPerEm: number = 1): number { +function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, fixedWidthRanges: readonly FixedWidthInjectedTextRange[] = []): number { let wrappedTextIndentLength = 0; if (wrappingIndent !== WrappingIndent.None) { const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); if (firstNonWhitespaceIndex !== -1) { // Track existing indent - let fixedWidthRangeIndex = 0; for (let i = 0; i < firstNonWhitespaceIndex; i++) { - const fixedWidthRange = fixedWidthRanges?.[fixedWidthRangeIndex]; + const fixedWidthRange = fixedWidthRanges[0]; const isFixedWidthStart = fixedWidthRange?.startOffset === i; if (isFixedWidthStart) { - wrappedTextIndentLength += fixedWidthRange.widthInEm * columnsPerEm; - i = fixedWidthRange.endOffset - 1; - fixedWidthRangeIndex++; + break; } else { const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1); wrappedTextIndentLength += charWidth; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index 882e5e061b3e93..936f9e10c10bee 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -49,9 +49,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { private readonly _decoratorLimitReporter = this._register(new DecoratorLimitReporter()); - private _colorDecoratorWidthInEm = 0.8; - private _colorDecoratorTopMarginInEm = 0.1; - private _colorDecoratorHorizontalMarginInEm = 0.2; + private static readonly colorDecoratorWidthInEm = 1.2; constructor( private readonly _editor: ICodeEditor, @@ -210,9 +208,6 @@ export class ColorDetector extends Disposable implements IEditorContribution { const decorations: IModelDeltaDecoration[] = []; const limit = this._editor.getOption(EditorOption.colorDecoratorsLimit); - const colorDecoratorMargin = `${this._colorDecoratorTopMarginInEm}em ${this._colorDecoratorHorizontalMarginInEm}em 0 ${this._colorDecoratorHorizontalMarginInEm}em`; - const colorDecoratorTotalWidthInEm = this._colorDecoratorWidthInEm + 2 * this._colorDecoratorHorizontalMarginInEm; - for (let i = 0; i < colorData.length && decorations.length < limit; i++) { const { red, green, blue, alpha } = colorData[i].colorInfo.color; const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha); @@ -220,9 +215,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { const ref = this._colorDecorationClassRefs.add( this._ruleFactory.createClassNameRef({ - backgroundColor: color, - margin: colorDecoratorMargin, - width: `${this._colorDecoratorWidthInEm}em` // !important + backgroundColor: color }) ); @@ -239,7 +232,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { content: noBreakWhitespace, inlineClassName: `${ref.className} colorpicker-color-decoration`, inlineClassNameAffectsLetterSpacing: true, - widthInEm: colorDecoratorTotalWidthInEm, + widthInEm: ColorDetector.colorDecoratorWidthInEm, attachedData: ColorDecorationInjectedTextMarker } } diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css index 08fa48dc2d4dd4..24696139e515a3 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css +++ b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css @@ -13,16 +13,27 @@ .colorpicker-color-decoration, .hc-light .colorpicker-color-decoration { - border: solid 0.1em #000; box-sizing: border-box; + padding: 0 0.2em; + background-clip: content-box; height: 0.8em; line-height: 0.8em; display: inline-block; + position: relative; cursor: pointer; } -.hc-black .colorpicker-color-decoration, -.vs-dark .colorpicker-color-decoration { +.colorpicker-color-decoration::after { + content: ''; + position: absolute; + inset: 0 0.2em; + box-sizing: border-box; + border: solid 0.1em #000; + pointer-events: none; +} + +.hc-black .colorpicker-color-decoration::after, +.vs-dark .colorpicker-color-decoration::after { border: solid 0.1em #eee; } diff --git a/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts b/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts new file mode 100644 index 00000000000000..e64261dd62269b --- /dev/null +++ b/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { mainWindow } from '../../../../base/browser/window.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { DOMLineBreaksComputerFactory } from '../../../browser/view/domLineBreaksComputer.js'; +import { WrappingIndent } from '../../../common/config/editorOptions.js'; +import { FontInfo } from '../../../common/config/fontInfo.js'; +import { ILineBreaksComputerContext, ModelLineProjectionData } from '../../../common/modelLineProjectionData.js'; +import { LineInjectedText } from '../../../common/textModelEvents.js'; + +suite('DOMLineBreaksComputer', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const fontInfo = new FontInfo({ + pixelRatio: 1, + fontFamily: 'Arial', + fontWeight: 'normal', + fontSize: 14, + fontFeatureSettings: '', + fontVariationSettings: '', + lineHeight: 19, + letterSpacing: 0, + isMonospace: false, + typicalHalfwidthCharacterWidth: 7, + typicalFullwidthCharacterWidth: 14, + canUseHalfwidthRightwardsArrow: true, + spaceWidth: 7, + middotWidth: 7, + wsmiddotWidth: 7, + maxDigitWidth: 7 + }, false); + + function computeLineBreaks(text: string, injectedText: LineInjectedText[] | null, wrappingColumn = 4): ModelLineProjectionData | null { + const context: ILineBreaksComputerContext = { + getLineContent: () => text, + getLineInjectedText: () => injectedText + }; + const computer = DOMLineBreaksComputerFactory.create(mainWindow).createLineBreaksComputer( + context, + fontInfo, + 4, + wrappingColumn, + WrappingIndent.None, + 'normal', + false + ); + computer.addRequest(1, null); + return computer.finalize()[0]; + } + + test('tracks DOM spans without fixed-width injected text', () => { + const result = computeLineBreaks('alpha beta gamma', null); + + assert.ok(result && result.breakOffsets.length > 1); + }); + + test('tracks DOM spans with fixed-width injected text', () => { + const result = computeLineBreaks('alpha beta gamma', [ + new LineInjectedText(0, 1, 7, { content: '\xa0', widthInEm: 3 }, 0) + ]); + + assert.ok(result && result.breakOffsets.length > 1); + }); + + test('tracks adjacent fixed-width DOM spans', () => { + const result = computeLineBreaks('alpha beta gamma', [ + new LineInjectedText(0, 1, 7, { content: 'x', widthInEm: 1 }, 0), + new LineInjectedText(0, 1, 7, { content: 'y', widthInEm: 1 }, 1) + ]); + + assert.ok(result && result.breakOffsets.length > 1); + }); + + test('splits long DOM spans without fixed-width injected text', () => { + const text = 'a'.repeat(16385); + const result = computeLineBreaks(text, null, text.length + 1); + + assert.strictEqual(result?.breakOffsets.at(-1), text.length); + }); +}); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 8f8bcfa6a8560f..8bb01e56182392 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -201,7 +201,7 @@ suite('renderViewLine', () => { injectedTextLineParts: [new InjectedTextLinePart(1, 2, 'injected', 1)] })); - assert.strictEqual(actual.html, '\xa0'); + assert.strictEqual(actual.html, '\xa0'); }); test('enforces fixed injected text width without a class name', () => { @@ -211,7 +211,7 @@ suite('renderViewLine', () => { injectedTextLineParts: [new InjectedTextLinePart(1, 2, '', 1)] })); - assert.strictEqual(actual.html, '\xa0'); + assert.strictEqual(actual.html, '\xa0'); }); test('does not render whitespace markers inside fixed injected text', () => { @@ -222,7 +222,7 @@ suite('renderViewLine', () => { renderWhitespace: 'all' })); - assert.strictEqual(actual.html, '\xa0'); + assert.strictEqual(actual.html, '\xa0'); }); test('preserves content around fixed injected text', () => { @@ -232,7 +232,17 @@ suite('renderViewLine', () => { injectedTextLineParts: [new InjectedTextLinePart(2, 5, 'injected', 3)] })); - assert.strictEqual(actual.html, 'xabcy'); + assert.strictEqual(actual.html, 'xabcy'); + }); + + test('splits line parts at fixed injected text boundaries', () => { + const actual = renderViewLine(createRenderLineInput({ + lineContent: 'xabcy', + lineTokens: createViewLineTokens([createPart(5, 0)]), + injectedTextLineParts: [new InjectedTextLinePart(2, 5, 'injected', 3)] + })); + + assert.strictEqual(actual.html, 'xabcy'); }); test('applies fixed injected text width once across line parts', () => { @@ -243,7 +253,7 @@ suite('renderViewLine', () => { renderWhitespace: 'all' })); - assert.strictEqual(actual.html, 'a\xa0b'); + assert.strictEqual(actual.html, 'a\xa0b'); }); test('keeps adjacent equal fixed widths separate', () => { @@ -256,7 +266,7 @@ suite('renderViewLine', () => { ] })); - assert.strictEqual(actual.html, 'ab'); + assert.strictEqual(actual.html, 'ab'); }); test('applies decorations covering fixed-width injected text', () => { @@ -267,7 +277,7 @@ suite('renderViewLine', () => { injectedTextLineParts: [new InjectedTextLinePart(1, 4, 'injected', 3)] })); - assert.strictEqual(actual.html, 'abc'); + assert.strictEqual(actual.html, 'abc'); }); test('keeps fixed-width RTL injected text atomic', () => { @@ -279,7 +289,7 @@ suite('renderViewLine', () => { injectedTextLineParts: [new InjectedTextLinePart(1, 4, 'injected', 3)] })); - assert.strictEqual(actual.html, '\xa0אב'); + assert.strictEqual(actual.html, '\xa0אב'); }); test('clamps fixed injected text to the rendered line length', () => { @@ -290,7 +300,7 @@ suite('renderViewLine', () => { stopRenderingLineAfter: 3 })); - assert.ok(actual.html.includes('bc')); + assert.ok(actual.html.includes('bc')); }); // overflow diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index b2b6835327f9f7..1c634619afef77 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -32,6 +32,17 @@ suite('Editor ViewModel - LineBreakData', () => { ); }); + test('fixed width must be finite and non-negative', () => { + assert.throws( + () => ModelDecorationInjectedTextOptions.from({ content: 'text', widthInEm: Number.NaN }), + /Injected text widthInEm must be a finite non-negative number/ + ); + assert.throws( + () => ModelDecorationInjectedTextOptions.from({ content: 'text', widthInEm: -1 }), + /Injected text widthInEm must be a finite non-negative number/ + ); + }); + function sequence(length: number, start = 0): number[] { const result = new Array(); for (let i = 0; i < length; i++) { diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 108421300977fd..173fe4dec55fe9 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -196,6 +196,21 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { }); }); + test('does not use fixed-width injected whitespace as continuation indentation', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 5, 2, WrappingIndent.Same, 'normal', false, 'abcdef', null, [ + new LineInjectedText(0, 1, 1, { content: ' ', widthInEm: 0.75 }, 0) + ]); + + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + wrappedTextIndentLength: lineBreakData?.wrappedTextIndentLength + }, { + breakOffsets: [4, 7], + wrappedTextIndentLength: 0 + }); + }); + function assertLineBreakDataEqual(a: ModelLineProjectionData | null, b: ModelLineProjectionData | null): void { if (!a || !b) { assert.deepStrictEqual(a, b); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 6ee10c8b3f260b..a4b9d2ea45585f 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -142,7 +142,8 @@ function getBreakpointDecorationOptions(accessor: ServicesAccessor, model: IText before: renderInline ? { content: noBreakWhitespace, inlineClassName: `debug-breakpoint-placeholder`, - inlineClassNameAffectsLetterSpacing: true + inlineClassNameAffectsLetterSpacing: true, + widthInEm: 0.9 } : undefined, overviewRuler: overviewRulerDecoration, zIndex: 9999 @@ -195,7 +196,8 @@ function createCandidateDecorations(model: ITextModel, breakpointDecorations: IB before: breakpointAtPosition ? undefined : { content: noBreakWhitespace, inlineClassName: `debug-breakpoint-placeholder`, - inlineClassNameAffectsLetterSpacing: true + inlineClassNameAffectsLetterSpacing: true, + widthInEm: 0.9 }, }, breakpoint: breakpointAtPosition ? breakpointAtPosition.breakpoint : undefined diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index 2a8f7661cb29a5..e0a5a6c5c1c331 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -25,7 +25,6 @@ } .monaco-editor .debug-breakpoint-placeholder { - width: 0.9em; display: inline-flex; vertical-align: middle; margin-top: -1px; diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index c53b662bf176f4..5ebafa4978ae00 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -432,7 +432,13 @@ suite('Debug - Breakpoints', () => { assert.deepStrictEqual(decorations[1].range, new Range(2, 4, 2, 5)); assert.deepStrictEqual(decorations[2].range, new Range(3, 5, 3, 6)); assert.strictEqual(decorations[0].options.beforeContentClassName, undefined); - assert.strictEqual(decorations[1].options.before?.inlineClassName, `debug-breakpoint-placeholder`); + assert.deepStrictEqual({ + inlineClassName: decorations[1].options.before?.inlineClassName, + widthInEm: decorations[1].options.before?.widthInEm + }, { + inlineClassName: 'debug-breakpoint-placeholder', + widthInEm: 0.9 + }); assert.strictEqual(decorations[0].options.overviewRuler?.position, OverviewRulerLane.Left); const expected = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendCodeblock(languageId, 'Condition: x > 5'); assert.deepStrictEqual(decorations[0].options.glyphMarginHoverMessage, expected); From 63a762ff207f2a37e4b478b837d0f0c21ded654c Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 24 Aug 2026 16:52:46 +0200 Subject: [PATCH 17/41] wip --- .../lib/stylelint/vscode-known-variables.json | 106 ++++++++---------- .../native/screenReaderContentRich.ts | 2 +- src/vs/editor/browser/gpu/viewGpuContext.ts | 4 - .../browser/viewParts/viewLines/viewLine.ts | 2 +- src/vs/editor/common/model.ts | 4 +- src/vs/editor/common/model/textModel.ts | 3 - .../editor/common/modelLineProjectionData.ts | 16 ++- .../common/viewLayout/viewLineRenderer.ts | 38 +++---- src/vs/editor/common/viewModel.ts | 17 ++- .../common/viewModel/injectedTextLinePart.ts | 33 ------ .../common/viewModel/inlineDecorations.ts | 52 +++++++-- .../common/viewModel/modelLineProjection.ts | 18 ++- .../editor/common/viewModel/viewModelImpl.ts | 3 +- .../colorPicker/browser/colorDetector.ts | 14 ++- .../colorPicker/browser/colorPicker.css | 10 +- .../browser/stickyScrollWidget.ts | 2 +- .../viewModel/modelLineProjection.test.ts | 6 +- .../viewLayout/viewLineRenderer.test.ts | 25 ++--- .../viewModel/inlineDecorations.test.ts | 7 +- .../editor/injectedTextDecorations.fixture.ts | 11 +- 20 files changed, 176 insertions(+), 197 deletions(-) delete mode 100644 src/vs/editor/common/viewModel/injectedTextLinePart.ts diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 593a6366dbd202..6f8d2ebad0604f 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1,6 +1,8 @@ { "colors": [ "--vscode-actionBar-toggledBackground", + "--vscode-activeSessionView-background", + "--vscode-activeSessionView-foreground", "--vscode-activityBar-activeBackground", "--vscode-activityBar-activeBorder", "--vscode-activityBar-activeFocusBorder", @@ -28,12 +30,30 @@ "--vscode-agentSessionSelectedBadge-border", "--vscode-agentSessionSelectedUnfocusedBadge-border", "--vscode-agentStatusIndicator-background", + "--vscode-agents-background", + "--vscode-agentsBadge-background", + "--vscode-agentsBadge-foreground", + "--vscode-agentsBottomPanel-border", + "--vscode-agentsCard-border", + "--vscode-agentsChatInput-background", + "--vscode-agentsChatInput-border", + "--vscode-agentsChatInput-focusBorder", + "--vscode-agentsChatInput-foreground", + "--vscode-agentsChatInput-placeholderForeground", + "--vscode-agentsGradient-tintColor", + "--vscode-agentsNewSessionButton-background", + "--vscode-agentsNewSessionButton-border", + "--vscode-agentsNewSessionButton-foreground", + "--vscode-agentsNewSessionButton-hoverBackground", + "--vscode-agentsPanel-background", + "--vscode-agentsPanel-border", + "--vscode-agentsPanel-foreground", + "--vscode-agentsUnreadBadge-background", + "--vscode-agentsUnreadBadge-foreground", + "--vscode-agentsUpdateButton-downloadedBackground", + "--vscode-agentsUpdateButton-downloadingBackground", "--vscode-agentsVoice-speakingBackground", "--vscode-agentsVoice-speakingForeground", - "--vscode-activeSessionView-background", - "--vscode-activeSessionView-foreground", - "--vscode-inactiveSessionView-background", - "--vscode-inactiveSessionView-foreground", "--vscode-badge-background", "--vscode-badge-foreground", "--vscode-banner-background", @@ -85,12 +105,9 @@ "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", "--vscode-chat-thinkingShimmer", - "--vscode-agentsChatInput-background", - "--vscode-agentsChatInput-border", - "--vscode-agentsChatInput-focusBorder", - "--vscode-agentsChatInput-foreground", - "--vscode-agentsChatInput-placeholderForeground", - "--vscode-chatManagement-sashBorder", + "--vscode-chat-voiceGlowBaseColor", + "--vscode-chat-voiceListeningGlow", + "--vscode-chat-voiceSpeakingGlow", "--vscode-checkbox-background", "--vscode-checkbox-border", "--vscode-checkbox-disabled-background", @@ -399,22 +416,13 @@ "--vscode-extensionIcon-verifiedForeground", "--vscode-focusBorder", "--vscode-foreground", - "--vscode-gauge-background", - "--vscode-gauge-border", - "--vscode-gauge-errorBackground", - "--vscode-gauge-errorForeground", - "--vscode-gauge-foreground", - "--vscode-gauge-warningBackground", - "--vscode-gauge-warningForeground", - "--vscode-gitDecoration-addedResourceForeground", - "--vscode-gitDecoration-deletedResourceForeground", - "--vscode-gitDecoration-modifiedResourceForeground", "--vscode-icon-foreground", + "--vscode-inactiveSessionView-background", + "--vscode-inactiveSessionView-foreground", "--vscode-inlineChat-background", "--vscode-inlineChat-border", "--vscode-inlineChat-foreground", "--vscode-inlineChat-shadow", - "--vscode-inlineChat-regionHighlight", "--vscode-inlineChatDiff-inserted", "--vscode-inlineChatDiff-removed", "--vscode-inlineChatInput-background", @@ -547,8 +555,10 @@ "--vscode-minimapSlider-hoverBackground", "--vscode-modernActivityBar-activeBackground", "--vscode-modernActivityBar-activeForeground", + "--vscode-modernActivityBar-background", "--vscode-modernActivityBar-hoverBackground", "--vscode-modernActivityBar-hoverForeground", + "--vscode-modernActivityBar-inactiveBackground", "--vscode-modernEditorTab-activeActionBackground", "--vscode-modernEditorTab-activeBackground", "--vscode-modernEditorTab-activeForeground", @@ -686,29 +696,6 @@ "--vscode-searchEditor-findMatchBorder", "--vscode-searchEditor-textInputBorder", "--vscode-selection-background", - "--vscode-surface-background", - "--vscode-surface-border", - "--vscode-surface-foreground", - "--vscode-agentsPanel-background", - "--vscode-agentsPanel-border", - "--vscode-agentsPanel-foreground", - "--vscode-agentsCard-border", - "--vscode-agentsBottomPanel-border", - "--vscode-agentsBadge-background", - "--vscode-agentsBadge-foreground", - "--vscode-agentsGradient-tintColor", - "--vscode-agentsNewSessionButton-background", - "--vscode-agentsNewSessionButton-border", - "--vscode-agentsNewSessionButton-foreground", - "--vscode-agentsNewSessionButton-hoverBackground", - "--vscode-agents-background", - "--vscode-agentsUnreadBadge-background", - "--vscode-agentsUnreadBadge-foreground", - "--vscode-agentsUpdateButton-downloadedBackground", - "--vscode-agentsUpdateButton-downloadingBackground", - "--vscode-agentsMobileDiff-addedForeground", - "--vscode-agentsMobileDiff-modifiedForeground", - "--vscode-agentsMobileDiff-deletedForeground", "--vscode-settings-checkboxBackground", "--vscode-settings-checkboxBorder", "--vscode-settings-checkboxForeground", @@ -782,6 +769,10 @@ "--vscode-statusBarItem-warningForeground", "--vscode-statusBarItem-warningHoverBackground", "--vscode-statusBarItem-warningHoverForeground", + "--vscode-strongForeground", + "--vscode-surface-background", + "--vscode-surface-border", + "--vscode-surface-foreground", "--vscode-symbolIcon-arrayForeground", "--vscode-symbolIcon-booleanForeground", "--vscode-symbolIcon-classForeground", @@ -815,7 +806,6 @@ "--vscode-symbolIcon-typeParameterForeground", "--vscode-symbolIcon-unitForeground", "--vscode-symbolIcon-variableForeground", - "--vscode-strongForeground", "--vscode-tab-activeBackground", "--vscode-tab-activeBorder", "--vscode-tab-activeBorderTop", @@ -910,6 +900,7 @@ "--vscode-testing-coveredBackground", "--vscode-testing-coveredBorder", "--vscode-testing-coveredGutterBackground", + "--vscode-testing-coveredMinimapBackground", "--vscode-testing-iconErrored", "--vscode-testing-iconErrored-retired", "--vscode-testing-iconFailed", @@ -937,6 +928,7 @@ "--vscode-testing-uncoveredBorder", "--vscode-testing-uncoveredBranchBackground", "--vscode-testing-uncoveredGutterBackground", + "--vscode-testing-uncoveredMinimapBackground", "--vscode-textBlockQuote-background", "--vscode-textBlockQuote-border", "--vscode-textCodeBlock-background", @@ -969,10 +961,7 @@ "--vscode-widget-border", "--vscode-widget-shadow", "--vscode-window-activeBorder", - "--vscode-window-inactiveBorder", - "--vscode-chat-voiceGlowBaseColor", - "--vscode-chat-voiceListeningGlow", - "--vscode-chat-voiceSpeakingGlow" + "--vscode-window-inactiveBorder" ], "others": [ "--action-widget-close-start-opacity", @@ -1215,12 +1204,6 @@ "--vg-inner-fade" ], "sizes": [ - "--segmented-icon-toggle-cell-radius", - "--segmented-icon-toggle-cell-width", - "--segmented-icon-toggle-height", - "--segmented-icon-toggle-radius", - "--segmented-icon-toggle-single-width", - "--segmented-icon-toggle-width", "--vscode-agents-fontSize-body1", "--vscode-agents-fontSize-body2", "--vscode-agents-fontSize-heading1", @@ -1253,21 +1236,20 @@ "--vscode-fontSize-label3", "--vscode-fontWeight-regular", "--vscode-fontWeight-semiBold", - "--vscode-keyboard-height", - "--vscode-spacing-sizeNone", - "--vscode-spacing-size20", - "--vscode-spacing-size40", - "--vscode-spacing-size60", - "--vscode-spacing-size80", "--vscode-spacing-size100", "--vscode-spacing-size120", "--vscode-spacing-size160", + "--vscode-spacing-size20", "--vscode-spacing-size200", "--vscode-spacing-size240", "--vscode-spacing-size280", "--vscode-spacing-size320", "--vscode-spacing-size360", + "--vscode-spacing-size40", "--vscode-spacing-size400", + "--vscode-spacing-size60", + "--vscode-spacing-size80", + "--vscode-spacing-sizeNone", "--vscode-strokeThickness" ] -} +} \ No newline at end of file diff --git a/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts b/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts index de59b0453e2ab0..7e417195bfd482 100644 --- a/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts +++ b/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts @@ -195,7 +195,7 @@ export class RichScreenReaderContent extends Disposable implements IScreenReader positionLineData.minColumn - 1, positionLineData.tokens, lineDecorations, - positionLineData.injectedTextLineParts, + positionLineData.fixedWidthInlineDecorations, positionLineData.tabSize, positionLineData.startVisibleColumn, fontInfo.spaceWidth, diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index 0691e93d54f893..9d520abdb3b077 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -164,7 +164,6 @@ export class ViewGpuContext extends Disposable { // Check if the line has simple attributes that aren't supported if ( data.containsRTL || - data.injectedTextLineParts.length > 0 || data.maxColumn > this.maxGpuCols ) { return false; @@ -210,9 +209,6 @@ export class ViewGpuContext extends Disposable { if (data.containsRTL) { reasons.push('containsRTL'); } - if (data.injectedTextLineParts.length > 0) { - reasons.push('contains fixed-width injected text'); - } if (data.maxColumn > this.maxGpuCols) { reasons.push('maxColumn > maxGpuCols'); } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index 468eb895c2f78c..118d9a9268549b 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -158,7 +158,7 @@ export class ViewLine implements IVisibleLine { lineData.minColumn - 1, lineData.tokens, actualInlineDecorations, - lineData.injectedTextLineParts, + lineData.fixedWidthInlineDecorations, lineData.tabSize, lineData.startVisibleColumn, options.spaceWidth, diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index cfbf2389d9db71..4f189021c028f3 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -348,8 +348,8 @@ export interface InjectedTextOptions { readonly inlineClassNameAffectsLetterSpacing?: boolean; /** - * Sets the atomic rendered width and wrapping advance of this injected text in editor-font em units. - * The inline class must not change the font size or add horizontal margins. Cannot be combined with {@link tokens}. + * Sets the atomic rendered width and wrapping width of this injected text in editor-font em units. + * Cannot be combined with {@link tokens}. * @internal */ readonly widthInEm?: number; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 625ce2fc3951c8..62d1579f571260 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2471,9 +2471,6 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt if (options.widthInEm !== undefined && options.tokens) { throw new BugIndicatingError('Injected text cannot define both tokens and widthInEm'); } - if (options.widthInEm !== undefined && (!Number.isFinite(options.widthInEm) || options.widthInEm < 0)) { - throw new BugIndicatingError('Injected text widthInEm must be a finite non-negative number'); - } this.content = options.content || ''; this.tokens = options.tokens ?? null; this.inlineClassName = options.inlineClassName || null; diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index c1584d79026ead..72e7bdf6239c8e 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -346,7 +346,7 @@ export interface ILineBreaksComputer { } /** - * The fixed-width geometry of injected text after all preceding injections have been applied. + * The fixed-widthinjected text range after all preceding injections have been applied. */ export interface FixedWidthInjectedTextRange { readonly startOffset: number; @@ -361,16 +361,14 @@ export function getFixedWidthInjectedTextRanges(injectedTexts: readonly LineInje const result: FixedWidthInjectedTextRange[] = []; let injectedTextLength = 0; for (const injectedText of injectedTexts ?? []) { + const length = injectedText.options.content.length; const startOffset = injectedText.column - 1 + injectedTextLength; - const endOffset = startOffset + injectedText.options.content.length; - if (injectedText.options.widthInEm !== undefined) { - result.push({ - startOffset, - endOffset, - widthInEm: injectedText.options.widthInEm - }); + const endOffset = startOffset + length; + const widthInEm = injectedText.options.widthInEm; + if (widthInEm !== undefined) { + result.push({ startOffset, endOffset, widthInEm }); } - injectedTextLength += injectedText.options.content.length; + injectedTextLength += length; } return result; } diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index e362c235bea18a..adb1a79c29ea9f 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -11,8 +11,7 @@ import { StringBuilder } from '../core/stringBuilder.js'; import { LineDecoration, LineDecorationsNormalizer } from './lineDecorations.js'; import { LinePart, LinePartMetadata } from './linePart.js'; import { OffsetRange } from '../core/ranges/offsetRange.js'; -import { InlineDecorationType } from '../viewModel/inlineDecorations.js'; -import { InjectedTextLinePart } from '../viewModel/injectedTextLinePart.js'; +import { FixedWidthInlineDecoration, InlineDecorationType } from '../viewModel/inlineDecorations.js'; import { TextDirection } from '../model.js'; export const enum RenderWhitespace { @@ -33,7 +32,7 @@ export interface IRenderLineInputOptions { fauxIndentLength: number; lineTokens: IViewLineTokens; lineDecorations: LineDecoration[]; - injectedTextLineParts: readonly InjectedTextLinePart[]; + injectedTextLineParts: readonly FixedWidthInlineDecoration[]; tabSize: number; startVisibleColumn: number; spaceWidth: number; @@ -60,7 +59,7 @@ export class RenderLineInput { public readonly fauxIndentLength: number; public readonly lineTokens: IViewLineTokens; public readonly lineDecorations: LineDecoration[]; - public readonly injectedTextLineParts: readonly InjectedTextLinePart[]; + public readonly injectedTextLineParts: readonly FixedWidthInlineDecoration[]; public readonly tabSize: number; public readonly startVisibleColumn: number; public readonly spaceWidth: number; @@ -97,7 +96,7 @@ export class RenderLineInput { fauxIndentLength: number, lineTokens: IViewLineTokens, lineDecorations: LineDecoration[], - injectedTextLineParts: readonly InjectedTextLinePart[], + injectedTextLineParts: readonly FixedWidthInlineDecoration[], tabSize: number, startVisibleColumn: number, spaceWidth: number, @@ -196,7 +195,7 @@ export class RenderLineInput { && this.renderControlCharacters === other.renderControlCharacters && this.fontLigatures === other.fontLigatures && LineDecoration.equalsArr(this.lineDecorations, other.lineDecorations) - && InjectedTextLinePart.equalsArr(this.injectedTextLineParts, other.injectedTextLineParts) + && FixedWidthInlineDecoration.equalsArr(this.injectedTextLineParts, other.injectedTextLineParts) && this.lineTokens.equals(other.lineTokens) && this.sameSelection(other.selectionsOnLine) && this.textDirection === other.textDirection @@ -484,19 +483,6 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput len = lineContent.length; } - let containsForeignElements = input.injectedTextLineParts.length > 0 ? ForeignElementType.Before : ForeignElementType.None; - for (let i = 0, len = input.lineDecorations.length; i < len; i++) { - const lineDecoration = input.lineDecorations[i]; - if (lineDecoration.type === InlineDecorationType.RegularAffectingLetterSpacing) { - // Pretend there are foreign elements... although not 100% accurate. - containsForeignElements |= ForeignElementType.Before; - } else if (lineDecoration.type === InlineDecorationType.Before) { - containsForeignElements |= ForeignElementType.Before; - } else if (lineDecoration.type === InlineDecorationType.After) { - containsForeignElements |= ForeignElementType.After; - } - } - let tokens = transformAndRemoveOverflowing(lineContent, input.containsRTL, input.lineTokens, input.fauxIndentLength, len); if (input.renderControlCharacters && !input.isBasicASCII) { // Calling `extractControlCharacters` before adding (possibly empty) line parts @@ -510,7 +496,19 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput ) { tokens = _applyRenderWhitespace(input, lineContent, len, tokens); } + let containsForeignElements = ForeignElementType.None; if (input.lineDecorations.length > 0) { + for (let i = 0, len = input.lineDecorations.length; i < len; i++) { + const lineDecoration = input.lineDecorations[i]; + if (lineDecoration.type === InlineDecorationType.RegularAffectingLetterSpacing) { + // Pretend there are foreign elements... although not 100% accurate. + containsForeignElements |= ForeignElementType.Before; + } else if (lineDecoration.type === InlineDecorationType.Before) { + containsForeignElements |= ForeignElementType.Before; + } else if (lineDecoration.type === InlineDecorationType.After) { + containsForeignElements |= ForeignElementType.After; + } + } tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations); } tokens = createAtomicInjectedTextParts(tokens, input.injectedTextLineParts); @@ -984,7 +982,7 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP } // Fixed-width injected text replaces all line parts in its projected range with one atomic part. -function createAtomicInjectedTextParts(parts: LinePart[], injectedTextLineParts: readonly InjectedTextLinePart[]): LinePart[] { +function createAtomicInjectedTextParts(parts: LinePart[], injectedTextLineParts: readonly FixedWidthInlineDecoration[]): LinePart[] { if (injectedTextLineParts.length === 0) { return parts; } diff --git a/src/vs/editor/common/viewModel.ts b/src/vs/editor/common/viewModel.ts index a275ecafd261e8..257044dc55a1a1 100644 --- a/src/vs/editor/common/viewModel.ts +++ b/src/vs/editor/common/viewModel.ts @@ -21,8 +21,7 @@ import { BracketGuideOptions, IActiveIndentGuideInfo, IndentGuide } from './text import { IViewLineTokens } from './tokens/lineTokens.js'; import { ViewEventHandler } from './viewEventHandler.js'; import { VerticalRevealType } from './viewEvents.js'; -import { InlineDecoration } from './viewModel/inlineDecorations.js'; -import { InjectedTextLinePart } from './viewModel/injectedTextLinePart.js'; +import { FixedWidthInlineDecoration, InlineDecoration } from './viewModel/inlineDecorations.js'; import { EditorOption, FindComputedEditorOptionValueById } from './config/editorOptions.js'; export interface IViewModel extends ICursorSimpleModel, ISimpleModel { @@ -283,7 +282,7 @@ export class ViewLineData { /** * Fixed-width injected text projected onto this view line. */ - public readonly injectedTextLineParts: readonly InjectedTextLinePart[] | null; + public readonly fixedWidthInlineDecorations: readonly FixedWidthInlineDecoration[] | null; constructor( content: string, @@ -293,7 +292,7 @@ export class ViewLineData { startVisibleColumn: number, tokens: IViewLineTokens, inlineDecorations: readonly InlineDecoration[] | null, - injectedTextLineParts: readonly InjectedTextLinePart[] | null + fixedWidthInlineDecorations: readonly FixedWidthInlineDecoration[] | null ) { this.content = content; this.continuesWithWrappedLine = continuesWithWrappedLine; @@ -302,7 +301,7 @@ export class ViewLineData { this.startVisibleColumn = startVisibleColumn; this.tokens = tokens; this.inlineDecorations = inlineDecorations; - this.injectedTextLineParts = injectedTextLineParts; + this.fixedWidthInlineDecorations = fixedWidthInlineDecorations; } } @@ -340,9 +339,9 @@ export class ViewLineRenderingData { */ public readonly inlineDecorations: InlineDecoration[]; /** - * Fixed-width injected text projected onto this view line. + * Fixed-width inline decorations at this view line. */ - public readonly injectedTextLineParts: readonly InjectedTextLinePart[]; + public readonly fixedWidthInlineDecorations: FixedWidthInlineDecoration[]; /** * The tab size for this view model. */ @@ -369,7 +368,7 @@ export class ViewLineRenderingData { mightContainNonBasicASCII: boolean, tokens: IViewLineTokens, inlineDecorations: InlineDecoration[], - injectedTextLineParts: readonly InjectedTextLinePart[], + fixedWidthInlineDecorations: FixedWidthInlineDecoration[], tabSize: number, startVisibleColumn: number, textDirection: TextDirection, @@ -385,7 +384,7 @@ export class ViewLineRenderingData { this.tokens = tokens; this.inlineDecorations = inlineDecorations; - this.injectedTextLineParts = injectedTextLineParts; + this.fixedWidthInlineDecorations = fixedWidthInlineDecorations; this.tabSize = tabSize; this.startVisibleColumn = startVisibleColumn; this.textDirection = textDirection; diff --git a/src/vs/editor/common/viewModel/injectedTextLinePart.ts b/src/vs/editor/common/viewModel/injectedTextLinePart.ts deleted file mode 100644 index f6fae23f264e7b..00000000000000 --- a/src/vs/editor/common/viewModel/injectedTextLinePart.ts +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * A fixed-width injected text range projected onto a single view line. - */ -export class InjectedTextLinePart { - constructor( - public readonly startColumn: number, - public readonly endColumn: number, - public readonly inlineClassName: string, - public readonly widthInEm: number - ) { } - - public static equalsArr(a: readonly InjectedTextLinePart[], b: readonly InjectedTextLinePart[]): boolean { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if ( - a[i].startColumn !== b[i].startColumn - || a[i].endColumn !== b[i].endColumn - || a[i].inlineClassName !== b[i].inlineClassName - || a[i].widthInEm !== b[i].widthInEm - ) { - return false; - } - } - return true; - } -} diff --git a/src/vs/editor/common/viewModel/inlineDecorations.ts b/src/vs/editor/common/viewModel/inlineDecorations.ts index 46bf1cc6f75aec..a33853fab009b5 100644 --- a/src/vs/editor/common/viewModel/inlineDecorations.ts +++ b/src/vs/editor/common/viewModel/inlineDecorations.ts @@ -8,7 +8,6 @@ import { Range } from '../core/range.js'; import { Position } from '../core/position.js'; import { ICoordinatesConverter } from '../coordinatesConverter.js'; import { isModelDecorationVisible, ViewModelDecoration } from './viewModelDecoration.js'; -import { InjectedTextLinePart } from './injectedTextLinePart.js'; export const enum InlineDecorationType { Regular = 0, @@ -25,9 +24,38 @@ export class InlineDecoration { ) { } } +/** + * A fixed-width inline decoration. + */ +export class FixedWidthInlineDecoration { + constructor( + public readonly startColumn: number, + public readonly endColumn: number, + public readonly inlineClassName: string, + public readonly widthInEm: number + ) { } + + public static equalsArr(a: readonly FixedWidthInlineDecoration[], b: readonly FixedWidthInlineDecoration[]): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if ( + a[i].startColumn !== b[i].startColumn + || a[i].endColumn !== b[i].endColumn + || a[i].inlineClassName !== b[i].inlineClassName + || a[i].widthInEm !== b[i].widthInEm + ) { + return false; + } + } + return true; + } +} + export interface IInjectedTextRenderingData { readonly inlineDecorations: InlineDecoration[][]; - readonly injectedTextLineParts: InjectedTextLinePart[][]; + readonly fixedWidthInlineDecorations: FixedWidthInlineDecoration[][]; } /** @@ -217,16 +245,16 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations constructor(private readonly context: IInjectedTextInlineDecorationsComputerContext) { } public getInlineDecorations(modelLineNumber: number): InlineDecoration[][] { - return this.getDecorations(modelLineNumber).inlineDecorations; + return this.getRenderingData(modelLineNumber).inlineDecorations; } - public getDecorations(modelLineNumber: number): IInjectedTextRenderingData { + public getRenderingData(modelLineNumber: number): IInjectedTextRenderingData { const injectionOffsets = this.context.getInjectionOffsets(modelLineNumber); if (!injectionOffsets) { - return { inlineDecorations: [], injectedTextLineParts: [] }; + return { inlineDecorations: [], fixedWidthInlineDecorations: [] }; } const lineInlineDecorations = []; - const injectedTextLineParts: InjectedTextLinePart[][] = []; + const lineFixedWidthInlineDecorations: FixedWidthInlineDecoration[][] = []; let totalInjectedTextLengthBefore = 0; let currentInjectedOffset = 0; @@ -236,8 +264,8 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations for (let outputLineIndex = 0; outputLineIndex < breakOffsets.length; outputLineIndex++) { const inlineDecorations = new Array(); lineInlineDecorations[outputLineIndex] = inlineDecorations; - const outputLineParts = new Array(); - injectedTextLineParts[outputLineIndex] = outputLineParts; + const fixedWidthInlineDecorations = new Array(); + lineFixedWidthInlineDecorations[outputLineIndex] = fixedWidthInlineDecorations; const lineStartOffsetInInputWithInjections = outputLineIndex > 0 ? breakOffsets[outputLineIndex - 1] : 0; const lineEndOffsetInInputWithInjections = breakOffsets[outputLineIndex]; @@ -263,11 +291,11 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations if (start !== end) { const viewLineNumber = this.context.getBaseViewLineNumber(modelLineNumber) + outputLineIndex; if (options.widthInEm !== undefined) { - outputLineParts.push(new InjectedTextLinePart(start + 1, end + 1, options.inlineClassName ?? '', options.widthInEm)); - } else if (options.inlineClassName) { + fixedWidthInlineDecorations.push(new FixedWidthInlineDecoration(start + 1, end + 1, options.inlineClassName ?? '', options.widthInEm)); + } else { const range = new Range(viewLineNumber, start + 1, viewLineNumber, end + 1); const type: InlineDecorationType = options.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular; - inlineDecorations.push(new InlineDecoration(range, options.inlineClassName, type)); + inlineDecorations.push(new InlineDecoration(range, options.inlineClassName ?? '', type)); } } } @@ -281,6 +309,6 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations } } } - return { inlineDecorations: lineInlineDecorations, injectedTextLineParts }; + return { inlineDecorations: lineInlineDecorations, fixedWidthInlineDecorations: lineFixedWidthInlineDecorations }; } } diff --git a/src/vs/editor/common/viewModel/modelLineProjection.ts b/src/vs/editor/common/viewModel/modelLineProjection.ts index 8a18218d949c56..53d324ed6db682 100644 --- a/src/vs/editor/common/viewModel/modelLineProjection.ts +++ b/src/vs/editor/common/viewModel/modelLineProjection.ts @@ -10,9 +10,8 @@ import { EndOfLinePreference, ITextModel, PositionAffinity } from '../model.js'; import { LineInjectedText } from '../textModelEvents.js'; import { InjectedText, ModelLineProjectionData } from '../modelLineProjectionData.js'; import { ViewLineData } from '../viewModel.js'; -import { IInjectedTextInlineDecorationsComputerContext, InjectedTextInlineDecorationsComputer, InlineDecoration } from './inlineDecorations.js'; +import { FixedWidthInlineDecoration, IInjectedTextInlineDecorationsComputerContext, InjectedTextInlineDecorationsComputer, InlineDecoration } from './inlineDecorations.js'; import { getLineTokensWithInjections } from '../model/textModel.js'; -import { InjectedTextLinePart } from './injectedTextLinePart.js'; export interface IModelLineProjection { isVisible(): boolean; @@ -174,7 +173,9 @@ class ModelLineProjection implements IModelLineProjection { getBaseViewLineNumber: () => baseViewLineNumber }; const computer = new InjectedTextInlineDecorationsComputer(context); - const injectedTextRenderingData = computer.getDecorations(modelLineNumber); + const injectedTextRenderingData = computer.getRenderingData(modelLineNumber); + const lineInlineDecorations = injectedTextRenderingData.inlineDecorations; + const lineFixedWidthInlineDecorations = injectedTextRenderingData.fixedWidthInlineDecorations; const lineTokens = model.tokenization.getLineTokens(modelLineNumber); const lineWithInjections = getLineTokensWithInjections(lineTokens, injectionOptions, injectionOffsets); @@ -184,16 +185,11 @@ class ModelLineProjection implements IModelLineProjection { result[globalIndex] = null; continue; } - result[globalIndex] = this._getViewLineData( - lineWithInjections, - injectedTextRenderingData.inlineDecorations[outputLineIndex] ?? null, - injectedTextRenderingData.injectedTextLineParts[outputLineIndex] ?? null, - outputLineIndex - ); + result[globalIndex] = this._getViewLineData(lineWithInjections, lineInlineDecorations[outputLineIndex] ?? null, lineFixedWidthInlineDecorations[outputLineIndex] ?? null, outputLineIndex); } } - private _getViewLineData(lineWithInjections: LineTokens, inlineDecorations: null | InlineDecoration[], injectedTextLineParts: null | InjectedTextLinePart[], outputLineIndex: number): ViewLineData { + private _getViewLineData(lineWithInjections: LineTokens, inlineDecorations: null | InlineDecoration[], fixedWidthInlineDecorations: null | FixedWidthInlineDecoration[], outputLineIndex: number): ViewLineData { this._assertVisible(); const lineBreakData = this._projectionData; const deltaStartIndex = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0); @@ -220,7 +216,7 @@ class ModelLineProjection implements IModelLineProjection { startVisibleColumn, tokens, inlineDecorations, - injectedTextLineParts + fixedWidthInlineDecorations ); } diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index b0feb6a2b9d455..1c75ff246a3f34 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -895,6 +895,7 @@ export class ViewModel extends Disposable implements IViewModel { const mightContainNonBasicASCII = this.model.mightContainNonBasicASCII(); const tabSize = this.getTabSize(); const lineData = this._lines.getViewLineData(lineNumber); + const fixedWidthInlineDecorations = lineData.fixedWidthInlineDecorations ?? []; if (lineData.inlineDecorations) { inlineDecorations = [ @@ -912,7 +913,7 @@ export class ViewModel extends Disposable implements IViewModel { mightContainNonBasicASCII, lineData.tokens, inlineDecorations, - lineData.injectedTextLineParts ?? [], + fixedWidthInlineDecorations, tabSize, lineData.startVisibleColumn, this._getTextDirection(lineNumber, decorations), diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index 936f9e10c10bee..b8c9ef724166e4 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -7,7 +7,7 @@ import { CancelablePromise, createCancelablePromise, TimeoutTimer } from '../../ import { RGBA } from '../../../../base/common/color.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { noBreakWhitespace } from '../../../../base/common/strings.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; @@ -49,7 +49,9 @@ export class ColorDetector extends Disposable implements IEditorContribution { private readonly _decoratorLimitReporter = this._register(new DecoratorLimitReporter()); - private static readonly colorDecoratorWidthInEm = 1.2; + private static readonly colorDecoratorSizeInEm = 0.8; + private static readonly colorDecoratorMarginInEm = 0.2; + private static readonly colorDecoratorWidthInEm = this.colorDecoratorSizeInEm + 2 * this.colorDecoratorMarginInEm; constructor( private readonly _editor: ICodeEditor, @@ -60,6 +62,13 @@ export class ColorDetector extends Disposable implements IEditorContribution { super(); this._colorDecoratorIds = this._editor.createDecorationsCollection(); this._ruleFactory = this._register(new DynamicCssRules(this._editor)); + const editorDomNode = this._editor.getContainerDomNode(); + editorDomNode.style.setProperty('--vscode-colorPicker-colorDecoratorSize', `${ColorDetector.colorDecoratorSizeInEm}em`); + editorDomNode.style.setProperty('--vscode-colorPicker-colorDecoratorMargin', `${ColorDetector.colorDecoratorMarginInEm}em`); + this._register(toDisposable(() => { + editorDomNode.style.removeProperty('--vscode-colorPicker-colorDecoratorSize'); + editorDomNode.style.removeProperty('--vscode-colorPicker-colorDecoratorMargin'); + })); this._debounceInformation = languageFeatureDebounceService.for(_languageFeaturesService.colorProvider, 'Document Colors', { min: ColorDetector.RECOMPUTE_TIME }); this._register(_editor.onDidChangeModel(() => { this._isColorDecoratorsEnabled = this.isEnabled(); @@ -208,6 +217,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { const decorations: IModelDeltaDecoration[] = []; const limit = this._editor.getOption(EditorOption.colorDecoratorsLimit); + for (let i = 0; i < colorData.length && decorations.length < limit; i++) { const { red, green, blue, alpha } = colorData[i].colorInfo.color; const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha); diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css index 24696139e515a3..e8663eaecb7050 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css +++ b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css @@ -14,10 +14,10 @@ .colorpicker-color-decoration, .hc-light .colorpicker-color-decoration { box-sizing: border-box; - padding: 0 0.2em; + padding: 0 var(--vscode-colorPicker-colorDecoratorMargin); background-clip: content-box; - height: 0.8em; - line-height: 0.8em; + height: var(--vscode-colorPicker-colorDecoratorSize); + line-height: var(--vscode-colorPicker-colorDecoratorSize); display: inline-block; position: relative; cursor: pointer; @@ -26,7 +26,7 @@ .colorpicker-color-decoration::after { content: ''; position: absolute; - inset: 0 0.2em; + inset: 0 var(--vscode-colorPicker-colorDecoratorMargin); box-sizing: border-box; border: solid 0.1em #000; pointer-events: none; @@ -215,6 +215,6 @@ cursor: pointer; } -.colorpicker-body .insert-button:hover{ +.colorpicker-body .insert-button:hover { background: var(--vscode-button-hoverBackground); } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 466c261f89cb67..d22d005aa0ce12 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -465,7 +465,7 @@ class RenderedStickyLine { const renderLineInput: RenderLineInput = new RenderLineInput(true, true, lineRenderingData.content, lineRenderingData.continuesWithWrappedLine, lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0, - lineRenderingData.tokens, actualInlineDecorations, lineRenderingData.injectedTextLineParts, + lineRenderingData.tokens, actualInlineDecorations, lineRenderingData.fixedWidthInlineDecorations, lineRenderingData.tabSize, lineRenderingData.startVisibleColumn, 1, 1, 1, 500, 'none', true, true, null, textDirection, verticalScrollbarSize diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 58de04cd5d563e..843ab6ee11cb4a 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -17,12 +17,12 @@ import { TextModel } from '../../../common/model/textModel.js'; import { ModelLineProjectionData } from '../../../common/modelLineProjectionData.js'; import { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; import { ViewLineData } from '../../../common/viewModel.js'; -import { InjectedTextLinePart } from '../../../common/viewModel/injectedTextLinePart.js'; import { IModelLineProjection, ISimpleModel, createModelLineProjection } from '../../../common/viewModel/modelLineProjection.js'; import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js'; import { ViewModelLinesFromProjectedModel } from '../../../common/viewModel/viewModelLines.js'; import { TestConfiguration } from '../config/testConfiguration.js'; import { createTextModel } from '../../common/testTextModel.js'; +import { FixedWidthInlineDecoration } from '../../../common/viewModel/inlineDecorations.js'; suite('Editor ViewModel - SplitLinesCollection', () => { @@ -954,8 +954,8 @@ suite('SplitLinesCollection', () => { }]); withSplitLinesCollection(model, 'wordWrapColumn', 30, false, splitLinesCollection => { - assert.deepStrictEqual(splitLinesCollection.getViewLineData(1).injectedTextLineParts, [ - new InjectedTextLinePart(9, 10, 'fixed-width', 3) + assert.deepStrictEqual(splitLinesCollection.getViewLineData(1).fixedWidthInlineDecorations, [ + new FixedWidthInlineDecoration(9, 10, 'fixed-width', 3) ]); }); }); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 8bb01e56182392..00208266938238 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -13,8 +13,7 @@ import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; import { IViewLineTokens } from '../../../common/tokens/lineTokens.js'; import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js'; import { CharacterMapping, DomPosition, IRenderLineInputOptions, RenderLineInput, RenderLineOutput2, renderViewLine2 as renderViewLine } from '../../../common/viewLayout/viewLineRenderer.js'; -import { InlineDecorationType } from '../../../common/viewModel/inlineDecorations.js'; -import { InjectedTextLinePart } from '../../../common/viewModel/injectedTextLinePart.js'; +import { FixedWidthInlineDecoration, InlineDecorationType } from '../../../common/viewModel/inlineDecorations.js'; import { TestLineToken, TestLineTokens } from '../core/testLineToken.js'; const HTML_EXTENSION = { extension: 'html' }; @@ -198,7 +197,7 @@ suite('renderViewLine', () => { const actual = renderViewLine(createRenderLineInput({ lineContent: '\xa0', lineTokens: createViewLineTokens([createPart(1, 0)]), - injectedTextLineParts: [new InjectedTextLinePart(1, 2, 'injected', 1)] + injectedTextLineParts: [new FixedWidthInlineDecoration(1, 2, 'injected', 1)] })); assert.strictEqual(actual.html, '\xa0'); @@ -208,7 +207,7 @@ suite('renderViewLine', () => { const actual = renderViewLine(createRenderLineInput({ lineContent: '\xa0', lineTokens: createViewLineTokens([createPart(1, 0)]), - injectedTextLineParts: [new InjectedTextLinePart(1, 2, '', 1)] + injectedTextLineParts: [new FixedWidthInlineDecoration(1, 2, '', 1)] })); assert.strictEqual(actual.html, '\xa0'); @@ -218,7 +217,7 @@ suite('renderViewLine', () => { const actual = renderViewLine(createRenderLineInput({ lineContent: ' ', lineTokens: createViewLineTokens([createPart(1, 0)]), - injectedTextLineParts: [new InjectedTextLinePart(1, 2, 'injected', 1)], + injectedTextLineParts: [new FixedWidthInlineDecoration(1, 2, 'injected', 1)], renderWhitespace: 'all' })); @@ -229,7 +228,7 @@ suite('renderViewLine', () => { const actual = renderViewLine(createRenderLineInput({ lineContent: 'xabcy', lineTokens: createViewLineTokens([createPart(1, 0), createPart(4, 0), createPart(5, 0)]), - injectedTextLineParts: [new InjectedTextLinePart(2, 5, 'injected', 3)] + injectedTextLineParts: [new FixedWidthInlineDecoration(2, 5, 'injected', 3)] })); assert.strictEqual(actual.html, 'xabcy'); @@ -239,7 +238,7 @@ suite('renderViewLine', () => { const actual = renderViewLine(createRenderLineInput({ lineContent: 'xabcy', lineTokens: createViewLineTokens([createPart(5, 0)]), - injectedTextLineParts: [new InjectedTextLinePart(2, 5, 'injected', 3)] + injectedTextLineParts: [new FixedWidthInlineDecoration(2, 5, 'injected', 3)] })); assert.strictEqual(actual.html, 'xabcy'); @@ -249,7 +248,7 @@ suite('renderViewLine', () => { const actual = renderViewLine(createRenderLineInput({ lineContent: 'a b', lineTokens: createViewLineTokens([createPart(1, 1), createPart(3, 2)]), - injectedTextLineParts: [new InjectedTextLinePart(1, 4, 'injected', 3)], + injectedTextLineParts: [new FixedWidthInlineDecoration(1, 4, 'injected', 3)], renderWhitespace: 'all' })); @@ -261,8 +260,8 @@ suite('renderViewLine', () => { lineContent: 'ab', lineTokens: createViewLineTokens([createPart(1, 0), createPart(2, 0)]), injectedTextLineParts: [ - new InjectedTextLinePart(1, 2, 'injected', 1), - new InjectedTextLinePart(2, 3, 'injected', 1) + new FixedWidthInlineDecoration(1, 2, 'injected', 1), + new FixedWidthInlineDecoration(2, 3, 'injected', 1) ] })); @@ -274,7 +273,7 @@ suite('renderViewLine', () => { lineContent: 'abc', lineTokens: createViewLineTokens([createPart(3, 0)]), lineDecorations: [new LineDecoration(1, 4, 'secondary', InlineDecorationType.Regular)], - injectedTextLineParts: [new InjectedTextLinePart(1, 4, 'injected', 3)] + injectedTextLineParts: [new FixedWidthInlineDecoration(1, 4, 'injected', 3)] })); assert.strictEqual(actual.html, 'abc'); @@ -286,7 +285,7 @@ suite('renderViewLine', () => { isBasicASCII: false, containsRTL: true, lineTokens: createViewLineTokens([createPart(3, 0)]), - injectedTextLineParts: [new InjectedTextLinePart(1, 4, 'injected', 3)] + injectedTextLineParts: [new FixedWidthInlineDecoration(1, 4, 'injected', 3)] })); assert.strictEqual(actual.html, '\xa0אב'); @@ -296,7 +295,7 @@ suite('renderViewLine', () => { const actual = renderViewLine(createRenderLineInput({ lineContent: 'abcde', lineTokens: createViewLineTokens([createPart(1, 0), createPart(5, 0)]), - injectedTextLineParts: [new InjectedTextLinePart(2, 6, 'injected', 3)], + injectedTextLineParts: [new FixedWidthInlineDecoration(2, 6, 'injected', 3)], stopRenderingLineAfter: 3 })); diff --git a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts index e85359d076e6f8..9405fad93ea72e 100644 --- a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts @@ -7,8 +7,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { Range } from '../../../common/core/range.js'; import { IModelDecoration, IModelDecorationOptions, InjectedTextOptions } from '../../../common/model.js'; -import { InlineDecoration, InlineDecorationType, InlineModelDecorationsComputer, IInlineModelDecorationsComputerContext, InjectedTextInlineDecorationsComputer, IInjectedTextInlineDecorationsComputerContext } from '../../../common/viewModel/inlineDecorations.js'; -import { InjectedTextLinePart } from '../../../common/viewModel/injectedTextLinePart.js'; +import { InlineDecoration, InlineDecorationType, InlineModelDecorationsComputer, IInlineModelDecorationsComputerContext, InjectedTextInlineDecorationsComputer, IInjectedTextInlineDecorationsComputerContext, FixedWidthInlineDecoration } from '../../../common/viewModel/inlineDecorations.js'; import { createTextModel } from '../testTextModel.js'; import { IdentityCoordinatesConverter } from '../../../common/coordinatesConverter.js'; @@ -329,10 +328,10 @@ suite('InjectedTextInlineDecorationsComputer', () => { getBaseViewLineNumber: () => 1, }; const computer = new InjectedTextInlineDecorationsComputer(context); - const result = computer.getDecorations(1); + const result = computer.getRenderingData(1); assert.deepStrictEqual(result, { inlineDecorations: [[]], - injectedTextLineParts: [[new InjectedTextLinePart(6, 7, '', 3)]] + injectedTextLineParts: [[new FixedWidthInlineDecoration(6, 7, '', 3)]] }); }); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts index d6567b21f59da8..16f939add7362f 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts @@ -23,7 +23,7 @@ import { ComponentFixtureContext, createEditorServices, createTextModel, defineC const colorDetectorContribution = EditorExtensionsRegistry.getSomeEditorContributions([ColorDetector.ID])[0]; const inlayHintsContribution = EditorExtensionsRegistry.getSomeEditorContributions([InlayHintsController.ID])[0]; -async function renderColorDecorators(context: ComponentFixtureContext): Promise { +async function renderColorDecorators(context: ComponentFixtureContext, selectFirstColor = false): Promise { const { editor } = createEditor( context, '.red { color: #ff0000; }\n.green { color: #00ff00; }\n.blue { color: #0000ff; }', @@ -46,6 +46,10 @@ async function renderColorDecorators(context: ComponentFixtureContext): Promise< ); editor.getContribution(ColorDetector.ID); await timeout(0); + if (selectFirstColor) { + editor.setSelection(new Range(1, 14, 1, editor.getModel()!.getLineMaxColumn(1))); + editor.focus(); + } } async function renderInlineProgress(context: ComponentFixtureContext): Promise { @@ -176,6 +180,11 @@ export default defineThemedFixtureGroup({ path: 'editor/' }, { expectedVisualDescriptions: ['Three CSS declarations appear on separate lines. Each hexadecimal color is preceded by a square swatch whose fill matches the value. Every swatch is the same size, has a visible contrasting border, and is vertically aligned with its line of text.'], render: renderColorDecorators, }), + SelectedColorDecorator: defineComponentFixture({ + labels: { kind: 'screenshot', blocksCi: true }, + expectedVisualDescriptions: ['The first CSS color and the text after it are selected. The selection is continuous on both sides of the square red swatch and ends at the closing brace without detached or misplaced selection blocks.'], + render: context => renderColorDecorators(context, true), + }), InlineProgress: defineComponentFixture({ labels: { kind: 'screenshot', blocksCi: true }, expectedVisualDescriptions: ['A single TypeScript statement appears on one line. A small inline progress placeholder separates the equals sign from await without changing the line height or vertical alignment.'], From d7c05c27a14c653f29fbc069f7ccb48a3f5b1fdc Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 24 Aug 2026 17:10:44 +0200 Subject: [PATCH 18/41] removing rendering code --- .../native/screenReaderContentRich.ts | 1 - .../browser/viewParts/viewLines/viewLine.ts | 1 - .../components/accessibleDiffViewer.ts | 1 - .../diffEditorViewZones/renderLines.ts | 1 - src/vs/editor/common/model.ts | 3 +- src/vs/editor/common/model/textModel.ts | 4 +- src/vs/editor/common/viewLayout/linePart.ts | 3 +- .../common/viewLayout/viewLineRenderer.ts | 90 ++------------ src/vs/editor/common/viewModel.ts | 16 +-- .../common/viewModel/inlineDecorations.ts | 57 +-------- .../common/viewModel/modelLineProjection.ts | 14 +-- .../editor/common/viewModel/viewModelImpl.ts | 2 - .../editor/common/viewModel/viewModelLines.ts | 1 - .../colorPicker/browser/colorDetector.ts | 13 +- .../colorPicker/browser/colorPicker.css | 25 ++-- .../browser/view/ghostText/ghostTextView.ts | 1 - .../browser/stickyScrollWidget.ts | 2 +- src/vs/editor/standalone/browser/colorizer.ts | 3 - .../viewModel/modelLineProjection.test.ts | 22 ---- .../viewLayout/viewLineRenderer.test.ts | 114 +----------------- .../viewModel/inlineDecorations.test.ts | 15 ++- .../common/viewModel/lineBreakData.test.ts | 12 -- 22 files changed, 44 insertions(+), 357 deletions(-) diff --git a/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts b/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts index 7e417195bfd482..7a274e27f3bebb 100644 --- a/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts +++ b/src/vs/editor/browser/controller/editContext/native/screenReaderContentRich.ts @@ -195,7 +195,6 @@ export class RichScreenReaderContent extends Disposable implements IScreenReader positionLineData.minColumn - 1, positionLineData.tokens, lineDecorations, - positionLineData.fixedWidthInlineDecorations, positionLineData.tabSize, positionLineData.startVisibleColumn, fontInfo.spaceWidth, diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index 118d9a9268549b..3bf79d8b679259 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -158,7 +158,6 @@ export class ViewLine implements IVisibleLine { lineData.minColumn - 1, lineData.tokens, actualInlineDecorations, - lineData.fixedWidthInlineDecorations, lineData.tabSize, lineData.startVisibleColumn, options.spaceWidth, diff --git a/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts b/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts index 088fda4c6c34ec..9660b641fddbdd 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts @@ -672,7 +672,6 @@ class View extends Disposable { 0, lineTokens, [], - [], tabSize, 0, fontInfo.spaceWidth, diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts index bc7a33f63d7cec..fc04faf71044e1 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.ts @@ -295,7 +295,6 @@ function renderOriginalLine( 0, lineTokens, decorations, - [], options.tabSize, 0, options.fontInfo.spaceWidth, diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 4f189021c028f3..34c57f255760cf 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -348,8 +348,7 @@ export interface InjectedTextOptions { readonly inlineClassNameAffectsLetterSpacing?: boolean; /** - * Sets the atomic rendered width and wrapping width of this injected text in editor-font em units. - * Cannot be combined with {@link tokens}. + * Sets the width used to wrap this injected text in editor-font em units. * @internal */ readonly widthInEm?: number; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 62d1579f571260..8d934c61b32947 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2468,8 +2468,8 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt readonly cursorStops: model.InjectedTextCursorStops | null; private constructor(options: model.InjectedTextOptions) { - if (options.widthInEm !== undefined && options.tokens) { - throw new BugIndicatingError('Injected text cannot define both tokens and widthInEm'); + if (options.widthInEm !== undefined && (!Number.isFinite(options.widthInEm) || options.widthInEm < 0)) { + throw new BugIndicatingError('Injected text widthInEm must be a finite non-negative number'); } this.content = options.content || ''; this.tokens = options.tokens ?? null; diff --git a/src/vs/editor/common/viewLayout/linePart.ts b/src/vs/editor/common/viewLayout/linePart.ts index 29eeb12e5a570a..a179915fca60b0 100644 --- a/src/vs/editor/common/viewLayout/linePart.ts +++ b/src/vs/editor/common/viewLayout/linePart.ts @@ -23,8 +23,7 @@ export class LinePart { public readonly endIndex: number, public readonly type: string, public readonly metadata: number, - public readonly containsRTL: boolean, - public readonly widthInEm: number | undefined = undefined + public readonly containsRTL: boolean ) { } public isWhitespace(): boolean { diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index adb1a79c29ea9f..11aca36bbb9a7c 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -11,7 +11,7 @@ import { StringBuilder } from '../core/stringBuilder.js'; import { LineDecoration, LineDecorationsNormalizer } from './lineDecorations.js'; import { LinePart, LinePartMetadata } from './linePart.js'; import { OffsetRange } from '../core/ranges/offsetRange.js'; -import { FixedWidthInlineDecoration, InlineDecorationType } from '../viewModel/inlineDecorations.js'; +import { InlineDecorationType } from '../viewModel/inlineDecorations.js'; import { TextDirection } from '../model.js'; export const enum RenderWhitespace { @@ -32,7 +32,6 @@ export interface IRenderLineInputOptions { fauxIndentLength: number; lineTokens: IViewLineTokens; lineDecorations: LineDecoration[]; - injectedTextLineParts: readonly FixedWidthInlineDecoration[]; tabSize: number; startVisibleColumn: number; spaceWidth: number; @@ -59,7 +58,6 @@ export class RenderLineInput { public readonly fauxIndentLength: number; public readonly lineTokens: IViewLineTokens; public readonly lineDecorations: LineDecoration[]; - public readonly injectedTextLineParts: readonly FixedWidthInlineDecoration[]; public readonly tabSize: number; public readonly startVisibleColumn: number; public readonly spaceWidth: number; @@ -96,7 +94,6 @@ export class RenderLineInput { fauxIndentLength: number, lineTokens: IViewLineTokens, lineDecorations: LineDecoration[], - injectedTextLineParts: readonly FixedWidthInlineDecoration[], tabSize: number, startVisibleColumn: number, spaceWidth: number, @@ -120,7 +117,6 @@ export class RenderLineInput { this.fauxIndentLength = fauxIndentLength; this.lineTokens = lineTokens; this.lineDecorations = lineDecorations.sort(LineDecoration.compare); - this.injectedTextLineParts = injectedTextLineParts; this.tabSize = tabSize; this.startVisibleColumn = startVisibleColumn; this.spaceWidth = spaceWidth; @@ -195,7 +191,6 @@ export class RenderLineInput { && this.renderControlCharacters === other.renderControlCharacters && this.fontLigatures === other.fontLigatures && LineDecoration.equalsArr(this.lineDecorations, other.lineDecorations) - && FixedWidthInlineDecoration.equalsArr(this.injectedTextLineParts, other.injectedTextLineParts) && this.lineTokens.equals(other.lineTokens) && this.sameSelection(other.selectionsOnLine) && this.textDirection === other.textDirection @@ -511,7 +506,6 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput } tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations); } - tokens = createAtomicInjectedTextParts(tokens, input.injectedTextLineParts); if (!input.containsRTL) { // We can never split RTL text, as it ruins the rendering tokens = splitLargeTokens(lineContent, tokens, !input.isBasicASCII || input.fontLigatures); @@ -594,7 +588,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: for (let i = 0, len = tokens.length; i < len; i++) { const token = tokens[i]; const tokenEndIndex = token.endIndex; - if (token.widthInEm === undefined && lastTokenEndIndex + Constants.LongToken < tokenEndIndex) { + if (lastTokenEndIndex + Constants.LongToken < tokenEndIndex) { const tokenType = token.type; const tokenMetadata = token.metadata; const tokenContainsRTL = token.containsRTL; @@ -607,13 +601,13 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: } if (lastSpaceOffset !== -1 && j - currTokenStart >= Constants.LongToken) { // Split at `lastSpaceOffset` + 1 - result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType, tokenMetadata, tokenContainsRTL, undefined); + result[resultLen++] = new LinePart(lastSpaceOffset + 1, tokenType, tokenMetadata, tokenContainsRTL); currTokenStart = lastSpaceOffset + 1; lastSpaceOffset = -1; } } if (currTokenStart !== tokenEndIndex) { - result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL, undefined); + result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL); } } else { result[resultLen++] = token; @@ -627,16 +621,16 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: const token = tokens[i]; const tokenEndIndex = token.endIndex; const diff = (tokenEndIndex - lastTokenEndIndex); - if (token.widthInEm === undefined && diff > Constants.LongToken) { + if (diff > Constants.LongToken) { const tokenType = token.type; const tokenMetadata = token.metadata; const tokenContainsRTL = token.containsRTL; const piecesCount = Math.ceil(diff / Constants.LongToken); for (let j = 1; j < piecesCount; j++) { const pieceEndIndex = lastTokenEndIndex + (j * Constants.LongToken); - result[resultLen++] = new LinePart(pieceEndIndex, tokenType, tokenMetadata, tokenContainsRTL, undefined); + result[resultLen++] = new LinePart(pieceEndIndex, tokenType, tokenMetadata, tokenContainsRTL); } - result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL, undefined); + result[resultLen++] = new LinePart(tokenEndIndex, tokenType, tokenMetadata, tokenContainsRTL); } else { result[resultLen++] = token; } @@ -656,9 +650,6 @@ function splitLeadingWhitespaceFromRTL(lineContent: string, tokens: LinePart[]): } const firstToken = tokens[0]; - if (firstToken.widthInEm !== undefined) { - return tokens; - } if (!firstToken.containsRTL) { return tokens; } @@ -981,61 +972,6 @@ function _applyInlineDecorations(lineContent: string, len: number, tokens: LineP return result; } -// Fixed-width injected text replaces all line parts in its projected range with one atomic part. -function createAtomicInjectedTextParts(parts: LinePart[], injectedTextLineParts: readonly FixedWidthInlineDecoration[]): LinePart[] { - if (injectedTextLineParts.length === 0) { - return parts; - } - - const result: LinePart[] = []; - let partIndex = 0; - let partStartIndex = 0; - const renderedEndIndex = parts[parts.length - 1]?.endIndex ?? 0; - - for (const injectedTextPart of injectedTextLineParts) { - const injectedTextStartIndex = injectedTextPart.startColumn - 1; - const injectedTextEndIndex = Math.min(injectedTextPart.endColumn - 1, renderedEndIndex); - if (injectedTextStartIndex >= injectedTextEndIndex) { - continue; - } - - while (partIndex < parts.length && partStartIndex < injectedTextStartIndex) { - const part = parts[partIndex]; - const endIndex = Math.min(part.endIndex, injectedTextStartIndex); - result.push(new LinePart(endIndex, part.type, part.metadata, part.containsRTL, part.widthInEm)); - partStartIndex = endIndex; - if (partStartIndex === part.endIndex) { - partIndex++; - } - } - - const firstPart = parts[partIndex]; - if (!firstPart) { - break; - } - - let containsRTL = false; - while (partIndex < parts.length && partStartIndex < injectedTextEndIndex) { - const part = parts[partIndex]; - containsRTL ||= part.containsRTL; - partStartIndex = Math.min(part.endIndex, injectedTextEndIndex); - if (partStartIndex === part.endIndex) { - partIndex++; - } - } - - const type = injectedTextPart.inlineClassName ? firstPart.type + ' ' + injectedTextPart.inlineClassName : firstPart.type; - result.push(new LinePart(injectedTextEndIndex, type, 0, containsRTL, injectedTextPart.widthInEm)); - } - - while (partIndex < parts.length) { - const part = parts[partIndex++]; - result.push(new LinePart(part.endIndex, part.type, part.metadata, part.containsRTL, part.widthInEm)); - } - - return result; -} - /** * This function is on purpose not split up into multiple functions to allow runtime type inference (i.e. performance reasons). * Notice how all the needed data is fully resolved and passed in (i.e. no other calls). @@ -1075,23 +1011,13 @@ function _renderLine(input: ResolvedRenderLineInput, sb: StringBuilder): RenderL const partEndIndex = part.endIndex; const partType = part.type; const partContainsRTL = part.containsRTL; - const partWidthInEm = part.widthInEm; const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && part.isWhitespace()); const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements); const partIsEmptyAndHasPseudoAfter = (charIndex === partEndIndex && part.isPseudoAfter()); charOffsetInPart = 0; sb.appendString('xyz', [[0, [0, 0]], [1, [0, 1]], [2, [1, 0]], [3, [1, 1]]]); }); - test('enforces fixed injected text width on a flat span', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: '\xa0', - lineTokens: createViewLineTokens([createPart(1, 0)]), - injectedTextLineParts: [new FixedWidthInlineDecoration(1, 2, 'injected', 1)] - })); - - assert.strictEqual(actual.html, '\xa0'); - }); - - test('enforces fixed injected text width without a class name', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: '\xa0', - lineTokens: createViewLineTokens([createPart(1, 0)]), - injectedTextLineParts: [new FixedWidthInlineDecoration(1, 2, '', 1)] - })); - - assert.strictEqual(actual.html, '\xa0'); - }); - - test('does not render whitespace markers inside fixed injected text', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: ' ', - lineTokens: createViewLineTokens([createPart(1, 0)]), - injectedTextLineParts: [new FixedWidthInlineDecoration(1, 2, 'injected', 1)], - renderWhitespace: 'all' - })); - - assert.strictEqual(actual.html, '\xa0'); - }); - - test('preserves content around fixed injected text', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: 'xabcy', - lineTokens: createViewLineTokens([createPart(1, 0), createPart(4, 0), createPart(5, 0)]), - injectedTextLineParts: [new FixedWidthInlineDecoration(2, 5, 'injected', 3)] - })); - - assert.strictEqual(actual.html, 'xabcy'); - }); - - test('splits line parts at fixed injected text boundaries', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: 'xabcy', - lineTokens: createViewLineTokens([createPart(5, 0)]), - injectedTextLineParts: [new FixedWidthInlineDecoration(2, 5, 'injected', 3)] - })); - - assert.strictEqual(actual.html, 'xabcy'); - }); - - test('applies fixed injected text width once across line parts', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: 'a b', - lineTokens: createViewLineTokens([createPart(1, 1), createPart(3, 2)]), - injectedTextLineParts: [new FixedWidthInlineDecoration(1, 4, 'injected', 3)], - renderWhitespace: 'all' - })); - - assert.strictEqual(actual.html, 'a\xa0b'); - }); - - test('keeps adjacent equal fixed widths separate', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: 'ab', - lineTokens: createViewLineTokens([createPart(1, 0), createPart(2, 0)]), - injectedTextLineParts: [ - new FixedWidthInlineDecoration(1, 2, 'injected', 1), - new FixedWidthInlineDecoration(2, 3, 'injected', 1) - ] - })); - - assert.strictEqual(actual.html, 'ab'); - }); - - test('applies decorations covering fixed-width injected text', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: 'abc', - lineTokens: createViewLineTokens([createPart(3, 0)]), - lineDecorations: [new LineDecoration(1, 4, 'secondary', InlineDecorationType.Regular)], - injectedTextLineParts: [new FixedWidthInlineDecoration(1, 4, 'injected', 3)] - })); - - assert.strictEqual(actual.html, 'abc'); - }); - - test('keeps fixed-width RTL injected text atomic', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: ' אב', - isBasicASCII: false, - containsRTL: true, - lineTokens: createViewLineTokens([createPart(3, 0)]), - injectedTextLineParts: [new FixedWidthInlineDecoration(1, 4, 'injected', 3)] - })); - - assert.strictEqual(actual.html, '\xa0אב'); - }); - - test('clamps fixed injected text to the rendered line length', () => { - const actual = renderViewLine(createRenderLineInput({ - lineContent: 'abcde', - lineTokens: createViewLineTokens([createPart(1, 0), createPart(5, 0)]), - injectedTextLineParts: [new FixedWidthInlineDecoration(2, 6, 'injected', 3)], - stopRenderingLineAfter: 3 - })); - - assert.ok(actual.html.includes('bc')); - }); - // overflow test('overflow', async () => { const _actual = renderViewLine(createRenderLineInput({ @@ -540,7 +429,6 @@ suite('renderViewLine', () => { 0, lineTokens, [], - [], 4, 0, 10, diff --git a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts index 9405fad93ea72e..1169e66b7abb1e 100644 --- a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; import { Range } from '../../../common/core/range.js'; import { IModelDecoration, IModelDecorationOptions, InjectedTextOptions } from '../../../common/model.js'; -import { InlineDecoration, InlineDecorationType, InlineModelDecorationsComputer, IInlineModelDecorationsComputerContext, InjectedTextInlineDecorationsComputer, IInjectedTextInlineDecorationsComputerContext, FixedWidthInlineDecoration } from '../../../common/viewModel/inlineDecorations.js'; +import { InlineDecoration, InlineDecorationType, InlineModelDecorationsComputer, IInlineModelDecorationsComputerContext, InjectedTextInlineDecorationsComputer, IInjectedTextInlineDecorationsComputerContext } from '../../../common/viewModel/inlineDecorations.js'; import { createTextModel } from '../testTextModel.js'; import { IdentityCoordinatesConverter } from '../../../common/coordinatesConverter.js'; @@ -316,9 +316,9 @@ suite('InjectedTextInlineDecorationsComputer', () => { ]); }); - test('fixed width injection creates a projected line part', () => { + test('fixed width injection uses its regular inline decoration', () => { const injectionOptions: InjectedTextOptions[] = [ - { content: '\xa0', widthInEm: 3 } + { content: '\xa0', inlineClassName: 'fixed-width', widthInEm: 3 } ]; const context: IInjectedTextInlineDecorationsComputerContext = { getInjectionOptions: () => injectionOptions, @@ -328,11 +328,10 @@ suite('InjectedTextInlineDecorationsComputer', () => { getBaseViewLineNumber: () => 1, }; const computer = new InjectedTextInlineDecorationsComputer(context); - const result = computer.getRenderingData(1); - assert.deepStrictEqual(result, { - inlineDecorations: [[]], - injectedTextLineParts: [[new FixedWidthInlineDecoration(6, 7, '', 3)]] - }); + const result = computer.getInlineDecorations(1); + assert.deepStrictEqual(result, [ + [new InlineDecoration(new Range(1, 6, 1, 7), 'fixed-width', InlineDecorationType.Regular)] + ]); }); test('injection with inlineClassNameAffectsLetterSpacing', () => { diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index 1c634619afef77..8699805a259bd1 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -8,7 +8,6 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/c import { PositionAffinity } from '../../../common/model.js'; import { ModelDecorationInjectedTextOptions } from '../../../common/model/textModel.js'; import { ModelLineProjectionData } from '../../../common/modelLineProjectionData.js'; -import { TokenArray, TokenInfo } from '../../../common/tokens/lineTokens.js'; suite('Editor ViewModel - LineBreakData', () => { @@ -21,17 +20,6 @@ suite('Editor ViewModel - LineBreakData', () => { assert.strictEqual(data.translateToInputOffset(1, 60), 150); }); - test('fixed width cannot be combined with tokens', () => { - assert.throws( - () => ModelDecorationInjectedTextOptions.from({ - content: 'text', - tokens: TokenArray.create([new TokenInfo(4, 0)]), - widthInEm: 1 - }), - /Injected text cannot define both tokens and widthInEm/ - ); - }); - test('fixed width must be finite and non-negative', () => { assert.throws( () => ModelDecorationInjectedTextOptions.from({ content: 'text', widthInEm: Number.NaN }), From 8a2c199c5571fcd0f1f5c9eefe0653d18ddd5c06 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 24 Aug 2026 17:22:45 +0200 Subject: [PATCH 19/41] wip --- .../lib/stylelint/vscode-known-variables.json | 108 +++++++++++------- src/vs/editor/common/model/textModel.ts | 3 - .../editor/common/modelLineProjectionData.ts | 2 +- .../colorPicker/browser/colorDetector.ts | 10 +- .../colorPicker/browser/colorPicker.css | 4 +- 5 files changed, 76 insertions(+), 51 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 6f8d2ebad0604f..0651f2b45cc7e5 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -1,8 +1,6 @@ { "colors": [ "--vscode-actionBar-toggledBackground", - "--vscode-activeSessionView-background", - "--vscode-activeSessionView-foreground", "--vscode-activityBar-activeBackground", "--vscode-activityBar-activeBorder", "--vscode-activityBar-activeFocusBorder", @@ -30,30 +28,12 @@ "--vscode-agentSessionSelectedBadge-border", "--vscode-agentSessionSelectedUnfocusedBadge-border", "--vscode-agentStatusIndicator-background", - "--vscode-agents-background", - "--vscode-agentsBadge-background", - "--vscode-agentsBadge-foreground", - "--vscode-agentsBottomPanel-border", - "--vscode-agentsCard-border", - "--vscode-agentsChatInput-background", - "--vscode-agentsChatInput-border", - "--vscode-agentsChatInput-focusBorder", - "--vscode-agentsChatInput-foreground", - "--vscode-agentsChatInput-placeholderForeground", - "--vscode-agentsGradient-tintColor", - "--vscode-agentsNewSessionButton-background", - "--vscode-agentsNewSessionButton-border", - "--vscode-agentsNewSessionButton-foreground", - "--vscode-agentsNewSessionButton-hoverBackground", - "--vscode-agentsPanel-background", - "--vscode-agentsPanel-border", - "--vscode-agentsPanel-foreground", - "--vscode-agentsUnreadBadge-background", - "--vscode-agentsUnreadBadge-foreground", - "--vscode-agentsUpdateButton-downloadedBackground", - "--vscode-agentsUpdateButton-downloadingBackground", "--vscode-agentsVoice-speakingBackground", "--vscode-agentsVoice-speakingForeground", + "--vscode-activeSessionView-background", + "--vscode-activeSessionView-foreground", + "--vscode-inactiveSessionView-background", + "--vscode-inactiveSessionView-foreground", "--vscode-badge-background", "--vscode-badge-foreground", "--vscode-banner-background", @@ -105,9 +85,12 @@ "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", "--vscode-chat-thinkingShimmer", - "--vscode-chat-voiceGlowBaseColor", - "--vscode-chat-voiceListeningGlow", - "--vscode-chat-voiceSpeakingGlow", + "--vscode-agentsChatInput-background", + "--vscode-agentsChatInput-border", + "--vscode-agentsChatInput-focusBorder", + "--vscode-agentsChatInput-foreground", + "--vscode-agentsChatInput-placeholderForeground", + "--vscode-chatManagement-sashBorder", "--vscode-checkbox-background", "--vscode-checkbox-border", "--vscode-checkbox-disabled-background", @@ -416,13 +399,22 @@ "--vscode-extensionIcon-verifiedForeground", "--vscode-focusBorder", "--vscode-foreground", + "--vscode-gauge-background", + "--vscode-gauge-border", + "--vscode-gauge-errorBackground", + "--vscode-gauge-errorForeground", + "--vscode-gauge-foreground", + "--vscode-gauge-warningBackground", + "--vscode-gauge-warningForeground", + "--vscode-gitDecoration-addedResourceForeground", + "--vscode-gitDecoration-deletedResourceForeground", + "--vscode-gitDecoration-modifiedResourceForeground", "--vscode-icon-foreground", - "--vscode-inactiveSessionView-background", - "--vscode-inactiveSessionView-foreground", "--vscode-inlineChat-background", "--vscode-inlineChat-border", "--vscode-inlineChat-foreground", "--vscode-inlineChat-shadow", + "--vscode-inlineChat-regionHighlight", "--vscode-inlineChatDiff-inserted", "--vscode-inlineChatDiff-removed", "--vscode-inlineChatInput-background", @@ -555,10 +547,8 @@ "--vscode-minimapSlider-hoverBackground", "--vscode-modernActivityBar-activeBackground", "--vscode-modernActivityBar-activeForeground", - "--vscode-modernActivityBar-background", "--vscode-modernActivityBar-hoverBackground", "--vscode-modernActivityBar-hoverForeground", - "--vscode-modernActivityBar-inactiveBackground", "--vscode-modernEditorTab-activeActionBackground", "--vscode-modernEditorTab-activeBackground", "--vscode-modernEditorTab-activeForeground", @@ -696,6 +686,29 @@ "--vscode-searchEditor-findMatchBorder", "--vscode-searchEditor-textInputBorder", "--vscode-selection-background", + "--vscode-surface-background", + "--vscode-surface-border", + "--vscode-surface-foreground", + "--vscode-agentsPanel-background", + "--vscode-agentsPanel-border", + "--vscode-agentsPanel-foreground", + "--vscode-agentsCard-border", + "--vscode-agentsBottomPanel-border", + "--vscode-agentsBadge-background", + "--vscode-agentsBadge-foreground", + "--vscode-agentsGradient-tintColor", + "--vscode-agentsNewSessionButton-background", + "--vscode-agentsNewSessionButton-border", + "--vscode-agentsNewSessionButton-foreground", + "--vscode-agentsNewSessionButton-hoverBackground", + "--vscode-agents-background", + "--vscode-agentsUnreadBadge-background", + "--vscode-agentsUnreadBadge-foreground", + "--vscode-agentsUpdateButton-downloadedBackground", + "--vscode-agentsUpdateButton-downloadingBackground", + "--vscode-agentsMobileDiff-addedForeground", + "--vscode-agentsMobileDiff-modifiedForeground", + "--vscode-agentsMobileDiff-deletedForeground", "--vscode-settings-checkboxBackground", "--vscode-settings-checkboxBorder", "--vscode-settings-checkboxForeground", @@ -769,10 +782,6 @@ "--vscode-statusBarItem-warningForeground", "--vscode-statusBarItem-warningHoverBackground", "--vscode-statusBarItem-warningHoverForeground", - "--vscode-strongForeground", - "--vscode-surface-background", - "--vscode-surface-border", - "--vscode-surface-foreground", "--vscode-symbolIcon-arrayForeground", "--vscode-symbolIcon-booleanForeground", "--vscode-symbolIcon-classForeground", @@ -806,6 +815,7 @@ "--vscode-symbolIcon-typeParameterForeground", "--vscode-symbolIcon-unitForeground", "--vscode-symbolIcon-variableForeground", + "--vscode-strongForeground", "--vscode-tab-activeBackground", "--vscode-tab-activeBorder", "--vscode-tab-activeBorderTop", @@ -900,7 +910,6 @@ "--vscode-testing-coveredBackground", "--vscode-testing-coveredBorder", "--vscode-testing-coveredGutterBackground", - "--vscode-testing-coveredMinimapBackground", "--vscode-testing-iconErrored", "--vscode-testing-iconErrored-retired", "--vscode-testing-iconFailed", @@ -928,7 +937,6 @@ "--vscode-testing-uncoveredBorder", "--vscode-testing-uncoveredBranchBackground", "--vscode-testing-uncoveredGutterBackground", - "--vscode-testing-uncoveredMinimapBackground", "--vscode-textBlockQuote-background", "--vscode-textBlockQuote-border", "--vscode-textCodeBlock-background", @@ -961,7 +969,10 @@ "--vscode-widget-border", "--vscode-widget-shadow", "--vscode-window-activeBorder", - "--vscode-window-inactiveBorder" + "--vscode-window-inactiveBorder", + "--vscode-chat-voiceGlowBaseColor", + "--vscode-chat-voiceListeningGlow", + "--vscode-chat-voiceSpeakingGlow" ], "others": [ "--action-widget-close-start-opacity", @@ -1083,6 +1094,8 @@ "--modern-ui-tab-hover-background", "--scroll-shadow-surface", "--vscode-chat-list-background", + "--vscode-colorPicker-colorDecoratorMargin", + "--vscode-colorPicker-colorDecoratorWidth", "--vscode-editorCodeLens-fontFamily", "--vscode-editorCodeLens-fontFamilyDefault", "--vscode-editorCodeLens-fontFeatureSettings", @@ -1204,6 +1217,12 @@ "--vg-inner-fade" ], "sizes": [ + "--segmented-icon-toggle-cell-radius", + "--segmented-icon-toggle-cell-width", + "--segmented-icon-toggle-height", + "--segmented-icon-toggle-radius", + "--segmented-icon-toggle-single-width", + "--segmented-icon-toggle-width", "--vscode-agents-fontSize-body1", "--vscode-agents-fontSize-body2", "--vscode-agents-fontSize-heading1", @@ -1236,20 +1255,21 @@ "--vscode-fontSize-label3", "--vscode-fontWeight-regular", "--vscode-fontWeight-semiBold", + "--vscode-keyboard-height", + "--vscode-spacing-sizeNone", + "--vscode-spacing-size20", + "--vscode-spacing-size40", + "--vscode-spacing-size60", + "--vscode-spacing-size80", "--vscode-spacing-size100", "--vscode-spacing-size120", "--vscode-spacing-size160", - "--vscode-spacing-size20", "--vscode-spacing-size200", "--vscode-spacing-size240", "--vscode-spacing-size280", "--vscode-spacing-size320", "--vscode-spacing-size360", - "--vscode-spacing-size40", "--vscode-spacing-size400", - "--vscode-spacing-size60", - "--vscode-spacing-size80", - "--vscode-spacing-sizeNone", "--vscode-strokeThickness" ] -} \ No newline at end of file +} diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 8d934c61b32947..bc81bcf1156755 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2468,9 +2468,6 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt readonly cursorStops: model.InjectedTextCursorStops | null; private constructor(options: model.InjectedTextOptions) { - if (options.widthInEm !== undefined && (!Number.isFinite(options.widthInEm) || options.widthInEm < 0)) { - throw new BugIndicatingError('Injected text widthInEm must be a finite non-negative number'); - } this.content = options.content || ''; this.tokens = options.tokens ?? null; this.inlineClassName = options.inlineClassName || null; diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index 72e7bdf6239c8e..93103739b89464 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -346,7 +346,7 @@ export interface ILineBreaksComputer { } /** - * The fixed-widthinjected text range after all preceding injections have been applied. + * The fixed-width injected text range after all preceding injections have been applied. */ export interface FixedWidthInjectedTextRange { readonly startOffset: number; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index 9c895984171e60..dae99fd98aabea 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -7,7 +7,7 @@ import { CancelablePromise, createCancelablePromise, TimeoutTimer } from '../../ import { RGBA } from '../../../../base/common/color.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { noBreakWhitespace } from '../../../../base/common/strings.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; @@ -50,6 +50,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { private readonly _decoratorLimitReporter = this._register(new DecoratorLimitReporter()); private static readonly colorDecoratorWidthInEm = 1.2; + private static readonly colorDecoratorMarginInEm = 0.2; constructor( private readonly _editor: ICodeEditor, @@ -60,6 +61,13 @@ export class ColorDetector extends Disposable implements IEditorContribution { super(); this._colorDecoratorIds = this._editor.createDecorationsCollection(); this._ruleFactory = this._register(new DynamicCssRules(this._editor)); + const editorDomNode = this._editor.getContainerDomNode(); + editorDomNode.style.setProperty('--vscode-colorPicker-colorDecoratorWidth', `${ColorDetector.colorDecoratorWidthInEm}em`); + editorDomNode.style.setProperty('--vscode-colorPicker-colorDecoratorMargin', `${ColorDetector.colorDecoratorMarginInEm}em`); + this._register(toDisposable(() => { + editorDomNode.style.removeProperty('--vscode-colorPicker-colorDecoratorWidth'); + editorDomNode.style.removeProperty('--vscode-colorPicker-colorDecoratorMargin'); + })); this._debounceInformation = languageFeatureDebounceService.for(_languageFeaturesService.colorProvider, 'Document Colors', { min: ColorDetector.RECOMPUTE_TIME }); this._register(_editor.onDidChangeModel(() => { this._isColorDecoratorsEnabled = this.isEnabled(); diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css index f484517caba972..4b8260ea866a30 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css +++ b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css @@ -15,8 +15,8 @@ .hc-light .colorpicker-color-decoration { border: solid 0.1em #000; box-sizing: border-box; - margin: 0.1em 0.2em 0 0.2em; - width: 0.8em; + margin: 0.1em var(--vscode-colorPicker-colorDecoratorMargin) 0; + width: calc(var(--vscode-colorPicker-colorDecoratorWidth) - var(--vscode-colorPicker-colorDecoratorMargin) - var(--vscode-colorPicker-colorDecoratorMargin)); height: 0.8em; line-height: 0.8em; display: inline-block; From 023ca8affac7c554a03390d7c4b062e2e62e0681 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 24 Aug 2026 20:47:46 +0200 Subject: [PATCH 20/41] polishing --- .../browser/view/domLineBreaksComputer.ts | 61 +++++-------- .../viewModel/monospaceLineBreaksComputer.ts | 86 +++++++++++-------- .../view/domLineBreaksComputer.test.ts | 8 ++ .../monospaceLineBreaksComputer.test.ts | 23 ++++- 4 files changed, 101 insertions(+), 77 deletions(-) diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index a47f5dae2e5b4d..869bdd7370e259 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -78,7 +78,7 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont const wrappedTextIndentLengths: number[] = []; const renderLineContents: string[] = []; const allCharOffsets: number[][] = []; - const allSpanStartOffsets: (number[] | null)[] = []; + const allSpanStartOffsets: number[][] = []; const allVisibleColumns: number[][] = []; for (let i = 0; i < lineNumbers.length; i++) { const lineNumber = lineNumbers[i]; @@ -101,17 +101,19 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont for (let i = 0; i < firstNonWhitespaceIndex; i++) { const fixedWidthRange = fixedWidthRanges[0]; - const isFixedWidthStart = fixedWidthRange?.startOffset === i; + const isFixedWidthStart = fixedWidthRange && fixedWidthRange.startOffset === i; if (isFixedWidthStart) { firstNonWhitespaceIndex = i; break; - } else if (lineContent.charCodeAt(i) === CharCode.Tab) { - wrappedTextIndentLength += tabSize - (wrappedTextIndentLength % tabSize); } else { - wrappedTextIndentLength++; + const charWidth = ( + lineContent.charCodeAt(i) === CharCode.Tab + ? (tabSize - (wrappedTextIndentLength % tabSize)) + : 1 + ); + wrappedTextIndentLength += charWidth; } } - const indentWidth = Math.ceil(fontInfo.spaceWidth * wrappedTextIndentLength); // Force sticking to beginning of line if no character would fit except for the indentation @@ -131,18 +133,8 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont startOffset: Math.max(0, range.startOffset - firstNonWhitespaceIndex), endOffset: range.endOffset - firstNonWhitespaceIndex, widthInEm: range.widthInEm - })).filter(range => range.endOffset > 0); - const renderLineFixedWidthRanges = shiftedFixedWidthRanges.length > 0 ? shiftedFixedWidthRanges : null; - const tmp = renderLine( - renderLineContent, - wrappedTextIndentLength, - tabSize, - width, - sb, - additionalIndentLength, - renderLineFixedWidthRanges, - fontInfo.fontSize / fontInfo.typicalHalfwidthCharacterWidth - ); + })); + const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength, shiftedFixedWidthRanges); firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex; wrappedTextIndentLengths[i] = wrappedTextIndentLength; renderLineContents[i] = renderLineContent; @@ -218,7 +210,7 @@ const enum Constants { SPAN_MODULO_LIMIT = 16384 } -function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly FixedWidthInjectedTextRange[] | null, columnsPerEm: number): [number[], number[], number[] | null] { +function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly FixedWidthInjectedTextRange[]): [number[], number[], number[]] { if (wrappingIndentLength !== 0) { const hangingOffset = String(wrappingIndentLength); @@ -241,15 +233,15 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: let charOffset = 0; let fixedWidthRangeIndex = 0; const charOffsets: number[] = []; - const spanStartOffsets: number[] | null = fixedWidthRanges ? [0] : null; + const spanStartOffsets: number[] = [0]; const visibleColumns: number[] = []; let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : CharCode.Null); let spanOpen = true; sb.appendString(''); for (let charIndex = 0; charIndex < len; charIndex++) { - const fixedWidthRange = fixedWidthRanges?.[fixedWidthRangeIndex]; - const startsFixedWidth = fixedWidthRange?.startOffset === charIndex; + const fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex]; + const startsFixedWidth = fixedWidthRange && fixedWidthRange.startOffset === charIndex; if (startsFixedWidth) { if (spanOpen) { sb.appendString(''); @@ -257,11 +249,15 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: sb.appendString(''); - spanStartOffsets!.push(charOffset); + spanStartOffsets.push(charOffset); + spanOpen = true; + } else if (!spanOpen) { + sb.appendString(''); + spanStartOffsets.push(charOffset); spanOpen = true; } else if ((!fixedWidthRange || charIndex < fixedWidthRange.startOffset) && charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) { sb.appendString(''); - spanStartOffsets?.push(charOffset); + spanStartOffsets.push(charOffset); } charOffsets[charIndex] = charOffset; visibleColumns[charIndex] = visibleColumn; @@ -325,22 +321,11 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: } charOffset += producedCharacters; - if (startsFixedWidth) { - charWidth = fixedWidthRange.widthInEm * columnsPerEm; - } else if (fixedWidthRange && charIndex > fixedWidthRange.startOffset && charIndex < fixedWidthRange.endOffset) { - charWidth = 0; - } visibleColumn += charWidth; if (fixedWidthRange && charIndex + 1 === fixedWidthRange.endOffset) { sb.appendString(''); spanOpen = false; - const nextFixedWidthRange = fixedWidthRanges?.[fixedWidthRangeIndex + 1]; - if (fixedWidthRange.endOffset < len && nextFixedWidthRange?.startOffset !== fixedWidthRange.endOffset) { - sb.appendString(''); - spanStartOffsets!.push(charOffset); - spanOpen = true; - } fixedWidthRangeIndex++; } } @@ -356,7 +341,7 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: return [charOffsets, visibleColumns, spanStartOffsets]; } -function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[], spanStartOffsets: number[] | null = null): number[] | null { +function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[], spanStartOffsets: number[]): number[] | null { if (lineContent.length <= 1) { return null; } @@ -378,7 +363,7 @@ function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: return breakOffsets; } -function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: number[], spanStartOffsets: number[] | null, low: number, lowRects: DOMRectList | null, high: number, highRects: DOMRectList | null, result: number[]): void { +function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: number[], spanStartOffsets: number[], low: number, lowRects: DOMRectList | null, high: number, highRects: DOMRectList | null, result: number[]): void { if (low === high) { return; } @@ -404,7 +389,7 @@ function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: num discoverBreaks(range, spans, charOffsets, spanStartOffsets, mid, midRects, high, highRects, result); } -function readClientRect(range: Range, spans: HTMLSpanElement[], startOffset: number, endOffset: number, spanStartOffsets: number[] | null): DOMRectList { +function readClientRect(range: Range, spans: HTMLSpanElement[], startOffset: number, endOffset: number, spanStartOffsets: number[]): DOMRectList { if (!spanStartOffsets) { range.setStart(spans[(startOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, startOffset % Constants.SPAN_MODULO_LIMIT); range.setEnd(spans[(endOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, endOffset % Constants.SPAN_MODULO_LIMIT); diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 11938145063019..17d0ffad28f83f 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -10,7 +10,7 @@ import { CharacterClassifier } from '../core/characterClassifier.js'; import { FontInfo } from '../config/fontInfo.js'; import { LineInjectedText } from '../textModelEvents.js'; import { InjectedTextOptions } from '../model.js'; -import { FixedWidthInjectedTextRange, getFixedWidthInjectedTextRanges, ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; +import { getFixedWidthInjectedTextRanges, ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory { public static create(options: IComputedEditorOptions): MonospaceLineBreaksComputerFactory { @@ -390,7 +390,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st } const isKeepAll = (wordBreak === 'keepAll'); - const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent, injectedTextWidthsInEm); + const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent); const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; const breakingOffsets: number[] = []; @@ -398,28 +398,33 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st let breakingOffsetsCount: number = 0; let breakOffset = 0; let breakOffsetVisibleColumn = 0; + let breakOffsetWrappingColumn = 0; let breakingColumn = firstLineBreakColumn; let fixedWidthRangeIndex = 0; - const firstFixedWidthRange = injectedTextWidthsInEm[fixedWidthRangeIndex]; - const startsWithFixedWidth = firstFixedWidthRange?.startOffset === 0; + const firstFixedWidthRange = injectedTextWidthsInEm[0]; + const startsWithFixedWidth = firstFixedWidthRange && firstFixedWidthRange.startOffset === 0; let prevCharCode: number; let visibleColumn: number; + let wrappingColumn: number; let startOffset: number; if (startsWithFixedWidth) { // The line starts with injected text of a specific width prevCharCode = lineText.charCodeAt(firstFixedWidthRange.endOffset - 1); - visibleColumn = firstFixedWidthRange.widthInEm * columnsPerEm; + visibleColumn = computeRangeWidth(lineText, 0, firstFixedWidthRange.endOffset, 0, tabSize, columnsForFullWidthChar); + wrappingColumn = firstFixedWidthRange.widthInEm * columnsPerEm; startOffset = firstFixedWidthRange.endOffset; fixedWidthRangeIndex++; } else { prevCharCode = lineText.charCodeAt(0); visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar); + wrappingColumn = visibleColumn; startOffset = 1; if (strings.isHighSurrogate(prevCharCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken visibleColumn += 1; + wrappingColumn += 1; prevCharCode = lineText.charCodeAt(1); startOffset++; } @@ -427,65 +432,68 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st let prevCharCodeClass = classifier.get(prevCharCode); for (let i = startOffset; i < len; i++) { - const charStartOffset = i; const fixedWidthRange = injectedTextWidthsInEm[fixedWidthRangeIndex]; - const charCode = lineText.charCodeAt(i); + const charStartOffset = i; + let charCode = lineText.charCodeAt(i); let charCodeClass: CharacterClass; - let charWidth: number; - let trailingCharCode = charCode; - let trailingCharCodeClass: CharacterClass; + let visibleCharWidth: number; + let wrappingCharWidth: number; let wrapEscapedLineFeed = false; - if (fixedWidthRange?.startOffset === i) { - charCodeClass = classifier.get(charCode); - charWidth = fixedWidthRange.widthInEm * columnsPerEm; + if (fixedWidthRange && fixedWidthRange.startOffset === i) { i = fixedWidthRange.endOffset - 1; - trailingCharCode = lineText.charCodeAt(i); - trailingCharCodeClass = classifier.get(trailingCharCode); + charCode = lineText.charCodeAt(i); + charCodeClass = classifier.get(charCode); + visibleCharWidth = computeRangeWidth(lineText, charStartOffset, fixedWidthRange.endOffset, visibleColumn, tabSize, columnsForFullWidthChar); + wrappingCharWidth = fixedWidthRange.widthInEm * columnsPerEm; fixedWidthRangeIndex++; } else if (strings.isHighSurrogate(charCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i++; charCodeClass = CharacterClass.NONE; - trailingCharCodeClass = charCodeClass; - charWidth = 2; + visibleCharWidth = 2; + wrappingCharWidth = visibleCharWidth; } else { charCodeClass = classifier.get(charCode); - trailingCharCodeClass = charCodeClass; - charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); + visibleCharWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); + wrappingCharWidth = visibleCharWidth; } // literal \n shall trigger a softwrap if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, i)) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; + breakOffsetWrappingColumn = wrappingColumn; wrapEscapedLineFeed = true; } else if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; + breakOffsetWrappingColumn = wrappingColumn; } - visibleColumn += charWidth; + visibleColumn += visibleCharWidth; + wrappingColumn += wrappingCharWidth; // check if adding character at `i` will go over the breaking column - if (visibleColumn > breakingColumn || wrapEscapedLineFeed) { + if (wrappingColumn > breakingColumn || wrapEscapedLineFeed) { // We need to break at least before character at `i`: - if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) { + if (breakOffset === 0 || wrappingColumn - breakOffsetWrappingColumn > wrappedLineBreakColumn) { // Cannot break at `breakOffset`, must break at `i` breakOffset = charStartOffset; - breakOffsetVisibleColumn = visibleColumn - charWidth; + breakOffsetVisibleColumn = visibleColumn - visibleCharWidth; + breakOffsetWrappingColumn = wrappingColumn - wrappingCharWidth; } breakingOffsets[breakingOffsetsCount] = breakOffset; breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; breakingOffsetsCount++; - breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; + breakingColumn = breakOffsetWrappingColumn + wrappedLineBreakColumn; breakOffset = 0; } - prevCharCode = trailingCharCode; - prevCharCodeClass = trailingCharCodeClass; + prevCharCode = charCode; + prevCharCodeClass = charCodeClass; } if (breakingOffsetsCount === 0 && (!injectedTexts || injectedTexts.length === 0)) { @@ -499,6 +507,20 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st return new ModelLineProjectionData(injectionOffsets, injectionOptions, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); } +function computeRangeWidth(lineText: string, startOffset: number, endOffset: number, startVisibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { + let width = 0; + for (let i = startOffset; i < endOffset; i++) { + const charCode = lineText.charCodeAt(i); + if (strings.isHighSurrogate(charCode)) { + width += 2; + i++; + } else { + width += computeCharWidth(charCode, startVisibleColumn + width, tabSize, columnsForFullWidthChar); + } + } + return width; +} + function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { if (charCode === CharCode.Tab) { return (tabSize - (visibleColumn % tabSize)); @@ -552,7 +574,7 @@ function canBreak(prevCharCode: number, prevCharCodeClass: CharacterClass, charC ); } -function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, fixedWidthRanges: readonly FixedWidthInjectedTextRange[] = []): number { +function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): number { let wrappedTextIndentLength = 0; if (wrappingIndent !== WrappingIndent.None) { const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); @@ -560,14 +582,8 @@ function computeWrappedTextIndentLength(lineText: string, tabSize: number, first // Track existing indent for (let i = 0; i < firstNonWhitespaceIndex; i++) { - const fixedWidthRange = fixedWidthRanges[0]; - const isFixedWidthStart = fixedWidthRange?.startOffset === i; - if (isFixedWidthStart) { - break; - } else { - const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1); - wrappedTextIndentLength += charWidth; - } + const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1); + wrappedTextIndentLength += charWidth; } // Increase indent of continuation lines, if desired diff --git a/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts b/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts index e64261dd62269b..2ca00f5440ab04 100644 --- a/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts +++ b/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts @@ -67,6 +67,14 @@ suite('DOMLineBreaksComputer', () => { assert.ok(result && result.breakOffsets.length > 1); }); + test('keeps rendered visible columns integral with fractional fixed widths', () => { + const result = computeLineBreaks('alpha beta gamma', [ + new LineInjectedText(0, 1, 7, { content: '\xa0', widthInEm: 0.75 }, 0) + ]); + + assert.ok(result?.breakOffsetsVisibleColumn.every(Number.isInteger)); + }); + test('tracks adjacent fixed-width DOM spans', () => { const result = computeLineBreaks('alpha beta gamma', [ new LineInjectedText(0, 1, 7, { content: 'x', widthInEm: 1 }, 0), diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 173fe4dec55fe9..dbd7f93393dd7c 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -146,7 +146,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [4, 7], - breakOffsetsVisibleColumn: [4, 8] + breakOffsetsVisibleColumn: [4, 7] }); }); @@ -161,7 +161,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [8, 11], - breakOffsetsVisibleColumn: [5, 8] + breakOffsetsVisibleColumn: [8, 11] }); }); @@ -177,7 +177,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [4, 8], - breakOffsetsVisibleColumn: [4, 9] + breakOffsetsVisibleColumn: [4, 8] }); }); @@ -192,7 +192,22 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [3, 4, 7], - breakOffsetsVisibleColumn: [3, 9, 12] + breakOffsetsVisibleColumn: [3, 4, 7] + }); + }); + + test('keeps rendered visible columns integral with fractional fixed widths', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 4, 2, WrappingIndent.None, 'normal', false, 'ab\tcd', null, [ + new LineInjectedText(0, 1, 3, { content: '\xa0', widthInEm: 0.75 }, 0) + ]); + + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [3, 6], + breakOffsetsVisibleColumn: [3, 6] }); }); From cf50a3e04d009b67a8d04f663515678832c4b225 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 24 Aug 2026 21:37:15 +0200 Subject: [PATCH 21/41] polishing --- .../browser/view/domLineBreaksComputer.ts | 6 +- .../editor/common/modelLineProjectionData.ts | 28 ------ src/vs/editor/common/textModelEvents.ts | 26 ++++++ .../viewModel/monospaceLineBreaksComputer.ts | 4 +- .../colorPicker/browser/colorDetector.ts | 9 +- .../colorPicker/browser/colorPicker.css | 2 +- .../browser/inlineProgressWidget.css | 1 + .../view/domLineBreaksComputer.test.ts | 93 ------------------- .../common/viewModel/lineBreakData.test.ts | 11 --- .../monospaceLineBreaksComputer.test.ts | 2 +- .../browser/media/debug.contribution.css | 1 + .../debug/test/browser/breakpoints.test.ts | 9 +- .../editor/injectedTextDecorations.fixture.ts | 40 +------- 13 files changed, 44 insertions(+), 188 deletions(-) delete mode 100644 src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 869bdd7370e259..e741c3366a244d 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -11,8 +11,8 @@ import { applyFontInfo } from '../config/domFontInfo.js'; import { WrappingIndent } from '../../common/config/editorOptions.js'; import { StringBuilder } from '../../common/core/stringBuilder.js'; import { InjectedTextOptions } from '../../common/model.js'; -import { FixedWidthInjectedTextRange, getFixedWidthInjectedTextRanges, ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js'; -import { LineInjectedText } from '../../common/textModelEvents.js'; +import { ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js'; +import { FixedWidthInjectedTextRange, LineInjectedText } from '../../common/textModelEvents.js'; import { FontInfo } from '../../common/config/fontInfo.js'; const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value }); @@ -84,7 +84,7 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont const lineNumber = lineNumbers[i]; const injectedTexts = context.getLineInjectedText(lineNumber); const lineContent = LineInjectedText.applyInjectedText(context.getLineContent(lineNumber), injectedTexts); - const fixedWidthRanges = getFixedWidthInjectedTextRanges(injectedTexts); + const fixedWidthRanges = LineInjectedText.getFixedWidthInjectedTextRanges(injectedTexts); let firstNonWhitespaceIndex = 0; let wrappedTextIndentLength = 0; diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index 93103739b89464..948214355ef3f8 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -344,31 +344,3 @@ export interface ILineBreaksComputer { addRequest(lineNumber: number, previousLineBreakData: ModelLineProjectionData | null): void; finalize(): (ModelLineProjectionData | null)[]; } - -/** - * The fixed-width injected text range after all preceding injections have been applied. - */ -export interface FixedWidthInjectedTextRange { - readonly startOffset: number; - readonly endOffset: number; - readonly widthInEm: number; -} - -/** - * Projects fixed-width injected text into offsets in the line with all injections applied. - */ -export function getFixedWidthInjectedTextRanges(injectedTexts: readonly LineInjectedText[] | null): FixedWidthInjectedTextRange[] { - const result: FixedWidthInjectedTextRange[] = []; - let injectedTextLength = 0; - for (const injectedText of injectedTexts ?? []) { - const length = injectedText.options.content.length; - const startOffset = injectedText.column - 1 + injectedTextLength; - const endOffset = startOffset + length; - const widthInEm = injectedText.options.widthInEm; - if (widthInEm !== undefined) { - result.push({ startOffset, endOffset, widthInEm }); - } - injectedTextLength += length; - } - return result; -} diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 1f504db5852e61..95b70e9876c73f 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -234,6 +234,16 @@ export class ModelRawFlush { public readonly changeType = RawContentChangedType.Flush; } +/** + * Represents a fixed-width injected text range within a line. + * @internal + */ +export interface FixedWidthInjectedTextRange { + readonly startOffset: number; + readonly endOffset: number; + readonly widthInEm: number; +} + /** * Represents text injected on a line * @internal @@ -288,6 +298,22 @@ export class LineInjectedText { return result; } + public static getFixedWidthInjectedTextRanges(injectedTexts: readonly LineInjectedText[] | null): FixedWidthInjectedTextRange[] { + const result: FixedWidthInjectedTextRange[] = []; + let injectedTextLength = 0; + for (const injectedText of injectedTexts ?? []) { + const length = injectedText.options.content.length; + const startOffset = injectedText.column - 1 + injectedTextLength; + const endOffset = startOffset + length; + const widthInEm = injectedText.options.widthInEm; + if (widthInEm !== undefined) { + result.push({ startOffset, endOffset, widthInEm }); + } + injectedTextLength += length; + } + return result; + } + constructor( public readonly ownerId: number, public readonly lineNumber: number, diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 17d0ffad28f83f..232b7b12f05bbc 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -10,7 +10,7 @@ import { CharacterClassifier } from '../core/characterClassifier.js'; import { FontInfo } from '../config/fontInfo.js'; import { LineInjectedText } from '../textModelEvents.js'; import { InjectedTextOptions } from '../model.js'; -import { getFixedWidthInjectedTextRanges, ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; +import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory { public static create(options: IComputedEditorOptions): MonospaceLineBreaksComputerFactory { @@ -358,7 +358,7 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, columnsPerEm: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ModelLineProjectionData | null { const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts); - const injectedTextWidthsInEm = getFixedWidthInjectedTextRanges(injectedTexts); + const injectedTextWidthsInEm = LineInjectedText.getFixedWidthInjectedTextRanges(injectedTexts); let injectionOptions: InjectedTextOptions[] | null; let injectionOffsets: number[] | null; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index dae99fd98aabea..a605d3c978965b 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -49,7 +49,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { private readonly _decoratorLimitReporter = this._register(new DecoratorLimitReporter()); - private static readonly colorDecoratorWidthInEm = 1.2; + private static readonly colorDecoratorInnerWidthInEm = 0.8; private static readonly colorDecoratorMarginInEm = 0.2; constructor( @@ -62,7 +62,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { this._colorDecoratorIds = this._editor.createDecorationsCollection(); this._ruleFactory = this._register(new DynamicCssRules(this._editor)); const editorDomNode = this._editor.getContainerDomNode(); - editorDomNode.style.setProperty('--vscode-colorPicker-colorDecoratorWidth', `${ColorDetector.colorDecoratorWidthInEm}em`); + editorDomNode.style.setProperty('--vscode-colorPicker-colorDecoratorWidth', `${ColorDetector.colorDecoratorInnerWidthInEm}em`); editorDomNode.style.setProperty('--vscode-colorPicker-colorDecoratorMargin', `${ColorDetector.colorDecoratorMarginInEm}em`); this._register(toDisposable(() => { editorDomNode.style.removeProperty('--vscode-colorPicker-colorDecoratorWidth'); @@ -214,6 +214,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { this._colorDecorationClassRefs.clear(); const decorations: IModelDeltaDecoration[] = []; + const widthInEm = ColorDetector.colorDecoratorInnerWidthInEm + 2 * ColorDetector.colorDecoratorMarginInEm; const limit = this._editor.getOption(EditorOption.colorDecoratorsLimit); @@ -241,8 +242,8 @@ export class ColorDetector extends Disposable implements IEditorContribution { content: noBreakWhitespace, inlineClassName: `${ref.className} colorpicker-color-decoration`, inlineClassNameAffectsLetterSpacing: true, - widthInEm: ColorDetector.colorDecoratorWidthInEm, - attachedData: ColorDecorationInjectedTextMarker + attachedData: ColorDecorationInjectedTextMarker, + widthInEm, } } }); diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css index 4b8260ea866a30..110aa63a08e69c 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css +++ b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css @@ -16,7 +16,7 @@ border: solid 0.1em #000; box-sizing: border-box; margin: 0.1em var(--vscode-colorPicker-colorDecoratorMargin) 0; - width: calc(var(--vscode-colorPicker-colorDecoratorWidth) - var(--vscode-colorPicker-colorDecoratorMargin) - var(--vscode-colorPicker-colorDecoratorMargin)); + width: var(--vscode-colorPicker-colorDecoratorWidth); height: 0.8em; line-height: 0.8em; display: inline-block; diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgressWidget.css b/src/vs/editor/contrib/inlineProgress/browser/inlineProgressWidget.css index ec14cfcdc2c002..cb376acc002e97 100644 --- a/src/vs/editor/contrib/inlineProgress/browser/inlineProgressWidget.css +++ b/src/vs/editor/contrib/inlineProgress/browser/inlineProgressWidget.css @@ -5,6 +5,7 @@ .inline-editor-progress-decoration { display: inline-block; + width: 1em; height: 1em; } diff --git a/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts b/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts deleted file mode 100644 index 2ca00f5440ab04..00000000000000 --- a/src/vs/editor/test/browser/view/domLineBreaksComputer.test.ts +++ /dev/null @@ -1,93 +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 assert from 'assert'; -import { mainWindow } from '../../../../base/browser/window.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { DOMLineBreaksComputerFactory } from '../../../browser/view/domLineBreaksComputer.js'; -import { WrappingIndent } from '../../../common/config/editorOptions.js'; -import { FontInfo } from '../../../common/config/fontInfo.js'; -import { ILineBreaksComputerContext, ModelLineProjectionData } from '../../../common/modelLineProjectionData.js'; -import { LineInjectedText } from '../../../common/textModelEvents.js'; - -suite('DOMLineBreaksComputer', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - const fontInfo = new FontInfo({ - pixelRatio: 1, - fontFamily: 'Arial', - fontWeight: 'normal', - fontSize: 14, - fontFeatureSettings: '', - fontVariationSettings: '', - lineHeight: 19, - letterSpacing: 0, - isMonospace: false, - typicalHalfwidthCharacterWidth: 7, - typicalFullwidthCharacterWidth: 14, - canUseHalfwidthRightwardsArrow: true, - spaceWidth: 7, - middotWidth: 7, - wsmiddotWidth: 7, - maxDigitWidth: 7 - }, false); - - function computeLineBreaks(text: string, injectedText: LineInjectedText[] | null, wrappingColumn = 4): ModelLineProjectionData | null { - const context: ILineBreaksComputerContext = { - getLineContent: () => text, - getLineInjectedText: () => injectedText - }; - const computer = DOMLineBreaksComputerFactory.create(mainWindow).createLineBreaksComputer( - context, - fontInfo, - 4, - wrappingColumn, - WrappingIndent.None, - 'normal', - false - ); - computer.addRequest(1, null); - return computer.finalize()[0]; - } - - test('tracks DOM spans without fixed-width injected text', () => { - const result = computeLineBreaks('alpha beta gamma', null); - - assert.ok(result && result.breakOffsets.length > 1); - }); - - test('tracks DOM spans with fixed-width injected text', () => { - const result = computeLineBreaks('alpha beta gamma', [ - new LineInjectedText(0, 1, 7, { content: '\xa0', widthInEm: 3 }, 0) - ]); - - assert.ok(result && result.breakOffsets.length > 1); - }); - - test('keeps rendered visible columns integral with fractional fixed widths', () => { - const result = computeLineBreaks('alpha beta gamma', [ - new LineInjectedText(0, 1, 7, { content: '\xa0', widthInEm: 0.75 }, 0) - ]); - - assert.ok(result?.breakOffsetsVisibleColumn.every(Number.isInteger)); - }); - - test('tracks adjacent fixed-width DOM spans', () => { - const result = computeLineBreaks('alpha beta gamma', [ - new LineInjectedText(0, 1, 7, { content: 'x', widthInEm: 1 }, 0), - new LineInjectedText(0, 1, 7, { content: 'y', widthInEm: 1 }, 1) - ]); - - assert.ok(result && result.breakOffsets.length > 1); - }); - - test('splits long DOM spans without fixed-width injected text', () => { - const text = 'a'.repeat(16385); - const result = computeLineBreaks(text, null, text.length + 1); - - assert.strictEqual(result?.breakOffsets.at(-1), text.length); - }); -}); diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index 8699805a259bd1..b621632fe1d39a 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -20,17 +20,6 @@ suite('Editor ViewModel - LineBreakData', () => { assert.strictEqual(data.translateToInputOffset(1, 60), 150); }); - test('fixed width must be finite and non-negative', () => { - assert.throws( - () => ModelDecorationInjectedTextOptions.from({ content: 'text', widthInEm: Number.NaN }), - /Injected text widthInEm must be a finite non-negative number/ - ); - assert.throws( - () => ModelDecorationInjectedTextOptions.from({ content: 'text', widthInEm: -1 }), - /Injected text widthInEm must be a finite non-negative number/ - ); - }); - function sequence(length: number, start = 0): number[] { const result = new Array(); for (let i = 0; i < length; i++) { diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index dbd7f93393dd7c..735294489786e3 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -222,7 +222,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { wrappedTextIndentLength: lineBreakData?.wrappedTextIndentLength }, { breakOffsets: [4, 7], - wrappedTextIndentLength: 0 + wrappedTextIndentLength: 1 }); }); diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index e0a5a6c5c1c331..2a8f7661cb29a5 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -25,6 +25,7 @@ } .monaco-editor .debug-breakpoint-placeholder { + width: 0.9em; display: inline-flex; vertical-align: middle; margin-top: -1px; diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 5ebafa4978ae00..842a53873c0609 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -432,13 +432,8 @@ suite('Debug - Breakpoints', () => { assert.deepStrictEqual(decorations[1].range, new Range(2, 4, 2, 5)); assert.deepStrictEqual(decorations[2].range, new Range(3, 5, 3, 6)); assert.strictEqual(decorations[0].options.beforeContentClassName, undefined); - assert.deepStrictEqual({ - inlineClassName: decorations[1].options.before?.inlineClassName, - widthInEm: decorations[1].options.before?.widthInEm - }, { - inlineClassName: 'debug-breakpoint-placeholder', - widthInEm: 0.9 - }); + assert.strictEqual(decorations[1].options.before?.inlineClassName, `debug-breakpoint-placeholder`); + assert.strictEqual(decorations[1].options.before?.widthInEm, 0.9); assert.strictEqual(decorations[0].options.overviewRuler?.position, OverviewRulerLane.Left); const expected = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendCodeblock(languageId, 'Condition: x > 5'); assert.deepStrictEqual(decorations[0].options.glyphMarginHoverMessage, expected); diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts index 16f939add7362f..db5814f6cf1b5d 100644 --- a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { timeout } from '../../../../../base/common/async.js'; -import { toDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { IEditorConstructionOptions } from '../../../../../editor/browser/config/editorConfiguration.js'; import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; @@ -55,7 +54,7 @@ async function renderColorDecorators(context: ComponentFixtureContext, selectFir async function renderInlineProgress(context: ComponentFixtureContext): Promise { const { editor, instantiationService } = createEditor(context, 'const result = await work();', 'typescript'); const progress = context.disposableStore.add(instantiationService.createInstance(InlineProgressManager, 'fixture', editor)); - void progress.showWhile( + progress.showWhile( { lineNumber: 1, column: 15 }, 'Computing result', new Promise(() => { }), @@ -95,36 +94,6 @@ async function renderInlayHints(context: ComponentFixtureContext): Promise await timeout(50); } -function renderFixedWidthWrapping(context: ComponentFixtureContext): void { - const { editor } = createEditor( - context, - 'alpha beta gamma delta epsilon', - 'plaintext', - [], - { - fontFamily: 'Arial, sans-serif', - wordWrap: 'wordWrapColumn', - wordWrapColumn: 12, - wrappingStrategy: 'advanced', - wrappingIndent: 'none', - } - ); - const decorations = editor.createDecorationsCollection([{ - range: new Range(1, 7, 1, 7), - options: { - description: 'fixed-width-fixture', - showIfCollapsed: true, - before: { - content: '\xa0', - inlineClassName: 'fixed-width-fixture', - inlineClassNameAffectsLetterSpacing: true, - widthInEm: 3, - } - } - }]); - context.disposableStore.add(toDisposable(() => decorations.clear())); -} - function createEditor( context: ComponentFixtureContext, content: string, @@ -194,10 +163,5 @@ export default defineThemedFixtureGroup({ path: 'editor/' }, { labels: { kind: 'screenshot', blocksCi: true }, expectedVisualDescriptions: ['A single TypeScript statement contains a muted : number inlay hint after value. Narrow, equal-width spaces separate the hint from the source text on both sides, and all content stays on one baseline.'], render: renderInlayHints, - }), - FixedWidthWrapping: defineComponentFixture({ - labels: { kind: 'screenshot', blocksCi: true }, - expectedVisualDescriptions: ['Proportional-font text wraps to three lines: alpha, beta gamma, and delta epsilon. The invisible fixed-width injection after alpha occupies enough horizontal space to move beta to the second line without creating visible content or horizontal overflow.'], - render: renderFixedWidthWrapping, - }), + }) }); From 0cea148cfda39d558779a022328f447ad88f02ca Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 24 Aug 2026 22:17:57 +0200 Subject: [PATCH 22/41] updating screenshots --- .../blocks-ci-screenshots.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index 55af523e15cd96..daa57c5c2e2c98 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -54,6 +54,30 @@ #### editor/codeEditor/CodeEditor/Light ![screenshot](https://hediet-screenshots.azurewebsites.net/images/89b2d50f5bd33feaa20b3c8f83f3d83548fda48b6f85cd7d784460aafaeeb596) +#### editor/injectedTextDecorations/ColorDecorators/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/33584d1dce7a32853b0c3c269ea341c1346364d42f3acede0f39d055ec54ff03) + +#### editor/injectedTextDecorations/ColorDecorators/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/96c71d6072b20e0b9a75089789af194cd503fce1585d013491a57613ede6c96d) + +#### editor/injectedTextDecorations/InlayHints/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/473d30b47283d6c5a10ca3e349c64564708a42898a78cee98ef0cfaa4ecd84d2) + +#### editor/injectedTextDecorations/InlayHints/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/8a9cc16b16476f8fad3c5e5a0fef7f5467624610b44b3929b30759f4ae90f536) + +#### editor/injectedTextDecorations/InlineProgress/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/e5020664a99f0ae97833a6e147699a3b378dd4866b9cc4cd78bde1d92b819286) + +#### editor/injectedTextDecorations/InlineProgress/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/4810c9ab74a193cc722de9f7d3fe7c3bffbf878d65da6c1b6e11cf4fb626ce1e) + +#### editor/injectedTextDecorations/SelectedColorDecorator/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/c2d6a7d8555ae65d180a8751365019f60b9859f62e620873959b802f667fe325) + +#### editor/injectedTextDecorations/SelectedColorDecorator/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/7f70224f7733a2461eba63fa98234aab38b8804a73460deffa11f49cd6f7172c) + #### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/7700bb9cad18d064af94493b4ae0a4f75e3c855df7ba4eb1d8a4a562eaa41dc6) From cae296f47514d52e9592a79019dc42a278153d51 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Tue, 25 Aug 2026 11:26:22 +0200 Subject: [PATCH 23/41] Remove injected text component fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../editor/injectedTextDecorations.fixture.ts | 167 ------------------ .../browser/componentFixtures/fixtureUtils.ts | 2 - .../blocks-ci-screenshots.md | 24 --- 3 files changed, 193 deletions(-) delete mode 100644 src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts diff --git a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts deleted file mode 100644 index db5814f6cf1b5d..00000000000000 --- a/src/vs/workbench/test/browser/componentFixtures/editor/injectedTextDecorations.fixture.ts +++ /dev/null @@ -1,167 +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 { timeout } from '../../../../../base/common/async.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { IEditorConstructionOptions } from '../../../../../editor/browser/config/editorConfiguration.js'; -import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { EditorExtensionsRegistry, IEditorContributionDescription } from '../../../../../editor/browser/editorExtensions.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { DocumentColorProvider, InlayHintsProvider } from '../../../../../editor/common/languages.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; -import { ColorDetector } from '../../../../../editor/contrib/colorPicker/browser/colorDetector.js'; -import '../../../../../editor/contrib/colorPicker/browser/colorPickerContribution.js'; -import '../../../../../editor/contrib/colorPicker/browser/colorPicker.css'; -import { IInlayHintsCache, InlayHintsController } from '../../../../../editor/contrib/inlayHints/browser/inlayHintsController.js'; -import '../../../../../editor/contrib/inlayHints/browser/inlayHintsContribution.js'; -import { InlineProgressManager } from '../../../../../editor/contrib/inlineProgress/browser/inlineProgress.js'; -import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup, ServiceRegistration } from '../fixtureUtils.js'; - -const colorDetectorContribution = EditorExtensionsRegistry.getSomeEditorContributions([ColorDetector.ID])[0]; -const inlayHintsContribution = EditorExtensionsRegistry.getSomeEditorContributions([InlayHintsController.ID])[0]; - -async function renderColorDecorators(context: ComponentFixtureContext, selectFirstColor = false): Promise { - const { editor } = createEditor( - context, - '.red { color: #ff0000; }\n.green { color: #00ff00; }\n.blue { color: #0000ff; }', - 'css', - [colorDetectorContribution], - { colorDecorators: true }, - languageFeaturesService => context.disposableStore.add(languageFeaturesService.colorProvider.register('*', new class implements DocumentColorProvider { - provideDocumentColors() { - return [ - { range: new Range(1, 15, 1, 22), color: { red: 1, green: 0, blue: 0, alpha: 1 } }, - { range: new Range(2, 17, 2, 24), color: { red: 0, green: 1, blue: 0, alpha: 1 } }, - { range: new Range(3, 16, 3, 23), color: { red: 0, green: 0, blue: 1, alpha: 1 } }, - ]; - } - - provideColorPresentations() { - return []; - } - })) - ); - editor.getContribution(ColorDetector.ID); - await timeout(0); - if (selectFirstColor) { - editor.setSelection(new Range(1, 14, 1, editor.getModel()!.getLineMaxColumn(1))); - editor.focus(); - } -} - -async function renderInlineProgress(context: ComponentFixtureContext): Promise { - const { editor, instantiationService } = createEditor(context, 'const result = await work();', 'typescript'); - const progress = context.disposableStore.add(instantiationService.createInstance(InlineProgressManager, 'fixture', editor)); - progress.showWhile( - { lineNumber: 1, column: 15 }, - 'Computing result', - new Promise(() => { }), - { cancel() { } }, - 0 - ); - await timeout(0); -} - -async function renderInlayHints(context: ComponentFixtureContext): Promise { - const { editor } = createEditor( - context, - 'const value = computeResult();', - 'typescript', - [inlayHintsContribution], - { inlayHints: { enabled: 'on', fontSize: 12 } }, - languageFeaturesService => context.disposableStore.add(languageFeaturesService.inlayHintsProvider.register('*', new class implements InlayHintsProvider { - provideInlayHints() { - return { - hints: [{ - label: ': number', - position: { lineNumber: 1, column: 12 }, - paddingLeft: true, - paddingRight: true, - }], - dispose() { } - }; - } - })), - registration => registration.defineInstance(IInlayHintsCache, { - _serviceBrand: undefined, - get: () => undefined, - set: () => { }, - }) - ); - editor.getContribution(InlayHintsController.ID); - await timeout(50); -} - -function createEditor( - context: ComponentFixtureContext, - content: string, - languageId: string, - contributions: IEditorContributionDescription[] = [], - options: IEditorConstructionOptions = {}, - registerLanguageFeatures?: (languageFeaturesService: ILanguageFeaturesService) => void, - registerServices?: (registration: ServiceRegistration) => void -) { - const { container, disposableStore, theme } = context; - container.style.width = '420px'; - container.style.height = '120px'; - container.style.border = '1px solid var(--vscode-editorWidget-border)'; - - const instantiationService = createEditorServices(disposableStore, { - colorTheme: theme, - additionalServices: registerServices, - }); - const languageFeaturesService = instantiationService.get(ILanguageFeaturesService); - registerLanguageFeatures?.(languageFeaturesService); - const model = disposableStore.add(createTextModel( - instantiationService, - content, - URI.parse(`inmemory://injected-text/${languageId}`), - languageId - )); - const editor = disposableStore.add(instantiationService.createInstance( - CodeEditorWidget, - container, - { - automaticLayout: true, - fontFamily: 'Consolas, "Courier New", monospace', - fontSize: 14, - glyphMargin: false, - lineNumbers: 'off', - minimap: { enabled: false }, - renderLineHighlight: 'none', - scrollBeyondLastLine: false, - scrollbar: { horizontal: 'hidden', vertical: 'hidden' }, - wordWrap: 'off', - ...options, - }, - { contributions } - )); - editor.setModel(model); - - return { editor, instantiationService, languageFeaturesService }; -} - -export default defineThemedFixtureGroup({ path: 'editor/' }, { - ColorDecorators: defineComponentFixture({ - labels: { kind: 'screenshot', blocksCi: true }, - expectedVisualDescriptions: ['Three CSS declarations appear on separate lines. Each hexadecimal color is preceded by a square swatch whose fill matches the value. Every swatch is the same size, has a visible contrasting border, and is vertically aligned with its line of text.'], - render: renderColorDecorators, - }), - SelectedColorDecorator: defineComponentFixture({ - labels: { kind: 'screenshot', blocksCi: true }, - expectedVisualDescriptions: ['The first CSS color and the text after it are selected. The selection is continuous on both sides of the square red swatch and ends at the closing brace without detached or misplaced selection blocks.'], - render: context => renderColorDecorators(context, true), - }), - InlineProgress: defineComponentFixture({ - labels: { kind: 'screenshot', blocksCi: true }, - expectedVisualDescriptions: ['A single TypeScript statement appears on one line. A small inline progress placeholder separates the equals sign from await without changing the line height or vertical alignment.'], - render: renderInlineProgress, - }), - InlayHints: defineComponentFixture({ - labels: { kind: 'screenshot', blocksCi: true }, - expectedVisualDescriptions: ['A single TypeScript statement contains a muted : number inlay hint after value. Narrow, equal-width spaces separate the hint from the source text on both sides, and all content stays on one baseline.'], - render: renderInlayHints, - }) -}); diff --git a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts index f7157b2473429a..5ec1d6826469f3 100644 --- a/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts +++ b/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts @@ -885,7 +885,6 @@ export interface ComponentFixtureOptions { labels?: ThemedFixtureGroupLabels; virtualTime?: { enabled?: boolean; durationMs?: number; teardownDrainMs?: number }; additionalThemes?: readonly ComponentFixtureAdditionalTheme[]; - expectedVisualDescriptions?: readonly string[]; } type ThemedFixtures = ReturnType; @@ -917,7 +916,6 @@ export function defineComponentFixture(options: ComponentFixtureOptions): Themed isolation: 'none', displayMode: { type: 'component' }, background: themeVariant.background, - expectedVisualDescriptions: options.expectedVisualDescriptions, inputSchema: fixtureInputSchema, inputControls: { reverseStylesheets: { placement: 'toolbar', label: 'Reverse Stylesheets' }, diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index daa57c5c2e2c98..55af523e15cd96 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -54,30 +54,6 @@ #### editor/codeEditor/CodeEditor/Light ![screenshot](https://hediet-screenshots.azurewebsites.net/images/89b2d50f5bd33feaa20b3c8f83f3d83548fda48b6f85cd7d784460aafaeeb596) -#### editor/injectedTextDecorations/ColorDecorators/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/33584d1dce7a32853b0c3c269ea341c1346364d42f3acede0f39d055ec54ff03) - -#### editor/injectedTextDecorations/ColorDecorators/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/96c71d6072b20e0b9a75089789af194cd503fce1585d013491a57613ede6c96d) - -#### editor/injectedTextDecorations/InlayHints/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/473d30b47283d6c5a10ca3e349c64564708a42898a78cee98ef0cfaa4ecd84d2) - -#### editor/injectedTextDecorations/InlayHints/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/8a9cc16b16476f8fad3c5e5a0fef7f5467624610b44b3929b30759f4ae90f536) - -#### editor/injectedTextDecorations/InlineProgress/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/e5020664a99f0ae97833a6e147699a3b378dd4866b9cc4cd78bde1d92b819286) - -#### editor/injectedTextDecorations/InlineProgress/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/4810c9ab74a193cc722de9f7d3fe7c3bffbf878d65da6c1b6e11cf4fb626ce1e) - -#### editor/injectedTextDecorations/SelectedColorDecorator/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/c2d6a7d8555ae65d180a8751365019f60b9859f62e620873959b802f667fe325) - -#### editor/injectedTextDecorations/SelectedColorDecorator/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/7f70224f7733a2461eba63fa98234aab38b8804a73460deffa11f49cd6f7172c) - #### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/7700bb9cad18d064af94493b4ae0a4f75e3c855df7ba4eb1d8a4a562eaa41dc6) From 9739fb73119c64a635b4aae6df83bd9c7a4f33ff Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 10:35:29 +0200 Subject: [PATCH 24/41] checking widthInEm is defined, finite and positive --- src/vs/editor/common/model/textModel.ts | 2 +- .../common/model/modelDecorations.test.ts | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index bc81bcf1156755..ee6ad23b353b83 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2472,7 +2472,7 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt this.tokens = options.tokens ?? null; this.inlineClassName = options.inlineClassName || null; this.inlineClassNameAffectsLetterSpacing = options.inlineClassNameAffectsLetterSpacing || false; - this.widthInEm = options.widthInEm; + this.widthInEm = options.widthInEm !== undefined && Number.isFinite(options.widthInEm) && options.widthInEm >= 0 ? options.widthInEm : undefined; this.attachedData = options.attachedData || null; this.cursorStops = options.cursorStops || null; } diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts index 142bbead17a39c..cf786fd4fe17e0 100644 --- a/src/vs/editor/test/common/model/modelDecorations.test.ts +++ b/src/vs/editor/test/common/model/modelDecorations.test.ts @@ -113,6 +113,26 @@ suite('Editor Model - Model Decorations', () => { lineHasNoDecorations(thisModel, 5); }); + test('injected text width must be finite and non-negative', () => { + const widths = [NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, -1, 0, 1]; + const decorationIds = thisModel.deltaDecorations([], widths.map(widthInEm => ({ + range: new Range(1, 1, 1, 1), + options: { + description: 'test', + after: { content: 'x', widthInEm } + } + }))); + + assert.deepStrictEqual(decorationIds.map(id => thisModel.getDecorationOptions(id)?.after?.widthInEm), [ + undefined, + undefined, + undefined, + undefined, + 0, + 1 + ]); + }); + test('line decoration', () => { addDecoration(thisModel, 1, 1, 1, 14, 'myType'); lineHasDecoration(thisModel, 1, 1, 14, 'myType'); From 8b47b84203d6aeab0153c4e36f87c9b1e80c0e2e Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 10:42:37 +0200 Subject: [PATCH 25/41] removing change to inlayhintscontroller --- .../contrib/inlayHints/browser/inlayHintsController.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index b83c69c60e0faa..4e3f3e7910be6f 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -40,12 +40,6 @@ import { Position } from '../../../common/core/position.js'; // --- hint caching service (per session) -export interface IInlayHintsCache { - readonly _serviceBrand: undefined; - get(model: ITextModel): InlayHintItem[] | undefined; - set(model: ITextModel, value: InlayHintItem[]): void; -} - class InlayHintsCache { declare readonly _serviceBrand: undefined; @@ -67,7 +61,8 @@ class InlayHintsCache { } } -export const IInlayHintsCache = createDecorator('IInlayHintsCache'); +interface IInlayHintsCache extends InlayHintsCache { } +const IInlayHintsCache = createDecorator('IInlayHintsCache'); registerSingleton(IInlayHintsCache, InlayHintsCache, InstantiationType.Delayed); // --- rendered label From 007cfe5a298586d3f3147570ea12429a683191a1 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 10:50:44 +0200 Subject: [PATCH 26/41] making firstFixedWidthRange as being potentially undefined more explicit --- src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 232b7b12f05bbc..de5057876c6450 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -402,7 +402,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st let breakingColumn = firstLineBreakColumn; let fixedWidthRangeIndex = 0; - const firstFixedWidthRange = injectedTextWidthsInEm[0]; + const firstFixedWidthRange = injectedTextWidthsInEm.length > 0 ? injectedTextWidthsInEm[0] : null; const startsWithFixedWidth = firstFixedWidthRange && firstFixedWidthRange.startOffset === 0; let prevCharCode: number; From 5584b634a939a7278f941b910e1c98458d2b32ed Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 11:06:12 +0200 Subject: [PATCH 27/41] adding check on fixedWidthRangeIndex index --- src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index de5057876c6450..24cd0662deee3f 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -432,7 +432,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st let prevCharCodeClass = classifier.get(prevCharCode); for (let i = startOffset; i < len; i++) { - const fixedWidthRange = injectedTextWidthsInEm[fixedWidthRangeIndex]; + const fixedWidthRange = injectedTextWidthsInEm.length > 0 && fixedWidthRangeIndex < injectedTextWidthsInEm.length ? injectedTextWidthsInEm[fixedWidthRangeIndex] : null; const charStartOffset = i; let charCode = lineText.charCodeAt(i); let charCodeClass: CharacterClass; From 679333642ee4eeffb4316508487cc22877f34902 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 11:59:12 +0200 Subject: [PATCH 28/41] using one accumulator --- .../viewModel/monospaceLineBreaksComputer.ts | 47 ++++--------------- .../monospaceLineBreaksComputer.test.ts | 14 +++--- 2 files changed, 17 insertions(+), 44 deletions(-) diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 24cd0662deee3f..adec2065f32f27 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -398,7 +398,6 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st let breakingOffsetsCount: number = 0; let breakOffset = 0; let breakOffsetVisibleColumn = 0; - let breakOffsetWrappingColumn = 0; let breakingColumn = firstLineBreakColumn; let fixedWidthRangeIndex = 0; @@ -407,24 +406,20 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st let prevCharCode: number; let visibleColumn: number; - let wrappingColumn: number; let startOffset: number; if (startsWithFixedWidth) { // The line starts with injected text of a specific width prevCharCode = lineText.charCodeAt(firstFixedWidthRange.endOffset - 1); - visibleColumn = computeRangeWidth(lineText, 0, firstFixedWidthRange.endOffset, 0, tabSize, columnsForFullWidthChar); - wrappingColumn = firstFixedWidthRange.widthInEm * columnsPerEm; + visibleColumn = firstFixedWidthRange.widthInEm * columnsPerEm; startOffset = firstFixedWidthRange.endOffset; fixedWidthRangeIndex++; } else { prevCharCode = lineText.charCodeAt(0); visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar); - wrappingColumn = visibleColumn; startOffset = 1; if (strings.isHighSurrogate(prevCharCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken visibleColumn += 1; - wrappingColumn += 1; prevCharCode = lineText.charCodeAt(1); startOffset++; } @@ -436,59 +431,51 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st const charStartOffset = i; let charCode = lineText.charCodeAt(i); let charCodeClass: CharacterClass; - let visibleCharWidth: number; - let wrappingCharWidth: number; + let charWidth: number; let wrapEscapedLineFeed = false; if (fixedWidthRange && fixedWidthRange.startOffset === i) { i = fixedWidthRange.endOffset - 1; charCode = lineText.charCodeAt(i); charCodeClass = classifier.get(charCode); - visibleCharWidth = computeRangeWidth(lineText, charStartOffset, fixedWidthRange.endOffset, visibleColumn, tabSize, columnsForFullWidthChar); - wrappingCharWidth = fixedWidthRange.widthInEm * columnsPerEm; + charWidth = fixedWidthRange.widthInEm * columnsPerEm; fixedWidthRangeIndex++; } else if (strings.isHighSurrogate(charCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i++; charCodeClass = CharacterClass.NONE; - visibleCharWidth = 2; - wrappingCharWidth = visibleCharWidth; + charWidth = 2; } else { charCodeClass = classifier.get(charCode); - visibleCharWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); - wrappingCharWidth = visibleCharWidth; + charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); } // literal \n shall trigger a softwrap if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, i)) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; - breakOffsetWrappingColumn = wrappingColumn; wrapEscapedLineFeed = true; } else if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; - breakOffsetWrappingColumn = wrappingColumn; } - visibleColumn += visibleCharWidth; - wrappingColumn += wrappingCharWidth; + visibleColumn += charWidth; // check if adding character at `i` will go over the breaking column - if (wrappingColumn > breakingColumn || wrapEscapedLineFeed) { + if (visibleColumn > breakingColumn || wrapEscapedLineFeed) { // We need to break at least before character at `i`: - if (breakOffset === 0 || wrappingColumn - breakOffsetWrappingColumn > wrappedLineBreakColumn) { + if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) { // Cannot break at `breakOffset`, must break at `i` breakOffset = charStartOffset; - breakOffsetVisibleColumn = visibleColumn - visibleCharWidth; - breakOffsetWrappingColumn = wrappingColumn - wrappingCharWidth; + breakOffsetVisibleColumn = visibleColumn - charWidth; } breakingOffsets[breakingOffsetsCount] = breakOffset; breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; breakingOffsetsCount++; - breakingColumn = breakOffsetWrappingColumn + wrappedLineBreakColumn; + breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; breakOffset = 0; } @@ -507,20 +494,6 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st return new ModelLineProjectionData(injectionOffsets, injectionOptions, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); } -function computeRangeWidth(lineText: string, startOffset: number, endOffset: number, startVisibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { - let width = 0; - for (let i = startOffset; i < endOffset; i++) { - const charCode = lineText.charCodeAt(i); - if (strings.isHighSurrogate(charCode)) { - width += 2; - i++; - } else { - width += computeCharWidth(charCode, startVisibleColumn + width, tabSize, columnsForFullWidthChar); - } - } - return width; -} - function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { if (charCode === CharCode.Tab) { return (tabSize - (visibleColumn % tabSize)); diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 735294489786e3..a5026e52cdc9a2 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -146,7 +146,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [4, 7], - breakOffsetsVisibleColumn: [4, 7] + breakOffsetsVisibleColumn: [4, 8] }); }); @@ -161,7 +161,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [8, 11], - breakOffsetsVisibleColumn: [8, 11] + breakOffsetsVisibleColumn: [5, 8] }); }); @@ -177,7 +177,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [4, 8], - breakOffsetsVisibleColumn: [4, 8] + breakOffsetsVisibleColumn: [4, 9] }); }); @@ -192,11 +192,11 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [3, 4, 7], - breakOffsetsVisibleColumn: [3, 4, 7] + breakOffsetsVisibleColumn: [3, 9, 12] }); }); - test('keeps rendered visible columns integral with fractional fixed widths', () => { + test('uses fixed injected text width when computing following tab stops', () => { const factory = new MonospaceLineBreaksComputerFactory('', ''); const lineBreakData = getLineBreakData(factory, 4, 4, 2, WrappingIndent.None, 'normal', false, 'ab\tcd', null, [ new LineInjectedText(0, 1, 3, { content: '\xa0', widthInEm: 0.75 }, 0) @@ -206,8 +206,8 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsets: lineBreakData?.breakOffsets, breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { - breakOffsets: [3, 6], - breakOffsetsVisibleColumn: [3, 6] + breakOffsets: [4, 6], + breakOffsetsVisibleColumn: [4, 6] }); }); From 4f3b31f5d228a9e96a7c77ddd33d0c88ee925794 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 15:54:01 +0200 Subject: [PATCH 29/41] using pixels for wrapping --- .../viewModel/monospaceLineBreaksComputer.ts | 87 +++++++++++++++---- .../monospaceLineBreaksComputer.test.ts | 38 ++++++-- 2 files changed, 98 insertions(+), 27 deletions(-) diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index adec2065f32f27..88adeec6922f16 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -8,7 +8,7 @@ import * as strings from '../../../base/common/strings.js'; import { WrappingIndent, IComputedEditorOptions, EditorOption } from '../config/editorOptions.js'; import { CharacterClassifier } from '../core/characterClassifier.js'; import { FontInfo } from '../config/fontInfo.js'; -import { LineInjectedText } from '../textModelEvents.js'; +import { FixedWidthInjectedTextRange, LineInjectedText } from '../textModelEvents.js'; import { InjectedTextOptions } from '../model.js'; import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData, ILineBreaksComputerContext } from '../modelLineProjectionData.js'; @@ -46,7 +46,7 @@ export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFa if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText && !isLineFeedWrappingEnabled) { result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, lineText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak); } else { - result[i] = createLineBreaks(this.classifier, lineText, injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, fontInfo.fontSize / fontInfo.typicalHalfwidthCharacterWidth, wrappingIndent, wordBreak, isLineFeedWrappingEnabled); + result[i] = createLineBreaks(this.classifier, lineText, injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, fontInfo, wrappingIndent, wordBreak, isLineFeedWrappingEnabled); } } arrPool1.length = 0; @@ -356,9 +356,9 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla return previousBreakingData; } -function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, columnsPerEm: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ModelLineProjectionData | null { +function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, fontInfo: FontInfo, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ModelLineProjectionData | null { const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts); - const injectedTextWidthsInEm = LineInjectedText.getFixedWidthInjectedTextRanges(injectedTexts); + const fixedWidthRanges = LineInjectedText.getFixedWidthInjectedTextRanges(injectedTexts); let injectionOptions: InjectedTextOptions[] | null; let injectionOffsets: number[] | null; @@ -391,91 +391,109 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st const isKeepAll = (wordBreak === 'keepAll'); const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent); - const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; + + // The wrapping decision is taken in pixels, because injected text can request an arbitrary + // width via `widthInEm` which does not map to a whole number of columns. + const typicalHalfwidthCharacterWidth = fontInfo.typicalHalfwidthCharacterWidth; + const wrappedLineBreakPixelWidth = (firstLineBreakColumn - wrappedTextIndentLength) * typicalHalfwidthCharacterWidth; const breakingOffsets: number[] = []; const breakingOffsetsVisibleColumn: number[] = []; let breakingOffsetsCount: number = 0; let breakOffset = 0; let breakOffsetVisibleColumn = 0; + let breakOffsetPixelWidth = 0; - let breakingColumn = firstLineBreakColumn; + let breakingPixelWidth = firstLineBreakColumn * typicalHalfwidthCharacterWidth; let fixedWidthRangeIndex = 0; - const firstFixedWidthRange = injectedTextWidthsInEm.length > 0 ? injectedTextWidthsInEm[0] : null; + const firstFixedWidthRange = fixedWidthRanges.length > 0 ? fixedWidthRanges[0] : null; const startsWithFixedWidth = firstFixedWidthRange && firstFixedWidthRange.startOffset === 0; let prevCharCode: number; + let prevCharCodeClass: CharacterClass; let visibleColumn: number; + let currentLinePixelWidth: number; let startOffset: number; if (startsWithFixedWidth) { - // The line starts with injected text of a specific width - prevCharCode = lineText.charCodeAt(firstFixedWidthRange.endOffset - 1); - visibleColumn = firstFixedWidthRange.widthInEm * columnsPerEm; + prevCharCode = CharCode.Null; + prevCharCodeClass = CharacterClass.NONE; + visibleColumn = computeFixedWidthRangeColumnWidth(lineText, firstFixedWidthRange, 0, tabSize, columnsForFullWidthChar); + currentLinePixelWidth = firstFixedWidthRange.widthInEm * fontInfo.fontSize; startOffset = firstFixedWidthRange.endOffset; fixedWidthRangeIndex++; } else { prevCharCode = lineText.charCodeAt(0); visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar); + currentLinePixelWidth = computeCharPixelWidth(prevCharCode, 0, tabSize, fontInfo); startOffset = 1; if (strings.isHighSurrogate(prevCharCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken visibleColumn += 1; + currentLinePixelWidth += typicalHalfwidthCharacterWidth; prevCharCode = lineText.charCodeAt(1); startOffset++; } + prevCharCodeClass = classifier.get(prevCharCode); } - let prevCharCodeClass = classifier.get(prevCharCode); for (let i = startOffset; i < len; i++) { - const fixedWidthRange = injectedTextWidthsInEm.length > 0 && fixedWidthRangeIndex < injectedTextWidthsInEm.length ? injectedTextWidthsInEm[fixedWidthRangeIndex] : null; + const fixedWidthRange = fixedWidthRanges.length > 0 && fixedWidthRangeIndex < fixedWidthRanges.length ? fixedWidthRanges[fixedWidthRangeIndex] : null; const charStartOffset = i; let charCode = lineText.charCodeAt(i); let charCodeClass: CharacterClass; let charWidth: number; + let charPixelWidth: number; let wrapEscapedLineFeed = false; if (fixedWidthRange && fixedWidthRange.startOffset === i) { + charCode = CharCode.Null; + charCodeClass = CharacterClass.NONE; + charWidth = computeFixedWidthRangeColumnWidth(lineText, fixedWidthRange, visibleColumn, tabSize, columnsForFullWidthChar); + charPixelWidth = fixedWidthRange.widthInEm * fontInfo.fontSize; i = fixedWidthRange.endOffset - 1; - charCode = lineText.charCodeAt(i); - charCodeClass = classifier.get(charCode); - charWidth = fixedWidthRange.widthInEm * columnsPerEm; fixedWidthRangeIndex++; } else if (strings.isHighSurrogate(charCode)) { // A surrogate pair must always be considered as a single unit, so it is never to be broken i++; charCodeClass = CharacterClass.NONE; charWidth = 2; + charPixelWidth = 2 * typicalHalfwidthCharacterWidth; } else { charCodeClass = classifier.get(charCode); charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); + charPixelWidth = computeCharPixelWidth(charCode, visibleColumn, tabSize, fontInfo); } // literal \n shall trigger a softwrap if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, i)) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; + breakOffsetPixelWidth = currentLinePixelWidth; wrapEscapedLineFeed = true; } else if (canBreak(prevCharCode, prevCharCodeClass, charCode, charCodeClass, isKeepAll)) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; + breakOffsetPixelWidth = currentLinePixelWidth; } visibleColumn += charWidth; + currentLinePixelWidth += charPixelWidth; - // check if adding character at `i` will go over the breaking column - if (visibleColumn > breakingColumn || wrapEscapedLineFeed) { + // check if adding character at `i` will go over the breaking width + if (currentLinePixelWidth > breakingPixelWidth || wrapEscapedLineFeed) { // We need to break at least before character at `i`: - if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) { + if (breakOffset === 0 || currentLinePixelWidth - breakOffsetPixelWidth > wrappedLineBreakPixelWidth) { // Cannot break at `breakOffset`, must break at `i` breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn - charWidth; + breakOffsetPixelWidth = currentLinePixelWidth - charPixelWidth; } breakingOffsets[breakingOffsetsCount] = breakOffset; breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; breakingOffsetsCount++; - breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; + breakingPixelWidth = breakOffsetPixelWidth + wrappedLineBreakPixelWidth; breakOffset = 0; } @@ -512,6 +530,37 @@ function tabCharacterWidth(visibleColumn: number, tabSize: number): number { return (tabSize - (visibleColumn % tabSize)); } +/** + * The width in pixels a character occupies. Used for the wrapping decision, which must reason + * in real widths because injected text can request an arbitrary width via `widthInEm`. + */ +function computeCharPixelWidth(charCode: number, visibleColumn: number, tabSize: number, fontInfo: FontInfo): number { + if (charCode === CharCode.Tab) { + return tabCharacterWidth(visibleColumn, tabSize) * fontInfo.typicalHalfwidthCharacterWidth; + } + if (strings.isFullWidthCharacter(charCode)) { + return fontInfo.typicalFullwidthCharacterWidth; + } + if (charCode < 32) { + // when using `editor.renderControlCharacters`, the substitutions are often wide + return fontInfo.typicalFullwidthCharacterWidth; + } + return fontInfo.typicalHalfwidthCharacterWidth; +} + +/** + * The number of columns the characters of a fixed width injected text range occupy. `widthInEm` + * is deliberately ignored here: `visibleColumn` only drives tab expansion, which must stay in + * sync with the line rendering, and the rendering does not know about `widthInEm`. + */ +function computeFixedWidthRangeColumnWidth(lineText: string, range: FixedWidthInjectedTextRange, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { + let width = 0; + for (let i = range.startOffset; i < range.endOffset; i++) { + width += computeCharWidth(lineText.charCodeAt(i), visibleColumn + width, tabSize, columnsForFullWidthChar); + } + return width; +} + /** * Checks if the current position in the text should trigger a soft wrap due to escaped line feeds. * This handles the wrapOnEscapedLineFeeds feature which allows \n sequences in strings to trigger wrapping. diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index a5026e52cdc9a2..54e711f28ee03f 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -146,7 +146,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [4, 7], - breakOffsetsVisibleColumn: [4, 8] + breakOffsetsVisibleColumn: [4, 7] }); }); @@ -161,7 +161,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [8, 11], - breakOffsetsVisibleColumn: [5, 8] + breakOffsetsVisibleColumn: [8, 11] }); }); @@ -177,7 +177,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [4, 8], - breakOffsetsVisibleColumn: [4, 9] + breakOffsetsVisibleColumn: [4, 8] }); }); @@ -192,22 +192,44 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { breakOffsets: [3, 4, 7], - breakOffsetsVisibleColumn: [3, 9, 12] + breakOffsetsVisibleColumn: [3, 4, 7] }); }); - test('uses fixed injected text width when computing following tab stops', () => { + test('wraps before a tab that no longer fits after fixed-width injected text', () => { const factory = new MonospaceLineBreaksComputerFactory('', ''); const lineBreakData = getLineBreakData(factory, 4, 4, 2, WrappingIndent.None, 'normal', false, 'ab\tcd', null, [ new LineInjectedText(0, 1, 3, { content: '\xa0', widthInEm: 0.75 }, 0) ]); + // `ab` plus a 0.75em wide injection fills 2.75 columns worth of pixels, so the tab (which + // expands from character column 3 to character column 4) no longer fits and the line wraps + // right before it. + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [3, 6], + breakOffsetsVisibleColumn: [3, 6] + }); + }); + + test('expands a tab following fixed-width injected text from the character column', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 10, 2, WrappingIndent.None, 'normal', false, 'ab\tcdefgh', null, [ + new LineInjectedText(0, 1, 3, { content: '\xa0', widthInEm: 1.5 }, 0) + ]); + + // The injection counts as a single character column, so the tab at character column 3 expands + // to character column 4 and the whole line spans 10 columns. Were the 1.5em width to leak into + // the column accumulator, the injection would count as 3 columns and the tab would expand to + // character column 8 instead. assert.deepStrictEqual({ breakOffsets: lineBreakData?.breakOffsets, breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { - breakOffsets: [4, 6], - breakOffsetsVisibleColumn: [4, 6] + breakOffsets: [4, 10], + breakOffsetsVisibleColumn: [4, 10] }); }); @@ -234,7 +256,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { assert.deepStrictEqual(a.breakOffsets, b.breakOffsets); assert.deepStrictEqual(a.wrappedTextIndentLength, b.wrappedTextIndentLength); for (let i = 0; i < a.breakOffsetsVisibleColumn.length; i++) { - const diff = a.breakOffsetsVisibleColumn[i] - b.breakOffsetsVisibleColumn[i]; + const diff = Math.abs(a.breakOffsetsVisibleColumn[i] - b.breakOffsetsVisibleColumn[i]); assert.ok(diff < 0.001); } } From f55144ef1e1b77b236d026041bcf5bc1be8253e2 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 19:13:35 +0200 Subject: [PATCH 30/41] allowing to set empty content --- .../browser/view/domLineBreaksComputer.ts | 32 ++++- src/vs/editor/common/textModelEvents.ts | 22 +++- .../common/viewLayout/lineDecorations.ts | 4 +- .../common/viewLayout/viewLineRenderer.ts | 14 ++- .../common/viewModel/inlineDecorations.ts | 27 +++- .../viewModel/monospaceLineBreaksComputer.ts | 7 +- .../colorPicker/browser/colorDetector.ts | 3 +- .../browser/inlayHintsController.ts | 2 +- .../inlineProgress/browser/inlineProgress.ts | 3 +- .../common/viewLayout/lineDecorations.test.ts | 34 +++++ .../viewModel/inlineDecorations.test.ts | 118 ++++++++++++++++++ .../monospaceLineBreaksComputer.test.ts | 82 +++++++++++- 12 files changed, 317 insertions(+), 31 deletions(-) diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index e741c3366a244d..255347c480cb3b 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -240,15 +240,33 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: sb.appendString(''); for (let charIndex = 0; charIndex < len; charIndex++) { - const fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex]; + let fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex]; const startsFixedWidth = fixedWidthRange && fixedWidthRange.startOffset === charIndex; if (startsFixedWidth) { if (spanOpen) { sb.appendString(''); } - sb.appendString(''); + // Injected text that only reserves horizontal space covers no character, so it gets a span of + // its own. Rendering it inside the span of the character below would make that character fixed + // width as well. Several such injections can sit at the same offset. + while (fixedWidthRange && fixedWidthRange.startOffset === charIndex && fixedWidthRange.endOffset === charIndex) { + sb.appendString(''); + sb.appendString(''); + spanStartOffsets.push(charOffset); + fixedWidthRange = fixedWidthRanges[++fixedWidthRangeIndex]; + } + // The character below goes into a fixed width span if one still covers it, a normal one + // otherwise. At most one such range can start here: injections at the same column are laid + // out one after the other, so only an empty one leaves the next starting at the same offset. + if (fixedWidthRange && fixedWidthRange.startOffset === charIndex) { + sb.appendString(''); + } else { + sb.appendString(''); + } spanStartOffsets.push(charOffset); spanOpen = true; } else if (!spanOpen) { @@ -323,7 +341,9 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: charOffset += producedCharacters; visibleColumn += charWidth; - if (fixedWidthRange && charIndex + 1 === fixedWidthRange.endOffset) { + // A range that covers no character has already been closed above, and must not be consumed here: + // its `endOffset` equals its `startOffset`, so this condition would hold one character too early. + if (fixedWidthRange && fixedWidthRange.startOffset < fixedWidthRange.endOffset && charIndex + 1 === fixedWidthRange.endOffset) { sb.appendString(''); spanOpen = false; fixedWidthRangeIndex++; @@ -332,6 +352,8 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: if (spanOpen) { sb.appendString(''); } + // A spacing-only injection at the very end of the line is left out on purpose: nothing follows it, + // so it cannot move a break point. `MonospaceLineBreaksComputer` ignores it for the same reason. charOffsets[lineContent.length] = charOffset; visibleColumns[lineContent.length] = visibleColumn; diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 95b70e9876c73f..561feefda77050 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -244,6 +244,14 @@ export interface FixedWidthInjectedTextRange { readonly widthInEm: number; } +/** + * Whether injected text takes up space on a line, either through its content or, when it is + * width-only (e.g. `{ content: '', widthInEm: 1 }`), through the horizontal space it reserves. + */ +function occupiesHorizontalSpace(options: InjectedTextOptions): boolean { + return options.content.length > 0 || (options.widthInEm !== undefined && options.widthInEm > 0); +} + /** * Represents text injected on a line * @internal @@ -267,7 +275,7 @@ export class LineInjectedText { public static fromDecorations(decorations: IModelDecoration[]): LineInjectedText[] { const result: LineInjectedText[] = []; for (const decoration of decorations) { - if (decoration.options.before && decoration.options.before.content.length > 0) { + if (decoration.options.before && occupiesHorizontalSpace(decoration.options.before)) { result.push(new LineInjectedText( decoration.ownerId, decoration.range.startLineNumber, @@ -276,7 +284,7 @@ export class LineInjectedText { 0, )); } - if (decoration.options.after && decoration.options.after.content.length > 0) { + if (decoration.options.after && occupiesHorizontalSpace(decoration.options.after)) { result.push(new LineInjectedText( decoration.ownerId, decoration.range.endLineNumber, @@ -298,6 +306,16 @@ export class LineInjectedText { return result; } + /** + * The ranges of `applyInjectedText(...)` that are rendered at a fixed width. Width-only injected + * text produces an empty range (`startOffset === endOffset`) which reserves horizontal space + * without covering any character. + * + * `injectedTexts` must be sorted by column, which is what `fromDecorations` produces and what + * `applyInjectedText` already requires. The result is then sorted by `startOffset` and never + * overlaps: injections at the same column are laid out one after the other, so only an injection + * with empty content leaves the next one starting at the same offset. + */ public static getFixedWidthInjectedTextRanges(injectedTexts: readonly LineInjectedText[] | null): FixedWidthInjectedTextRange[] { const result: FixedWidthInjectedTextRange[] = []; let injectedTextLength = 0; diff --git a/src/vs/editor/common/viewLayout/lineDecorations.ts b/src/vs/editor/common/viewLayout/lineDecorations.ts index 3439b945aacc27..521d2668face0e 100644 --- a/src/vs/editor/common/viewLayout/lineDecorations.ts +++ b/src/vs/editor/common/viewLayout/lineDecorations.ts @@ -92,7 +92,9 @@ export class LineDecoration { } private static _typeCompare(a: InlineDecorationType, b: InlineDecorationType): number { - const ORDER = [2, 0, 1, 3]; + // WidthOnly, Before, After, Regular, RegularAffectingLetterSpacing. + // Width only decorations come from injected text, which renders before any other decoration. + const ORDER = [3, 1, 2, 4, 0]; return ORDER[a] - ORDER[b]; } diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 11aca36bbb9a7c..3616af6c04d186 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -382,18 +382,19 @@ export function renderViewLine(input: RenderLineInput, sb: StringBuilder): Rende let afterCount = 0; let containsForeignElements = ForeignElementType.None; for (const lineDecoration of input.lineDecorations) { - if (lineDecoration.type === InlineDecorationType.Before || lineDecoration.type === InlineDecorationType.After) { + if (lineDecoration.type === InlineDecorationType.Before || lineDecoration.type === InlineDecorationType.After || lineDecoration.type === InlineDecorationType.WidthOnly) { sb.appendString(``); - if (lineDecoration.type === InlineDecorationType.Before) { - containsForeignElements |= ForeignElementType.Before; - beforeCount++; - } if (lineDecoration.type === InlineDecorationType.After) { containsForeignElements |= ForeignElementType.After; afterCount++; + } else { + // Width only decorations sit before the character at their column, just like + // before content decorations. + containsForeignElements |= ForeignElementType.Before; + beforeCount++; } } } @@ -502,6 +503,9 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput containsForeignElements |= ForeignElementType.Before; } else if (lineDecoration.type === InlineDecorationType.After) { containsForeignElements |= ForeignElementType.After; + } else if (lineDecoration.type === InlineDecorationType.WidthOnly) { + // Pretend there are foreign elements... although not 100% accurate. See above. + containsForeignElements |= ForeignElementType.Before; } } tokens = _applyInlineDecorations(lineContent, len, tokens, input.lineDecorations); diff --git a/src/vs/editor/common/viewModel/inlineDecorations.ts b/src/vs/editor/common/viewModel/inlineDecorations.ts index f6869c57dd334e..c1829eaf26296e 100644 --- a/src/vs/editor/common/viewModel/inlineDecorations.ts +++ b/src/vs/editor/common/viewModel/inlineDecorations.ts @@ -13,7 +13,15 @@ export const enum InlineDecorationType { Regular = 0, Before = 1, After = 2, - RegularAffectingLetterSpacing = 3 + RegularAffectingLetterSpacing = 3, + /** + * A decoration that covers no character, yet is rendered as an element of its own. + * Used for injected text that only reserves horizontal space (e.g. `{ content: '', widthInEm: 1 }`): + * there is no character to decorate, so the width comes from the decoration's class instead. + * Unlike {@link InlineDecorationType.Before}/{@link InlineDecorationType.After}, this is not a + * pseudo element attached to a surrounding decoration, but the injected content itself. + */ + WidthOnly = 4 } export class InlineDecoration { @@ -230,7 +238,8 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations const lineEndOffsetInInputWithInjections = breakOffsets[outputLineIndex]; while (currentInjectedOffset < injectionOffsets.length) { - const length = injectionOptions![currentInjectedOffset].content.length; + const options = injectionOptions![currentInjectedOffset]; + const length = options.content.length; const injectedTextStartOffsetInInputWithInjections = injectionOffsets[currentInjectedOffset] + totalInjectedTextLengthBefore; const injectedTextEndOffsetInInputWithInjections = injectedTextStartOffsetInInputWithInjections + length; @@ -239,18 +248,26 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations break; } + // Injected text that only reserves horizontal space covers no character, so it cannot be + // contained in a wrapped line the way injected text with content is. + const isWidthOnly = (length === 0 && options.widthInEm !== undefined); if (lineStartOffsetInInputWithInjections < injectedTextEndOffsetInInputWithInjections) { // Injected text ends after or in this line (but also starts in or before this line). - const options = injectionOptions![currentInjectedOffset]; if (options.inlineClassName) { const wrappedTextIndentLength = this.context.getWrappedTextIndentLength(modelLineNumber); const offset = (outputLineIndex > 0 ? wrappedTextIndentLength : 0); const start = offset + Math.max(injectedTextStartOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, 0); const end = offset + Math.min(injectedTextEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, lineEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections); - if (start !== end) { + if (start !== end || isWidthOnly) { const viewLineNumber = this.context.getBaseViewLineNumber(modelLineNumber) + outputLineIndex; const range = new Range(viewLineNumber, start + 1, viewLineNumber, end + 1); - const type: InlineDecorationType = options.inlineClassNameAffectsLetterSpacing ? InlineDecorationType.RegularAffectingLetterSpacing : InlineDecorationType.Regular; + const type: InlineDecorationType = ( + isWidthOnly + ? InlineDecorationType.WidthOnly + : options.inlineClassNameAffectsLetterSpacing + ? InlineDecorationType.RegularAffectingLetterSpacing + : InlineDecorationType.Regular + ); inlineDecorations.push(new InlineDecoration(range, options.inlineClassName, type)); } } diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 88adeec6922f16..acf1b0469f1c26 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -549,9 +549,10 @@ function computeCharPixelWidth(charCode: number, visibleColumn: number, tabSize: } /** - * The number of columns the characters of a fixed width injected text range occupy. `widthInEm` - * is deliberately ignored here: `visibleColumn` only drives tab expansion, which must stay in - * sync with the line rendering, and the rendering does not know about `widthInEm`. + * The number of columns the characters of a fixed width injected text range occupy, which is 0 for + * a width-only injection. `widthInEm` is deliberately ignored here: `visibleColumn` only drives tab + * expansion, which must stay in sync with the line rendering, and the rendering does not know about + * `widthInEm`. */ function computeFixedWidthRangeColumnWidth(lineText: string, range: FixedWidthInjectedTextRange, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { let width = 0; diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index a605d3c978965b..d0bda30a282268 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -9,7 +9,6 @@ import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; -import { noBreakWhitespace } from '../../../../base/common/strings.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { DynamicCssRules } from '../../../browser/editorDom.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -239,7 +238,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { options: { description: 'colorDetector', before: { - content: noBreakWhitespace, + content: '', inlineClassName: `${ref.className} colorpicker-color-decoration`, inlineClassNameAffectsLetterSpacing: true, attachedData: ColorDecorationInjectedTextMarker, diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 4e3f3e7910be6f..e09a3807fbd9e2 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -568,7 +568,7 @@ export class InlayHintsController implements IEditorContribution { width: `${widthInPixels}px`, display: 'inline-block' }); - addInjectedText(item, marginRule, '\u200a', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None, InlayHintsController._whitespaceData, widthInEm); + addInjectedText(item, marginRule, '', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None, InlayHintsController._whitespaceData, widthInEm); }; diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts index e111516e10f821..95ee9ae4fea4f3 100644 --- a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts +++ b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts @@ -7,7 +7,6 @@ import * as dom from '../../../../base/browser/dom.js'; import { disposableTimeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { noBreakWhitespace } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import './inlineProgressWidget.css'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js'; @@ -24,7 +23,7 @@ const inlineProgressDecoration = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, showIfCollapsed: true, after: { - content: noBreakWhitespace, + content: '', inlineClassName: 'inline-editor-progress-decoration', inlineClassNameAffectsLetterSpacing: true, widthInEm: 1, diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts index efe951f345715a..f19d4fcd653156 100644 --- a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts +++ b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts @@ -63,6 +63,40 @@ suite('Editor ViewLayout - ViewLineParts', () => { ]); }); + test('empty width only decorations are kept, empty regular ones are not', () => { + const result = LineDecoration.filter([ + new InlineDecoration(new Range(4, 3, 4, 3), 'spacer', InlineDecorationType.WidthOnly), + new InlineDecoration(new Range(4, 3, 4, 3), 'regular', InlineDecorationType.Regular), + ], 4, 1, 500); + + assert.deepStrictEqual(result, [ + new LineDecoration(3, 3, 'spacer', InlineDecorationType.WidthOnly), + ]); + }); + + test('width only decorations come before other decorations at the same position', () => { + const decorations = [ + new LineDecoration(3, 3, 'regular', InlineDecorationType.Regular), + new LineDecoration(3, 3, 'after', InlineDecorationType.After), + new LineDecoration(3, 3, 'spacer', InlineDecorationType.WidthOnly), + new LineDecoration(3, 3, 'before', InlineDecorationType.Before), + ]; + decorations.sort(LineDecoration.compare); + + assert.deepStrictEqual(decorations.map(d => d.className), ['spacer', 'before', 'after', 'regular']); + }); + + test('width only decorations are not turned into pseudo elements', () => { + const result = LineDecorationsNormalizer.normalize('abc', [ + new LineDecoration(2, 2, 'spacer', InlineDecorationType.WidthOnly), + ]); + + // Metadata stays `0`: unlike a before or after decoration, this is the injected content itself. + assert.deepStrictEqual(result, [ + new DecorationSegment(1, 0, 'spacer', 0), + ]); + }); + test('ViewLineParts', () => { assert.deepStrictEqual(LineDecorationsNormalizer.normalize('abcabcabcabcabcabcabcabcabcabc', [ diff --git a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts index 1169e66b7abb1e..39c5c5e9507aaf 100644 --- a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts @@ -506,4 +506,122 @@ suite('InjectedTextInlineDecorationsComputer', () => { [new InlineDecoration(new Range(6, 1, 6, 3), 'wrap-class', InlineDecorationType.Regular)], ]); }); + + test('spacing-only injection at the beginning of a line', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', inlineClassName: 'spacer', widthInEm: 1 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [0], + getBreakOffsets: () => [10], // the injection contributes no characters + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + // The decoration covers no character, so it is empty and starts where the injection sits. + assert.deepStrictEqual(result, [ + [new InlineDecoration(new Range(1, 1, 1, 1), 'spacer', InlineDecorationType.WidthOnly)] + ]); + }); + + test('spacing-only injection in the middle of a line', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', inlineClassName: 'spacer', widthInEm: 1 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [5], + getBreakOffsets: () => [10], + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + assert.deepStrictEqual(result, [ + [new InlineDecoration(new Range(1, 6, 1, 6), 'spacer', InlineDecorationType.WidthOnly)] + ]); + }); + + test('spacing-only injection at the end of a line', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', inlineClassName: 'spacer', widthInEm: 1 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [10], + getBreakOffsets: () => [10], + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + // There is no following output line to move it to, so it stays at the end of this one. + assert.deepStrictEqual(result, [ + [new InlineDecoration(new Range(1, 11, 1, 11), 'spacer', InlineDecorationType.WidthOnly)] + ]); + }); + + test('spacing-only injection sitting exactly on a line break moves to the next line', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', inlineClassName: 'spacer', widthInEm: 1 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [8], + getBreakOffsets: () => [8, 20], + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + // Injected text with content would also end up at the beginning of the second line, + // so a spacing-only injection must not be left dangling at the end of the first one. + assert.deepStrictEqual(result, [ + [], + [new InlineDecoration(new Range(2, 1, 2, 1), 'spacer', InlineDecorationType.WidthOnly)], + ]); + }); + + test('spacing-only injection next to an injection with content', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', inlineClassName: 'spacer', widthInEm: 1 }, + { content: 'AB', inlineClassName: 'text-class' } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [2, 5], + getBreakOffsets: () => [12], // 10 (original) + 0 + 2 + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + // The spacing-only injection must not shift the offsets of the injections that follow it. + assert.deepStrictEqual(result, [ + [ + new InlineDecoration(new Range(1, 3, 1, 3), 'spacer', InlineDecorationType.WidthOnly), + new InlineDecoration(new Range(1, 6, 1, 8), 'text-class', InlineDecorationType.Regular), + ] + ]); + }); + + test('spacing-only injection without inlineClassName produces no inline decorations', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', widthInEm: 1 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [5], + getBreakOffsets: () => [10], + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + assert.deepStrictEqual(result, [ + [] + ]); + }); }); diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 54e711f28ee03f..d87892ea85c6ed 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -221,15 +221,87 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { ]); // The injection counts as a single character column, so the tab at character column 3 expands - // to character column 4 and the whole line spans 10 columns. Were the 1.5em width to leak into - // the column accumulator, the injection would count as 3 columns and the tab would expand to - // character column 8 instead. + // to character column 4 and the whole line spans 10 columns. Its 1.5em is spent on the wrapping + // decision alone, which runs out of room at `g`. Were that width to leak into the column + // accumulator, the injection would count as 3 columns, the tab would expand to character + // column 8, and the line would already run out of room at `e`. assert.deepStrictEqual({ breakOffsets: lineBreakData?.breakOffsets, breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn }, { - breakOffsets: [4, 10], - breakOffsetsVisibleColumn: [4, 10] + breakOffsets: [8, 10], + breakOffsetsVisibleColumn: [8, 10] + }); + }); + + test('reserves width for a spacing-only injection at the beginning of a line', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 6, 2, WrappingIndent.None, 'normal', false, 'abcdef', null, [ + new LineInjectedText(0, 1, 1, { content: '', widthInEm: 1 }, 0) + ]); + + // 1em is two columns wide here, so the spacer pushes the last two characters onto a second + // line. It consumes no character, so the offsets still refer to the original text. + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [4, 6], + breakOffsetsVisibleColumn: [4, 6] + }); + }); + + test('lets a spacing-only injection in the middle of a line inherit surrounding break opportunities', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ' '); + const lineBreakData = getLineBreakData(factory, 4, 8, 2, WrappingIndent.None, 'normal', false, 'ab cdef', null, [ + new LineInjectedText(0, 1, 4, { content: '', widthInEm: 1 }, 0) + ]); + + // The spacer is transparent for break opportunities: it neither creates nor suppresses one, so + // the break after the space is still recorded even though the spacer sits on top of it, and the + // line breaks there rather than at `f` where it runs out of room. Its 1em is what makes the line + // run out of room at all: without it the seven characters fit in the eight available columns. + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [3, 7], + breakOffsetsVisibleColumn: [3, 7] + }); + }); + + test('ignores a spacing-only injection at the end of a line', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 6, 2, WrappingIndent.None, 'normal', false, 'abcdef', null, [ + new LineInjectedText(0, 1, 7, { content: '', widthInEm: 1 }, 0) + ]); + + // The line is exactly full, and there is no character after the spacer that could be moved to a + // following line, so the spacer cannot introduce a break. + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [6], + breakOffsetsVisibleColumn: [6] + }); + }); + + test('expands a tab following a spacing-only injection from the character column', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 6, 2, WrappingIndent.None, 'normal', false, 'ab\tcd', null, [ + new LineInjectedText(0, 1, 3, { content: '', widthInEm: 1 }, 0) + ]); + + // The spacer occupies no character column, so the tab still expands from character column 2 to + // character column 4. Were its 1em width to leak into the column accumulator, the tab would + // start at character column 4 and expand to character column 8 instead. + assert.deepStrictEqual({ + breakOffsets: lineBreakData?.breakOffsets, + breakOffsetsVisibleColumn: lineBreakData?.breakOffsetsVisibleColumn + }, { + breakOffsets: [3, 5], + breakOffsetsVisibleColumn: [4, 6] }); }); From 146580c89284069d6032fb498f209c09ef861ab8 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 19:21:38 +0200 Subject: [PATCH 31/41] polish --- .../viewModel/inlineDecorations.test.ts | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts index 39c5c5e9507aaf..1dce028e82fa81 100644 --- a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts @@ -507,25 +507,6 @@ suite('InjectedTextInlineDecorationsComputer', () => { ]); }); - test('spacing-only injection at the beginning of a line', () => { - const injectionOptions: InjectedTextOptions[] = [ - { content: '', inlineClassName: 'spacer', widthInEm: 1 } - ]; - const context: IInjectedTextInlineDecorationsComputerContext = { - getInjectionOptions: () => injectionOptions, - getInjectionOffsets: () => [0], - getBreakOffsets: () => [10], // the injection contributes no characters - getWrappedTextIndentLength: () => 0, - getBaseViewLineNumber: () => 1, - }; - const computer = new InjectedTextInlineDecorationsComputer(context); - const result = computer.getInlineDecorations(1); - // The decoration covers no character, so it is empty and starts where the injection sits. - assert.deepStrictEqual(result, [ - [new InlineDecoration(new Range(1, 1, 1, 1), 'spacer', InlineDecorationType.WidthOnly)] - ]); - }); - test('spacing-only injection in the middle of a line', () => { const injectionOptions: InjectedTextOptions[] = [ { content: '', inlineClassName: 'spacer', widthInEm: 1 } @@ -563,27 +544,6 @@ suite('InjectedTextInlineDecorationsComputer', () => { ]); }); - test('spacing-only injection sitting exactly on a line break moves to the next line', () => { - const injectionOptions: InjectedTextOptions[] = [ - { content: '', inlineClassName: 'spacer', widthInEm: 1 } - ]; - const context: IInjectedTextInlineDecorationsComputerContext = { - getInjectionOptions: () => injectionOptions, - getInjectionOffsets: () => [8], - getBreakOffsets: () => [8, 20], - getWrappedTextIndentLength: () => 0, - getBaseViewLineNumber: () => 1, - }; - const computer = new InjectedTextInlineDecorationsComputer(context); - const result = computer.getInlineDecorations(1); - // Injected text with content would also end up at the beginning of the second line, - // so a spacing-only injection must not be left dangling at the end of the first one. - assert.deepStrictEqual(result, [ - [], - [new InlineDecoration(new Range(2, 1, 2, 1), 'spacer', InlineDecorationType.WidthOnly)], - ]); - }); - test('spacing-only injection next to an injection with content', () => { const injectionOptions: InjectedTextOptions[] = [ { content: '', inlineClassName: 'spacer', widthInEm: 1 }, From 972ac33c27cf28527538571dffd837731665dd87 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 20:29:14 +0200 Subject: [PATCH 32/41] putting back inline progress nbsp --- src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts index 95ee9ae4fea4f3..e111516e10f821 100644 --- a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts +++ b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { disposableTimeout } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { noBreakWhitespace } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import './inlineProgressWidget.css'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js'; @@ -23,7 +24,7 @@ const inlineProgressDecoration = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, showIfCollapsed: true, after: { - content: '', + content: noBreakWhitespace, inlineClassName: 'inline-editor-progress-decoration', inlineClassNameAffectsLetterSpacing: true, widthInEm: 1, From eca03a17edf56aac18214eeecbbcd52a9a86d66e Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Wed, 26 Aug 2026 20:41:59 +0200 Subject: [PATCH 33/41] updating color decorator screenshots The color swatches use `content: ''`, so their `inline-block` aligns by its bottom margin edge instead of an inner text baseline. This shifts each swatch up by roughly half a pixel; the swatch body is otherwise unchanged. Update the four affected hashes. InlineProgress is untouched: restoring `noBreakWhitespace` there brought its rendering back to the committed screenshots, since `widthInEm` only feeds line-break computation and never reaches the DOM. Co-Authored-By: Claude Opus 5 --- test/componentFixtures/blocks-ci-screenshots.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index daa57c5c2e2c98..ebab2e893ee3d7 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -55,10 +55,10 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/89b2d50f5bd33feaa20b3c8f83f3d83548fda48b6f85cd7d784460aafaeeb596) #### editor/injectedTextDecorations/ColorDecorators/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/33584d1dce7a32853b0c3c269ea341c1346364d42f3acede0f39d055ec54ff03) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/b1f3d29ca20b8b47db526894171129248cb573ff3bdf0e9a900399edfeaaed5d) #### editor/injectedTextDecorations/ColorDecorators/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/96c71d6072b20e0b9a75089789af194cd503fce1585d013491a57613ede6c96d) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/4aea27d481bb767f11a744622c139a0a5a6a3ba2f2062cf5c16b3ff74b729a5c) #### editor/injectedTextDecorations/InlayHints/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/473d30b47283d6c5a10ca3e349c64564708a42898a78cee98ef0cfaa4ecd84d2) @@ -73,10 +73,10 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/4810c9ab74a193cc722de9f7d3fe7c3bffbf878d65da6c1b6e11cf4fb626ce1e) #### editor/injectedTextDecorations/SelectedColorDecorator/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/c2d6a7d8555ae65d180a8751365019f60b9859f62e620873959b802f667fe325) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/beb679f3f036cf6a697db444c041b3eb182653bf27037c55d4fed0aee6ec6bee) #### editor/injectedTextDecorations/SelectedColorDecorator/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/7f70224f7733a2461eba63fa98234aab38b8804a73460deffa11f49cd6f7172c) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/55b3e1130e0ce350be535161689f5e7314fbff0b712c1409a04bba70049ba7ab) #### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/7700bb9cad18d064af94493b4ae0a4f75e3c855df7ba4eb1d8a4a562eaa41dc6) From 9c5aca834724d777c50420d6c4d1f3e6dc039d86 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 27 Aug 2026 17:03:21 +0200 Subject: [PATCH 34/41] fix 'the wrapping algorithm now wraps immediately after color boxes' --- .../common/viewModel/inlineDecorations.ts | 4 +++- .../viewModel/inlineDecorations.test.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/viewModel/inlineDecorations.ts b/src/vs/editor/common/viewModel/inlineDecorations.ts index c1829eaf26296e..54d0ed6d8a6e6f 100644 --- a/src/vs/editor/common/viewModel/inlineDecorations.ts +++ b/src/vs/editor/common/viewModel/inlineDecorations.ts @@ -242,8 +242,10 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations const length = options.content.length; const injectedTextStartOffsetInInputWithInjections = injectionOffsets[currentInjectedOffset] + totalInjectedTextLengthBefore; const injectedTextEndOffsetInInputWithInjections = injectedTextStartOffsetInInputWithInjections + length; + const isWidthOnly = (length === 0 && options.widthInEm !== undefined); + const isAtWrapBoundary = injectedTextStartOffsetInInputWithInjections === lineEndOffsetInInputWithInjections && outputLineIndex < breakOffsets.length - 1; - if (injectedTextStartOffsetInInputWithInjections > lineEndOffsetInInputWithInjections) { + if (injectedTextStartOffsetInInputWithInjections > lineEndOffsetInInputWithInjections || (isWidthOnly && isAtWrapBoundary)) { // Injected text only starts in later wrapped lines. break; } diff --git a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts index 1dce028e82fa81..8e9336d0d3acfc 100644 --- a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts @@ -525,6 +525,25 @@ suite('InjectedTextInlineDecorationsComputer', () => { ]); }); + test('spacing-only injection at a wrap boundary belongs to the following line', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', inlineClassName: 'spacer', widthInEm: 1 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [5], + getBreakOffsets: () => [5, 10], + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + assert.deepStrictEqual(result, [ + [], + [new InlineDecoration(new Range(2, 1, 2, 1), 'spacer', InlineDecorationType.WidthOnly)] + ]); + }); + test('spacing-only injection at the end of a line', () => { const injectionOptions: InjectedTextOptions[] = [ { content: '', inlineClassName: 'spacer', widthInEm: 1 } From 557720b5ddbd57c54b1e736bb2fd99c20da58657 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 27 Aug 2026 17:18:00 +0200 Subject: [PATCH 35/41] fixing https://github.com/microsoft/vscode/pull/332340#discussion_r3872350727 --- .../common/viewModel/inlineDecorations.ts | 14 ++++---- .../viewModel/inlineDecorations.test.ts | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/vs/editor/common/viewModel/inlineDecorations.ts b/src/vs/editor/common/viewModel/inlineDecorations.ts index 54d0ed6d8a6e6f..5e8a25204e6444 100644 --- a/src/vs/editor/common/viewModel/inlineDecorations.ts +++ b/src/vs/editor/common/viewModel/inlineDecorations.ts @@ -243,17 +243,19 @@ export class InjectedTextInlineDecorationsComputer implements IInlineDecorations const injectedTextStartOffsetInInputWithInjections = injectionOffsets[currentInjectedOffset] + totalInjectedTextLengthBefore; const injectedTextEndOffsetInInputWithInjections = injectedTextStartOffsetInInputWithInjections + length; const isWidthOnly = (length === 0 && options.widthInEm !== undefined); - const isAtWrapBoundary = injectedTextStartOffsetInInputWithInjections === lineEndOffsetInInputWithInjections && outputLineIndex < breakOffsets.length - 1; + const isLastOutputLine = outputLineIndex === breakOffsets.length - 1; + const isAtInternalWrapBoundary = injectedTextStartOffsetInInputWithInjections === lineEndOffsetInInputWithInjections && !isLastOutputLine; - if (injectedTextStartOffsetInInputWithInjections > lineEndOffsetInInputWithInjections || (isWidthOnly && isAtWrapBoundary)) { + if (injectedTextStartOffsetInInputWithInjections > lineEndOffsetInInputWithInjections || (isWidthOnly && isAtInternalWrapBoundary)) { // Injected text only starts in later wrapped lines. break; } - // Injected text that only reserves horizontal space covers no character, so it cannot be - // contained in a wrapped line the way injected text with content is. - const isWidthOnly = (length === 0 && options.widthInEm !== undefined); - if (lineStartOffsetInInputWithInjections < injectedTextEndOffsetInInputWithInjections) { + const isInLine = isWidthOnly + ? lineStartOffsetInInputWithInjections <= injectedTextStartOffsetInInputWithInjections + && (injectedTextStartOffsetInInputWithInjections < lineEndOffsetInInputWithInjections || isLastOutputLine) + : lineStartOffsetInInputWithInjections < injectedTextEndOffsetInInputWithInjections; + if (isInLine) { // Injected text ends after or in this line (but also starts in or before this line). if (options.inlineClassName) { const wrappedTextIndentLength = this.context.getWrappedTextIndentLength(modelLineNumber); diff --git a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts index 8e9336d0d3acfc..28b01611eb066c 100644 --- a/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/inlineDecorations.test.ts @@ -525,6 +525,42 @@ suite('InjectedTextInlineDecorationsComputer', () => { ]); }); + test('spacing-only injection at the beginning of a line', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', inlineClassName: 'spacer', widthInEm: 1 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [0], + getBreakOffsets: () => [10], + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + assert.deepStrictEqual(result, [ + [new InlineDecoration(new Range(1, 1, 1, 1), 'spacer', InlineDecorationType.WidthOnly)] + ]); + }); + + test('spacing-only injection on an empty line', () => { + const injectionOptions: InjectedTextOptions[] = [ + { content: '', inlineClassName: 'spacer', widthInEm: 1 } + ]; + const context: IInjectedTextInlineDecorationsComputerContext = { + getInjectionOptions: () => injectionOptions, + getInjectionOffsets: () => [0], + getBreakOffsets: () => [0], + getWrappedTextIndentLength: () => 0, + getBaseViewLineNumber: () => 1, + }; + const computer = new InjectedTextInlineDecorationsComputer(context); + const result = computer.getInlineDecorations(1); + assert.deepStrictEqual(result, [ + [new InlineDecoration(new Range(1, 1, 1, 1), 'spacer', InlineDecorationType.WidthOnly)] + ]); + }); + test('spacing-only injection at a wrap boundary belongs to the following line', () => { const injectionOptions: InjectedTextOptions[] = [ { content: '', inlineClassName: 'spacer', widthInEm: 1 } From 56a4122dfae09ee319faa505c7b354784bc26368 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 27 Aug 2026 17:36:42 +0200 Subject: [PATCH 36/41] fixing https://github.com/microsoft/vscode/pull/332340#discussion_r3872350832 --- .../editor/common/viewModel/monospaceLineBreaksComputer.ts | 7 +++++-- .../common/viewModel/monospaceLineBreaksComputer.test.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index acf1b0469f1c26..9c280d73d2a43b 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -390,7 +390,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st } const isKeepAll = (wordBreak === 'keepAll'); - const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent); + const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent, fixedWidthRanges); // The wrapping decision is taken in pixels, because injected text can request an arbitrary // width via `widthInEm` which does not map to a whole number of columns. @@ -597,7 +597,7 @@ function canBreak(prevCharCode: number, prevCharCodeClass: CharacterClass, charC ); } -function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): number { +function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, fixedWidthRanges?: readonly FixedWidthInjectedTextRange[]): number { let wrappedTextIndentLength = 0; if (wrappingIndent !== WrappingIndent.None) { const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); @@ -605,6 +605,9 @@ function computeWrappedTextIndentLength(lineText: string, tabSize: number, first // Track existing indent for (let i = 0; i < firstNonWhitespaceIndex; i++) { + if (fixedWidthRanges?.[0]?.startOffset === i) { + break; + } const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1); wrappedTextIndentLength += charWidth; } diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index d87892ea85c6ed..25082f80cd7cf1 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -316,7 +316,7 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { wrappedTextIndentLength: lineBreakData?.wrappedTextIndentLength }, { breakOffsets: [4, 7], - wrappedTextIndentLength: 1 + wrappedTextIndentLength: 0 }); }); From 661ec8689ca077474d5b7ee4bbc1a45a642bc436 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 27 Aug 2026 17:44:29 +0200 Subject: [PATCH 37/41] fixing https://github.com/microsoft/vscode/pull/332340#discussion_r3872350905 --- src/vs/editor/browser/view/domLineBreaksComputer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 255347c480cb3b..abac9d8a123d1f 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -250,7 +250,7 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: // its own. Rendering it inside the span of the character below would make that character fixed // width as well. Several such injections can sit at the same offset. while (fixedWidthRange && fixedWidthRange.startOffset === charIndex && fixedWidthRange.endOffset === charIndex) { - sb.appendString(''); sb.appendString(''); @@ -261,7 +261,7 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: // otherwise. At most one such range can start here: injections at the same column are laid // out one after the other, so only an empty one leaves the next starting at the same offset. if (fixedWidthRange && fixedWidthRange.startOffset === charIndex) { - sb.appendString(''); } else { From 92eac286383bceb0b47175734cb708638c9604c4 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 27 Aug 2026 18:09:43 +0200 Subject: [PATCH 38/41] align the color decorator bottom --- .../editor/contrib/colorPicker/browser/colorPicker.css | 9 ++++++++- test/componentFixtures/blocks-ci-screenshots.md | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css index 110aa63a08e69c..e5573894e0b699 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css +++ b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css @@ -23,6 +23,13 @@ cursor: pointer; } +/* The injected text is empty, so this box has no line box of its own and would align by its bottom margin edge. +An empty inline-block child restores an in-flow line box, so the box keeps the font's strut baseline. */ +.colorpicker-color-decoration::before { + content: ''; + display: inline-block; +} + .hc-black .colorpicker-color-decoration, .vs-dark .colorpicker-color-decoration { border: solid 0.1em #eee; @@ -206,6 +213,6 @@ cursor: pointer; } -.colorpicker-body .insert-button:hover{ +.colorpicker-body .insert-button:hover { background: var(--vscode-button-hoverBackground); } diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md index ebab2e893ee3d7..daa57c5c2e2c98 100644 --- a/test/componentFixtures/blocks-ci-screenshots.md +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -55,10 +55,10 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/89b2d50f5bd33feaa20b3c8f83f3d83548fda48b6f85cd7d784460aafaeeb596) #### editor/injectedTextDecorations/ColorDecorators/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/b1f3d29ca20b8b47db526894171129248cb573ff3bdf0e9a900399edfeaaed5d) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/33584d1dce7a32853b0c3c269ea341c1346364d42f3acede0f39d055ec54ff03) #### editor/injectedTextDecorations/ColorDecorators/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/4aea27d481bb767f11a744622c139a0a5a6a3ba2f2062cf5c16b3ff74b729a5c) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/96c71d6072b20e0b9a75089789af194cd503fce1585d013491a57613ede6c96d) #### editor/injectedTextDecorations/InlayHints/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/473d30b47283d6c5a10ca3e349c64564708a42898a78cee98ef0cfaa4ecd84d2) @@ -73,10 +73,10 @@ ![screenshot](https://hediet-screenshots.azurewebsites.net/images/4810c9ab74a193cc722de9f7d3fe7c3bffbf878d65da6c1b6e11cf4fb626ce1e) #### editor/injectedTextDecorations/SelectedColorDecorator/Dark -![screenshot](https://hediet-screenshots.azurewebsites.net/images/beb679f3f036cf6a697db444c041b3eb182653bf27037c55d4fed0aee6ec6bee) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/c2d6a7d8555ae65d180a8751365019f60b9859f62e620873959b802f667fe325) #### editor/injectedTextDecorations/SelectedColorDecorator/Light -![screenshot](https://hediet-screenshots.azurewebsites.net/images/55b3e1130e0ce350be535161689f5e7314fbff0b712c1409a04bba70049ba7ab) +![screenshot](https://hediet-screenshots.azurewebsites.net/images/7f70224f7733a2461eba63fa98234aab38b8804a73460deffa11f49cd6f7172c) #### editor/inlineChatZoneWidget/InlineChatZoneWidget/Dark ![screenshot](https://hediet-screenshots.azurewebsites.net/images/7700bb9cad18d064af94493b4ae0a4f75e3c855df7ba4eb1d8a4a562eaa41dc6) From d5a1f60e0766325c47acd1cba71468d33a0a3bc0 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Thu, 27 Aug 2026 18:14:14 +0200 Subject: [PATCH 39/41] restoring non breaking space --- src/vs/editor/contrib/colorPicker/browser/colorDetector.ts | 3 ++- src/vs/editor/contrib/colorPicker/browser/colorPicker.css | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index d0bda30a282268..a605d3c978965b 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -9,6 +9,7 @@ import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; +import { noBreakWhitespace } from '../../../../base/common/strings.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { DynamicCssRules } from '../../../browser/editorDom.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; @@ -238,7 +239,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { options: { description: 'colorDetector', before: { - content: '', + content: noBreakWhitespace, inlineClassName: `${ref.className} colorpicker-color-decoration`, inlineClassNameAffectsLetterSpacing: true, attachedData: ColorDecorationInjectedTextMarker, diff --git a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css index e5573894e0b699..7f09b70cb98b08 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorPicker.css +++ b/src/vs/editor/contrib/colorPicker/browser/colorPicker.css @@ -23,13 +23,6 @@ cursor: pointer; } -/* The injected text is empty, so this box has no line box of its own and would align by its bottom margin edge. -An empty inline-block child restores an in-flow line box, so the box keeps the font's strut baseline. */ -.colorpicker-color-decoration::before { - content: ''; - display: inline-block; -} - .hc-black .colorpicker-color-decoration, .vs-dark .colorpicker-color-decoration { border: solid 0.1em #eee; From 16cc4c71daca1ca4ea50fc513abb3a10e6817a9e Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 28 Aug 2026 11:28:52 +0200 Subject: [PATCH 40/41] checking is escaped line break at offset charStartOffset --- .../common/viewModel/monospaceLineBreaksComputer.ts | 2 +- .../common/viewModel/monospaceLineBreaksComputer.test.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 9c280d73d2a43b..72f3f248d3d872 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -465,7 +465,7 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st } // literal \n shall trigger a softwrap - if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, i)) { + if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, charStartOffset)) { breakOffset = charStartOffset; breakOffsetVisibleColumn = visibleColumn; breakOffsetPixelWidth = currentLinePixelWidth; diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 25082f80cd7cf1..a1771b124c2a48 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -165,6 +165,15 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { }); }); + test('wraps after an escaped line feed followed by fixed-width injected text', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 100, 2, WrappingIndent.None, 'normal', true, '"a\\nb"', null, [ + new LineInjectedText(0, 1, 5, { content: 'hint', widthInEm: 1 }, 0) + ]); + + assert.deepStrictEqual(lineBreakData?.breakOffsets, [4, 10]); + }); + test('treats adjacent fixed-width injected texts as separate atomic spans', () => { const factory = new MonospaceLineBreaksComputerFactory('', ''); const lineBreakData = getLineBreakData(factory, 4, 5, 2, WrappingIndent.None, 'normal', false, 'abcdef', null, [ From c7060f463a4adfcfa56a71109f8d63e9a1b830e8 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 28 Aug 2026 12:00:51 +0200 Subject: [PATCH 41/41] fixing 'Oversized width-only injected text creates an empty output line' --- .../viewModel/monospaceLineBreaksComputer.ts | 16 +++++++++++----- .../monospaceLineBreaksComputer.test.ts | 11 +++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 72f3f248d3d872..08826554820276 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -490,11 +490,17 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st breakOffsetPixelWidth = currentLinePixelWidth - charPixelWidth; } - breakingOffsets[breakingOffsetsCount] = breakOffset; - breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; - breakingOffsetsCount++; - breakingPixelWidth = breakOffsetPixelWidth + wrappedLineBreakPixelWidth; - breakOffset = 0; + const currentLineStartOffset = breakingOffsetsCount > 0 ? breakingOffsets[breakingOffsetsCount - 1] : 0; + if (breakOffset > currentLineStartOffset) { + // Breaking at the start of the current output line would emit an empty line, which + // happens when an oversized leading width-only injection cannot fit before the first + // character. In that case keep the content on the current line and defer the break. + breakingOffsets[breakingOffsetsCount] = breakOffset; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; + breakingOffsetsCount++; + breakingPixelWidth = breakOffsetPixelWidth + wrappedLineBreakPixelWidth; + breakOffset = 0; + } } prevCharCode = charCode; diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index a1771b124c2a48..70019f302fea89 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -260,6 +260,17 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { }); }); + test('does not create empty lines for oversized spacing-only injected text', () => { + const factory = new MonospaceLineBreaksComputerFactory('', ''); + const lineBreakData = getLineBreakData(factory, 4, 2, 2, WrappingIndent.None, 'normal', false, 'abc', null, [ + new LineInjectedText(0, 1, 1, { content: '', widthInEm: 2 }, 0) + ]); + + // The spacer alone already overflows the wrap width, but there is no character before it to + // wrap, so it must share the first line with `a` rather than emit an empty leading line. + assert.deepStrictEqual(lineBreakData?.breakOffsets, [1, 3]); + }); + test('lets a spacing-only injection in the middle of a line inherit surrounding break opportunities', () => { const factory = new MonospaceLineBreaksComputerFactory('', ' '); const lineBreakData = getLineBreakData(factory, 4, 8, 2, WrappingIndent.None, 'normal', false, 'ab cdef', null, [