Skip to content

Commit 227803b

Browse files
aiday-marCopilotalexdimaclaude
authored
Adding widthInEm field to injected text options (#332340)
* 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * 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> * Fix fixed-width injected text rendering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * wip * wip * review changes * wip * removing rendering code * wip * polishing * polishing * updating screenshots * Remove injected text component fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * checking widthInEm is defined, finite and positive * removing change to inlayhintscontroller * making firstFixedWidthRange as being potentially undefined more explicit * adding check on fixedWidthRangeIndex index * using one accumulator * using pixels for wrapping * allowing to set empty content * polish * putting back inline progress nbsp * 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 <noreply@anthropic.com> * fix 'the wrapping algorithm now wraps immediately after color boxes' * fixing #332340 (comment) * fixing #332340 (comment) * fixing #332340 (comment) * align the color decorator bottom * restoring non breaking space * checking is escaped line break at offset charStartOffset * fixing 'Oversized width-only injected text creates an empty output line' --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Alexandru Dima <alex.dima@outlook.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 563e142 commit 227803b

19 files changed

Lines changed: 764 additions & 85 deletions

File tree

build/lib/stylelint/vscode-known-variables.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,8 @@
11161116
"--scroll-shadow-surface",
11171117
"--vscode-chat-list-background",
11181118
"--vscode-chat-persistent-content-height",
1119+
"--vscode-colorPicker-colorDecoratorMargin",
1120+
"--vscode-colorPicker-colorDecoratorWidth",
11191121
"--vscode-editorCodeLens-fontFamily",
11201122
"--vscode-editorCodeLens-fontFamilyDefault",
11211123
"--vscode-editorCodeLens-fontFeatureSettings",

src/vs/editor/browser/view/domLineBreaksComputer.ts

Lines changed: 113 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { WrappingIndent } from '../../common/config/editorOptions.js';
1212
import { StringBuilder } from '../../common/core/stringBuilder.js';
1313
import { InjectedTextOptions } from '../../common/model.js';
1414
import { ILineBreaksComputer, ILineBreaksComputerContext, ILineBreaksComputerFactory, ModelLineProjectionData } from '../../common/modelLineProjectionData.js';
15-
import { LineInjectedText } from '../../common/textModelEvents.js';
15+
import { FixedWidthInjectedTextRange, LineInjectedText } from '../../common/textModelEvents.js';
1616
import { FontInfo } from '../../common/config/fontInfo.js';
1717

1818
const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value });
@@ -78,10 +78,13 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont
7878
const wrappedTextIndentLengths: number[] = [];
7979
const renderLineContents: string[] = [];
8080
const allCharOffsets: number[][] = [];
81+
const allSpanStartOffsets: number[][] = [];
8182
const allVisibleColumns: number[][] = [];
8283
for (let i = 0; i < lineNumbers.length; i++) {
8384
const lineNumber = lineNumbers[i];
84-
const lineContent = LineInjectedText.applyInjectedText(context.getLineContent(lineNumber), context.getLineInjectedText(lineNumber));
85+
const injectedTexts = context.getLineInjectedText(lineNumber);
86+
const lineContent = LineInjectedText.applyInjectedText(context.getLineContent(lineNumber), injectedTexts);
87+
const fixedWidthRanges = LineInjectedText.getFixedWidthInjectedTextRanges(injectedTexts);
8588

8689
let firstNonWhitespaceIndex = 0;
8790
let wrappedTextIndentLength = 0;
@@ -97,14 +100,20 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont
97100
// Track existing indent
98101

99102
for (let i = 0; i < firstNonWhitespaceIndex; i++) {
100-
const charWidth = (
101-
lineContent.charCodeAt(i) === CharCode.Tab
102-
? (tabSize - (wrappedTextIndentLength % tabSize))
103-
: 1
104-
);
105-
wrappedTextIndentLength += charWidth;
103+
const fixedWidthRange = fixedWidthRanges[0];
104+
const isFixedWidthStart = fixedWidthRange && fixedWidthRange.startOffset === i;
105+
if (isFixedWidthStart) {
106+
firstNonWhitespaceIndex = i;
107+
break;
108+
} else {
109+
const charWidth = (
110+
lineContent.charCodeAt(i) === CharCode.Tab
111+
? (tabSize - (wrappedTextIndentLength % tabSize))
112+
: 1
113+
);
114+
wrappedTextIndentLength += charWidth;
115+
}
106116
}
107-
108117
const indentWidth = Math.ceil(fontInfo.spaceWidth * wrappedTextIndentLength);
109118

110119
// Force sticking to beginning of line if no character would fit except for the indentation
@@ -118,11 +127,19 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont
118127
}
119128

120129
const renderLineContent = lineContent.substr(firstNonWhitespaceIndex);
121-
const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength);
130+
const shiftedFixedWidthRanges = firstNonWhitespaceIndex === 0
131+
? fixedWidthRanges
132+
: fixedWidthRanges.map(range => ({
133+
startOffset: Math.max(0, range.startOffset - firstNonWhitespaceIndex),
134+
endOffset: range.endOffset - firstNonWhitespaceIndex,
135+
widthInEm: range.widthInEm
136+
}));
137+
const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength, shiftedFixedWidthRanges);
122138
firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex;
123139
wrappedTextIndentLengths[i] = wrappedTextIndentLength;
124140
renderLineContents[i] = renderLineContent;
125141
allCharOffsets[i] = tmp[0];
142+
allSpanStartOffsets[i] = tmp[2];
126143
allVisibleColumns[i] = tmp[1];
127144
}
128145
const html = sb.build();
@@ -149,7 +166,7 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont
149166
for (let i = 0; i < lineNumbers.length; i++) {
150167
const lineNumber = lineNumbers[i];
151168
const lineDomNode = lineDomNodes[i];
152-
const breakOffsets: number[] | null = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i]);
169+
const breakOffsets: number[] | null = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i], allSpanStartOffsets[i]);
153170
if (breakOffsets === null) {
154171
result[i] = createEmptyLineBreakWithPossiblyInjectedText(lineNumber);
155172
continue;
@@ -193,7 +210,7 @@ const enum Constants {
193210
SPAN_MODULO_LIMIT = 16384
194211
}
195212

196-
function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number): [number[], number[]] {
213+
function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: number, width: number, sb: StringBuilder, wrappingIndentLength: number, fixedWidthRanges: readonly FixedWidthInjectedTextRange[]): [number[], number[], number[]] {
197214

198215
if (wrappingIndentLength !== 0) {
199216
const hangingOffset = String(wrappingIndentLength);
@@ -214,14 +231,51 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize:
214231
const len = lineContent.length;
215232
let visibleColumn = initialVisibleColumn;
216233
let charOffset = 0;
234+
let fixedWidthRangeIndex = 0;
217235
const charOffsets: number[] = [];
236+
const spanStartOffsets: number[] = [0];
218237
const visibleColumns: number[] = [];
219238
let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : CharCode.Null);
239+
let spanOpen = true;
220240

221241
sb.appendString('<span>');
222242
for (let charIndex = 0; charIndex < len; charIndex++) {
223-
if (charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) {
243+
let fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex];
244+
const startsFixedWidth = fixedWidthRange && fixedWidthRange.startOffset === charIndex;
245+
if (startsFixedWidth) {
246+
if (spanOpen) {
247+
sb.appendString('</span>');
248+
}
249+
// Injected text that only reserves horizontal space covers no character, so it gets a span of
250+
// its own. Rendering it inside the span of the character below would make that character fixed
251+
// width as well. Several such injections can sit at the same offset.
252+
while (fixedWidthRange && fixedWidthRange.startOffset === charIndex && fixedWidthRange.endOffset === charIndex) {
253+
sb.appendString('<span style="display:inline-block;box-sizing:border-box;white-space:nowrap;width:');
254+
sb.appendString(String(fixedWidthRange.widthInEm));
255+
sb.appendString('em;">');
256+
sb.appendString('</span>');
257+
spanStartOffsets.push(charOffset);
258+
fixedWidthRange = fixedWidthRanges[++fixedWidthRangeIndex];
259+
}
260+
// The character below goes into a fixed width span if one still covers it, a normal one
261+
// otherwise. At most one such range can start here: injections at the same column are laid
262+
// out one after the other, so only an empty one leaves the next starting at the same offset.
263+
if (fixedWidthRange && fixedWidthRange.startOffset === charIndex) {
264+
sb.appendString('<span style="display:inline-block;box-sizing:border-box;white-space:nowrap;width:');
265+
sb.appendString(String(fixedWidthRange.widthInEm));
266+
sb.appendString('em;">');
267+
} else {
268+
sb.appendString('<span>');
269+
}
270+
spanStartOffsets.push(charOffset);
271+
spanOpen = true;
272+
} else if (!spanOpen) {
273+
sb.appendString('<span>');
274+
spanStartOffsets.push(charOffset);
275+
spanOpen = true;
276+
} else if ((!fixedWidthRange || charIndex < fixedWidthRange.startOffset) && charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) {
224277
sb.appendString('</span><span>');
278+
spanStartOffsets.push(charOffset);
225279
}
226280
charOffsets[charIndex] = charOffset;
227281
visibleColumns[charIndex] = visibleColumn;
@@ -286,26 +340,38 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize:
286340

287341
charOffset += producedCharacters;
288342
visibleColumn += charWidth;
343+
344+
// A range that covers no character has already been closed above, and must not be consumed here:
345+
// its `endOffset` equals its `startOffset`, so this condition would hold one character too early.
346+
if (fixedWidthRange && fixedWidthRange.startOffset < fixedWidthRange.endOffset && charIndex + 1 === fixedWidthRange.endOffset) {
347+
sb.appendString('</span>');
348+
spanOpen = false;
349+
fixedWidthRangeIndex++;
350+
}
351+
}
352+
if (spanOpen) {
353+
sb.appendString('</span>');
289354
}
290-
sb.appendString('</span>');
355+
// A spacing-only injection at the very end of the line is left out on purpose: nothing follows it,
356+
// so it cannot move a break point. `MonospaceLineBreaksComputer` ignores it for the same reason.
291357

292358
charOffsets[lineContent.length] = charOffset;
293359
visibleColumns[lineContent.length] = visibleColumn;
294360

295361
sb.appendString('</div>');
296362

297-
return [charOffsets, visibleColumns];
363+
return [charOffsets, visibleColumns, spanStartOffsets];
298364
}
299365

300-
function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[]): number[] | null {
366+
function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[], spanStartOffsets: number[]): number[] | null {
301367
if (lineContent.length <= 1) {
302368
return null;
303369
}
304370
const spans = <HTMLSpanElement[]>Array.prototype.slice.call(lineDomNode.children, 0);
305371

306372
const breakOffsets: number[] = [];
307373
try {
308-
discoverBreaks(range, spans, charOffsets, 0, null, lineContent.length - 1, null, breakOffsets);
374+
discoverBreaks(range, spans, charOffsets, spanStartOffsets, 0, null, lineContent.length - 1, null, breakOffsets);
309375
} catch (err) {
310376
console.error(err);
311377
return null;
@@ -319,13 +385,13 @@ function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent:
319385
return breakOffsets;
320386
}
321387

322-
function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: number[], low: number, lowRects: DOMRectList | null, high: number, highRects: DOMRectList | null, result: number[]): void {
388+
function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: number[], spanStartOffsets: number[], low: number, lowRects: DOMRectList | null, high: number, highRects: DOMRectList | null, result: number[]): void {
323389
if (low === high) {
324390
return;
325391
}
326392

327-
lowRects = lowRects || readClientRect(range, spans, charOffsets[low], charOffsets[low + 1]);
328-
highRects = highRects || readClientRect(range, spans, charOffsets[high], charOffsets[high + 1]);
393+
lowRects = lowRects || readClientRect(range, spans, charOffsets[low], charOffsets[low + 1], spanStartOffsets);
394+
highRects = highRects || readClientRect(range, spans, charOffsets[high], charOffsets[high + 1], spanStartOffsets);
329395

330396
if (Math.abs(lowRects[0].top - highRects[0].top) <= 0.1) {
331397
// same line
@@ -340,13 +406,34 @@ function discoverBreaks(range: Range, spans: HTMLSpanElement[], charOffsets: num
340406
}
341407

342408
const mid = low + ((high - low) / 2) | 0;
343-
const midRects = readClientRect(range, spans, charOffsets[mid], charOffsets[mid + 1]);
344-
discoverBreaks(range, spans, charOffsets, low, lowRects, mid, midRects, result);
345-
discoverBreaks(range, spans, charOffsets, mid, midRects, high, highRects, result);
409+
const midRects = readClientRect(range, spans, charOffsets[mid], charOffsets[mid + 1], spanStartOffsets);
410+
discoverBreaks(range, spans, charOffsets, spanStartOffsets, low, lowRects, mid, midRects, result);
411+
discoverBreaks(range, spans, charOffsets, spanStartOffsets, mid, midRects, high, highRects, result);
346412
}
347413

348-
function readClientRect(range: Range, spans: HTMLSpanElement[], startOffset: number, endOffset: number): DOMRectList {
349-
range.setStart(spans[(startOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, startOffset % Constants.SPAN_MODULO_LIMIT);
350-
range.setEnd(spans[(endOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, endOffset % Constants.SPAN_MODULO_LIMIT);
414+
function readClientRect(range: Range, spans: HTMLSpanElement[], startOffset: number, endOffset: number, spanStartOffsets: number[]): DOMRectList {
415+
if (!spanStartOffsets) {
416+
range.setStart(spans[(startOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, startOffset % Constants.SPAN_MODULO_LIMIT);
417+
range.setEnd(spans[(endOffset / Constants.SPAN_MODULO_LIMIT) | 0].firstChild!, endOffset % Constants.SPAN_MODULO_LIMIT);
418+
return range.getClientRects();
419+
}
420+
const startSpanIndex = findSpanIndex(spanStartOffsets, startOffset);
421+
const endSpanIndex = findSpanIndex(spanStartOffsets, endOffset);
422+
range.setStart(spans[startSpanIndex].firstChild!, startOffset - spanStartOffsets[startSpanIndex]);
423+
range.setEnd(spans[endSpanIndex].firstChild!, endOffset - spanStartOffsets[endSpanIndex]);
351424
return range.getClientRects();
352425
}
426+
427+
function findSpanIndex(spanStartOffsets: readonly number[], offset: number): number {
428+
let low = 0;
429+
let high = spanStartOffsets.length;
430+
while (low < high) {
431+
const mid = (low + high) >>> 1;
432+
if (spanStartOffsets[mid] <= offset) {
433+
low = mid + 1;
434+
} else {
435+
high = mid;
436+
}
437+
}
438+
return low - 1;
439+
}

src/vs/editor/common/model.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,12 @@ export interface InjectedTextOptions {
347347
*/
348348
readonly inlineClassNameAffectsLetterSpacing?: boolean;
349349

350+
/**
351+
* Sets the width used to wrap this injected text in editor-font em units.
352+
* @internal
353+
*/
354+
readonly widthInEm?: number;
355+
350356
/**
351357
* This field allows to attach data to this injected text.
352358
* The data can be read when injected texts at a given position are queried.

src/vs/editor/common/model/textModel.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2463,6 +2463,7 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt
24632463
public readonly tokens: TokenArray | null;
24642464
readonly inlineClassName: string | null;
24652465
readonly inlineClassNameAffectsLetterSpacing: boolean;
2466+
readonly widthInEm: number | undefined;
24662467
readonly attachedData: unknown | null;
24672468
readonly cursorStops: model.InjectedTextCursorStops | null;
24682469

@@ -2471,6 +2472,7 @@ export class ModelDecorationInjectedTextOptions implements model.InjectedTextOpt
24712472
this.tokens = options.tokens ?? null;
24722473
this.inlineClassName = options.inlineClassName || null;
24732474
this.inlineClassNameAffectsLetterSpacing = options.inlineClassNameAffectsLetterSpacing || false;
2475+
this.widthInEm = options.widthInEm !== undefined && Number.isFinite(options.widthInEm) && options.widthInEm >= 0 ? options.widthInEm : undefined;
24742476
this.attachedData = options.attachedData || null;
24752477
this.cursorStops = options.cursorStops || null;
24762478
}

src/vs/editor/common/textModelEvents.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,24 @@ export class ModelRawFlush {
234234
public readonly changeType = RawContentChangedType.Flush;
235235
}
236236

237+
/**
238+
* Represents a fixed-width injected text range within a line.
239+
* @internal
240+
*/
241+
export interface FixedWidthInjectedTextRange {
242+
readonly startOffset: number;
243+
readonly endOffset: number;
244+
readonly widthInEm: number;
245+
}
246+
247+
/**
248+
* Whether injected text takes up space on a line, either through its content or, when it is
249+
* width-only (e.g. `{ content: '', widthInEm: 1 }`), through the horizontal space it reserves.
250+
*/
251+
function occupiesHorizontalSpace(options: InjectedTextOptions): boolean {
252+
return options.content.length > 0 || (options.widthInEm !== undefined && options.widthInEm > 0);
253+
}
254+
237255
/**
238256
* Represents text injected on a line
239257
* @internal
@@ -257,7 +275,7 @@ export class LineInjectedText {
257275
public static fromDecorations(decorations: IModelDecoration[]): LineInjectedText[] {
258276
const result: LineInjectedText[] = [];
259277
for (const decoration of decorations) {
260-
if (decoration.options.before && decoration.options.before.content.length > 0) {
278+
if (decoration.options.before && occupiesHorizontalSpace(decoration.options.before)) {
261279
result.push(new LineInjectedText(
262280
decoration.ownerId,
263281
decoration.range.startLineNumber,
@@ -266,7 +284,7 @@ export class LineInjectedText {
266284
0,
267285
));
268286
}
269-
if (decoration.options.after && decoration.options.after.content.length > 0) {
287+
if (decoration.options.after && occupiesHorizontalSpace(decoration.options.after)) {
270288
result.push(new LineInjectedText(
271289
decoration.ownerId,
272290
decoration.range.endLineNumber,
@@ -288,6 +306,32 @@ export class LineInjectedText {
288306
return result;
289307
}
290308

309+
/**
310+
* The ranges of `applyInjectedText(...)` that are rendered at a fixed width. Width-only injected
311+
* text produces an empty range (`startOffset === endOffset`) which reserves horizontal space
312+
* without covering any character.
313+
*
314+
* `injectedTexts` must be sorted by column, which is what `fromDecorations` produces and what
315+
* `applyInjectedText` already requires. The result is then sorted by `startOffset` and never
316+
* overlaps: injections at the same column are laid out one after the other, so only an injection
317+
* with empty content leaves the next one starting at the same offset.
318+
*/
319+
public static getFixedWidthInjectedTextRanges(injectedTexts: readonly LineInjectedText[] | null): FixedWidthInjectedTextRange[] {
320+
const result: FixedWidthInjectedTextRange[] = [];
321+
let injectedTextLength = 0;
322+
for (const injectedText of injectedTexts ?? []) {
323+
const length = injectedText.options.content.length;
324+
const startOffset = injectedText.column - 1 + injectedTextLength;
325+
const endOffset = startOffset + length;
326+
const widthInEm = injectedText.options.widthInEm;
327+
if (widthInEm !== undefined) {
328+
result.push({ startOffset, endOffset, widthInEm });
329+
}
330+
injectedTextLength += length;
331+
}
332+
return result;
333+
}
334+
291335
constructor(
292336
public readonly ownerId: number,
293337
public readonly lineNumber: number,

src/vs/editor/common/viewLayout/lineDecorations.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ export class LineDecoration {
9292
}
9393

9494
private static _typeCompare(a: InlineDecorationType, b: InlineDecorationType): number {
95-
const ORDER = [2, 0, 1, 3];
95+
// WidthOnly, Before, After, Regular, RegularAffectingLetterSpacing.
96+
// Width only decorations come from injected text, which renders before any other decoration.
97+
const ORDER = [3, 1, 2, 4, 0];
9698
return ORDER[a] - ORDER[b];
9799
}
98100

0 commit comments

Comments
 (0)