diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json
index 79a40ba40876f..be4da5877daaf 100644
--- a/build/lib/stylelint/vscode-known-variables.json
+++ b/build/lib/stylelint/vscode-known-variables.json
@@ -1115,6 +1115,8 @@
"--scroll-shadow-surface",
"--vscode-chat-list-background",
"--vscode-chat-persistent-content-height",
+ "--vscode-colorPicker-colorDecoratorMargin",
+ "--vscode-colorPicker-colorDecoratorWidth",
"--vscode-editorCodeLens-fontFamily",
"--vscode-editorCodeLens-fontFamilyDefault",
"--vscode-editorCodeLens-fontFeatureSettings",
diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts
index 1c88653206fac..abac9d8a123d1 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 { FixedWidthInjectedTextRange, LineInjectedText } 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[][] = [];
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.getFixedWidthInjectedTextRanges(injectedTexts);
let firstNonWhitespaceIndex = 0;
let wrappedTextIndentLength = 0;
@@ -97,14 +100,20 @@ function createLineBreaks(targetWindow: Window, context: ILineBreaksComputerCont
// Track existing indent
for (let i = 0; i < firstNonWhitespaceIndex; i++) {
- const charWidth = (
- lineContent.charCodeAt(i) === CharCode.Tab
- ? (tabSize - (wrappedTextIndentLength % tabSize))
- : 1
- );
- wrappedTextIndentLength += charWidth;
+ const fixedWidthRange = fixedWidthRanges[0];
+ const isFixedWidthStart = fixedWidthRange && fixedWidthRange.startOffset === i;
+ if (isFixedWidthStart) {
+ firstNonWhitespaceIndex = i;
+ break;
+ } else {
+ 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
@@ -118,11 +127,19 @@ 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
+ }));
+ const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength, shiftedFixedWidthRanges);
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 +166,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 +210,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, fixedWidthRanges: readonly FixedWidthInjectedTextRange[]): [number[], number[], number[]] {
if (wrappingIndentLength !== 0) {
const hangingOffset = String(wrappingIndentLength);
@@ -214,14 +231,51 @@ 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[] = [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++) {
- if (charIndex !== 0 && charIndex % Constants.SPAN_MODULO_LIMIT === 0) {
+ let fixedWidthRange = fixedWidthRanges[fixedWidthRangeIndex];
+ const startsFixedWidth = fixedWidthRange && fixedWidthRange.startOffset === charIndex;
+ if (startsFixedWidth) {
+ if (spanOpen) {
+ 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) {
+ 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;
@@ -286,18 +340,30 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize:
charOffset += producedCharacters;
visibleColumn += charWidth;
+
+ // 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++;
+ }
+ }
+ if (spanOpen) {
+ sb.appendString('');
}
- 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;
sb.appendString('');
- return [charOffsets, visibleColumns];
+ return [charOffsets, visibleColumns, spanStartOffsets];
}
-function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[]): number[] | null {
+function readLineBreaks(range: Range, lineDomNode: HTMLDivElement, lineContent: string, charOffsets: number[], spanStartOffsets: number[]): number[] | null {
if (lineContent.length <= 1) {
return null;
}
@@ -305,7 +371,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 +385,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[], 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 +406,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[]): 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 2fc027b34a231..34c57f255760c 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;
+ /**
+ * Sets the width used to wrap this injected text in editor-font em units.
+ * @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 6d7e5a6be5503..ee6ad23b353b8 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 !== 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/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts
index 1f504db5852e6..561feefda7705 100644
--- a/src/vs/editor/common/textModelEvents.ts
+++ b/src/vs/editor/common/textModelEvents.ts
@@ -234,6 +234,24 @@ 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;
+}
+
+/**
+ * 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
@@ -257,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,
@@ -266,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,
@@ -288,6 +306,32 @@ 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;
+ 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/viewLayout/lineDecorations.ts b/src/vs/editor/common/viewLayout/lineDecorations.ts
index 3439b945aacc2..521d2668face0 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 11aca36bbb9a7..3616af6c04d18 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 f6869c57dd334..5e8a25204e644 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,27 +238,40 @@ 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;
+ const isWidthOnly = (length === 0 && options.widthInEm !== undefined);
+ const isLastOutputLine = outputLineIndex === breakOffsets.length - 1;
+ const isAtInternalWrapBoundary = injectedTextStartOffsetInInputWithInjections === lineEndOffsetInInputWithInjections && !isLastOutputLine;
- if (injectedTextStartOffsetInInputWithInjections > lineEndOffsetInInputWithInjections) {
+ if (injectedTextStartOffsetInInputWithInjections > lineEndOffsetInInputWithInjections || (isWidthOnly && isAtInternalWrapBoundary)) {
// Injected text only starts in later wrapped lines.
break;
}
- 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).
- 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 678c350916de2..0882655482027 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, wrappingIndent, wordBreak, isLineFeedWrappingEnabled);
+ result[i] = createLineBreaks(this.classifier, lineText, injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, fontInfo, wrappingIndent, wordBreak, isLineFeedWrappingEnabled);
}
}
arrPool1.length = 0;
@@ -356,8 +356,9 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla
return previousBreakingData;
}
-function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: 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 fixedWidthRanges = LineInjectedText.getFixedWidthInjectedTextRanges(injectedTexts);
let injectionOptions: InjectedTextOptions[] | null;
let injectionOffsets: number[] | null;
@@ -389,73 +390,117 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st
}
const isKeepAll = (wordBreak === 'keepAll');
- const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent);
- const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength;
+ 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.
+ 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 breakingColumn = firstLineBreakColumn;
- let prevCharCode = lineText.charCodeAt(0);
- let prevCharCodeClass = classifier.get(prevCharCode);
- let visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar);
-
- let 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;
- prevCharCode = lineText.charCodeAt(1);
+ let breakOffsetPixelWidth = 0;
+
+ let breakingPixelWidth = firstLineBreakColumn * typicalHalfwidthCharacterWidth;
+ let fixedWidthRangeIndex = 0;
+ 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) {
+ 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);
- startOffset++;
}
for (let i = startOffset; i < len; i++) {
+ const fixedWidthRange = fixedWidthRanges.length > 0 && fixedWidthRangeIndex < fixedWidthRanges.length ? fixedWidthRanges[fixedWidthRangeIndex] : null;
const charStartOffset = i;
- const charCode = lineText.charCodeAt(i);
+ let charCode = lineText.charCodeAt(i);
let charCodeClass: CharacterClass;
let charWidth: number;
+ let charPixelWidth: number;
let wrapEscapedLineFeed = false;
- if (strings.isHighSurrogate(charCode)) {
+ 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;
+ 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)) {
+ if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, charStartOffset)) {
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;
- 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;
@@ -491,6 +536,38 @@ 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, 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;
+ 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.
@@ -526,7 +603,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);
@@ -534,6 +611,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/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts
index 506be419193df..a605d3c978965 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,6 +49,9 @@ export class ColorDetector extends Disposable implements IEditorContribution {
private readonly _decoratorLimitReporter = this._register(new DecoratorLimitReporter());
+ private static readonly colorDecoratorInnerWidthInEm = 0.8;
+ private static readonly colorDecoratorMarginInEm = 0.2;
+
constructor(
private readonly _editor: ICodeEditor,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@@ -58,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.colorDecoratorInnerWidthInEm}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();
@@ -204,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);
@@ -231,7 +242,8 @@ export class ColorDetector extends Disposable implements IEditorContribution {
content: noBreakWhitespace,
inlineClassName: `${ref.className} colorpicker-color-decoration`,
inlineClassNameAffectsLetterSpacing: true,
- 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 f484517caba97..7f09b70cb98b0 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: var(--vscode-colorPicker-colorDecoratorWidth);
height: 0.8em;
line-height: 0.8em;
display: inline-block;
@@ -206,6 +206,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/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts
index 6c1c58c4f8ac1..e09a3807fbd9e 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,17 @@ export class InlayHintsController implements IEditorContribution {
};
const addInjectedWhitespace = (item: InlayHintItem, isLast: boolean): void => {
+ const widthInPixels = (fontSize / 3) | 0;
+ const widthInEm = widthInPixels / editorFontSize;
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, '', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None, InlayHintsController._whitespaceData, widthInEm);
};
//
- 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);
diff --git a/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts b/src/vs/editor/contrib/inlineProgress/browser/inlineProgress.ts
index 5b238c214768d..e111516e10f82 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,
}
});
diff --git a/src/vs/editor/test/common/model/modelDecorations.test.ts b/src/vs/editor/test/common/model/modelDecorations.test.ts
index 142bbead17a39..cf786fd4fe17e 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');
diff --git a/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts b/src/vs/editor/test/common/viewLayout/lineDecorations.test.ts
index efe951f345715..f19d4fcd65315 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 efc948f669e3c..28b01611eb066 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 uses its regular inline decoration', () => {
+ const injectionOptions: InjectedTextOptions[] = [
+ { content: '\xa0', inlineClassName: 'fixed-width', 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), 'fixed-width', InlineDecorationType.Regular)]
+ ]);
+ });
+
test('injection with inlineClassNameAffectsLetterSpacing', () => {
const injectionOptions: InjectedTextOptions[] = [
{ content: 'abc', inlineClassName: 'ls-class', inlineClassNameAffectsLetterSpacing: true }
@@ -488,4 +506,137 @@ suite('InjectedTextInlineDecorationsComputer', () => {
[new InlineDecoration(new Range(6, 1, 6, 3), 'wrap-class', InlineDecorationType.Regular)],
]);
});
+
+ 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 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 }
+ ];
+ 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 }
+ ];
+ 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 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 70d687d30e442..70019f302fea8 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);
@@ -134,6 +135,211 @@ 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, 7]
+ });
+ });
+
+ 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: [8, 11]
+ });
+ });
+
+ 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, [
+ 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, 8]
+ });
+ });
+
+ 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, 4, 7]
+ });
+ });
+
+ 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. 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: [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('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, [
+ 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]
+ });
+ });
+
+ 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);
@@ -142,7 +348,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);
}
}
diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts
index 6ee10c8b3f260..a4b9d2ea45585 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/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts
index c53b662bf176f..842a53873c060 100644
--- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts
+++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts
@@ -433,6 +433,7 @@ suite('Debug - Breakpoints', () => {
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.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);