diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts
index 92537ee51db..03336d34278 100644
--- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts
+++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts
@@ -8,6 +8,7 @@ import * as platform from 'vs/base/common/platform';
import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay';
import { RenderLineNumbersType, EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
+import { Range } from 'vs/editor/common/core/range';
import { RenderingContext } from 'vs/editor/browser/view/renderingContext';
import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import * as viewEvents from 'vs/editor/common/viewEvents';
@@ -98,6 +99,9 @@ export class LineNumbersOverlay extends DynamicViewOverlay {
public override onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean {
return true;
}
+ public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean {
+ return e.affectsLineNumber;
+ }
// --- end event handlers
@@ -143,36 +147,50 @@ export class LineNumbersOverlay extends DynamicViewOverlay {
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
+ const lineNoDecorations = this._context.viewModel.getDecorationsInViewport(ctx.visibleRange).filter(d => !!d.options.lineNumberClassName);
+ lineNoDecorations.sort((a, b) => Range.compareRangesUsingEnds(a.range, b.range));
+ let decorationStartIndex = 0;
+
const lineCount = this._context.viewModel.getLineCount();
const output: string[] = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
- const renderLineNumber = this._getLineRenderLineNumber(lineNumber);
+ let renderLineNumber = this._getLineRenderLineNumber(lineNumber);
+ let extraClassNames = '';
- if (!renderLineNumber) {
+ // skip decorations whose end positions we've already passed
+ while (decorationStartIndex < lineNoDecorations.length && lineNoDecorations[decorationStartIndex].range.endLineNumber < lineNumber) {
+ decorationStartIndex++;
+ }
+ for (let i = decorationStartIndex; i < lineNoDecorations.length; i++) {
+ const { range, options } = lineNoDecorations[i];
+ if (range.startLineNumber <= lineNumber) {
+ extraClassNames += ' ' + options.lineNumberClassName;
+ }
+ }
+
+ if (!renderLineNumber && !extraClassNames) {
output[lineIndex] = '';
continue;
}
- let extraClassName = '';
-
if (lineNumber === lineCount && this._context.viewModel.getLineLength(lineNumber) === 0) {
// this is the last line
if (this._renderFinalNewline === 'off') {
- output[lineIndex] = '';
- continue;
+ renderLineNumber = '';
}
if (this._renderFinalNewline === 'dimmed') {
- extraClassName = ' dimmed-line-number';
+ extraClassNames += ' dimmed-line-number';
}
}
if (lineNumber === this._activeLineNumber) {
- extraClassName = ' active-line-number';
+ extraClassNames += ' active-line-number';
}
+
output[lineIndex] = (
- `
`
+ ``
);
}
diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts
index 3d84860632f..ad0d31765ff 100644
--- a/src/vs/editor/common/model.ts
+++ b/src/vs/editor/common/model.ts
@@ -160,6 +160,10 @@ export interface IModelDecorationOptions {
* Array of MarkdownString to render as the decoration message.
*/
hoverMessage?: IMarkdownString | IMarkdownString[] | null;
+ /**
+ * Array of MarkdownString to render as the line number message.
+ */
+ lineNumberHoverMessage?: IMarkdownString | IMarkdownString[] | null;
/**
* Should the decoration expand to encompass a whole line.
*/
@@ -204,6 +208,10 @@ export interface IModelDecorationOptions {
* Controls the tooltip text of the line decoration.
*/
linesDecorationsTooltip?: string | null;
+ /**
+ * If set, the decoration will be rendered on the line number.
+ */
+ lineNumberClassName?: string | null;
/**
* If set, the decoration will be rendered in the lines decorations with this CSS class name, but only for the first line in case of line wrapping.
*/
diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts
index 8b91f6ae75e..e3e8239cb55 100644
--- a/src/vs/editor/common/model/textModel.ts
+++ b/src/vs/editor/common/model/textModel.ts
@@ -2299,6 +2299,8 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
readonly glyphMargin?: model.IModelDecorationGlyphMarginOptions | null | undefined;
readonly glyphMarginClassName: string | null;
readonly linesDecorationsClassName: string | null;
+ readonly lineNumberClassName: string | null;
+ readonly lineNumberHoverMessage: IMarkdownString | IMarkdownString[] | null;
readonly linesDecorationsTooltip: string | null;
readonly firstLineDecorationClassName: string | null;
readonly marginClassName: string | null;
@@ -2323,6 +2325,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
this.shouldFillLineOnLineBreak = options.shouldFillLineOnLineBreak ?? null;
this.hoverMessage = options.hoverMessage || null;
this.glyphMarginHoverMessage = options.glyphMarginHoverMessage || null;
+ this.lineNumberHoverMessage = options.lineNumberHoverMessage || null;
this.isWholeLine = options.isWholeLine || false;
this.showIfCollapsed = options.showIfCollapsed || false;
this.collapseOnReplaceEdit = options.collapseOnReplaceEdit || false;
@@ -2331,6 +2334,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
this.glyphMargin = options.glyphMarginClassName ? new ModelDecorationGlyphMarginOptions(options.glyphMargin) : null;
this.glyphMarginClassName = options.glyphMarginClassName ? cleanClassName(options.glyphMarginClassName) : null;
this.linesDecorationsClassName = options.linesDecorationsClassName ? cleanClassName(options.linesDecorationsClassName) : null;
+ this.lineNumberClassName = options.lineNumberClassName ? cleanClassName(options.lineNumberClassName) : null;
this.linesDecorationsTooltip = options.linesDecorationsTooltip ? strings.htmlAttributeEncodeValue(options.linesDecorationsTooltip) : null;
this.firstLineDecorationClassName = options.firstLineDecorationClassName ? cleanClassName(options.firstLineDecorationClassName) : null;
this.marginClassName = options.marginClassName ? cleanClassName(options.marginClassName) : null;
@@ -2374,6 +2378,7 @@ class DidChangeDecorationsEmitter extends Disposable {
private _affectsOverviewRuler: boolean;
private _affectedInjectedTextLines: Set | null = null;
private _affectsGlyphMargin: boolean;
+ private _affectsLineNumber: boolean;
constructor(private readonly handleBeforeFire: (affectedInjectedTextLines: Set | null) => void) {
super();
@@ -2382,6 +2387,7 @@ class DidChangeDecorationsEmitter extends Disposable {
this._affectsMinimap = false;
this._affectsOverviewRuler = false;
this._affectsGlyphMargin = false;
+ this._affectsLineNumber = false;
}
hasListeners(): boolean {
@@ -2412,15 +2418,10 @@ class DidChangeDecorationsEmitter extends Disposable {
}
public checkAffectedAndFire(options: ModelDecorationOptions): void {
- if (!this._affectsMinimap) {
- this._affectsMinimap = options.minimap && options.minimap.position ? true : false;
- }
- if (!this._affectsOverviewRuler) {
- this._affectsOverviewRuler = options.overviewRuler && options.overviewRuler.color ? true : false;
- }
- if (!this._affectsGlyphMargin) {
- this._affectsGlyphMargin = options.glyphMarginClassName ? true : false;
- }
+ this._affectsMinimap ||= !!options.minimap?.position;
+ this._affectsOverviewRuler ||= !!options.overviewRuler?.color;
+ this._affectsGlyphMargin ||= !!options.glyphMarginClassName;
+ this._affectsLineNumber ||= !!options.lineNumberClassName;
this.tryFire();
}
@@ -2445,7 +2446,8 @@ class DidChangeDecorationsEmitter extends Disposable {
const event: IModelDecorationsChangedEvent = {
affectsMinimap: this._affectsMinimap,
affectsOverviewRuler: this._affectsOverviewRuler,
- affectsGlyphMargin: this._affectsGlyphMargin
+ affectsGlyphMargin: this._affectsGlyphMargin,
+ affectsLineNumber: this._affectsLineNumber,
};
this._shouldFireDeferred = false;
this._affectsMinimap = false;
diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts
index 8c25e06776d..58c720ac87c 100644
--- a/src/vs/editor/common/textModelEvents.ts
+++ b/src/vs/editor/common/textModelEvents.ts
@@ -91,6 +91,7 @@ export interface IModelDecorationsChangedEvent {
readonly affectsMinimap: boolean;
readonly affectsOverviewRuler: boolean;
readonly affectsGlyphMargin: boolean;
+ readonly affectsLineNumber: boolean;
}
/**
diff --git a/src/vs/editor/common/viewEvents.ts b/src/vs/editor/common/viewEvents.ts
index 0e6cb0fa166..f607afe73e9 100644
--- a/src/vs/editor/common/viewEvents.ts
+++ b/src/vs/editor/common/viewEvents.ts
@@ -76,16 +76,19 @@ export class ViewDecorationsChangedEvent {
readonly affectsMinimap: boolean;
readonly affectsOverviewRuler: boolean;
readonly affectsGlyphMargin: boolean;
+ readonly affectsLineNumber: boolean;
constructor(source: IModelDecorationsChangedEvent | null) {
if (source) {
this.affectsMinimap = source.affectsMinimap;
this.affectsOverviewRuler = source.affectsOverviewRuler;
this.affectsGlyphMargin = source.affectsGlyphMargin;
+ this.affectsLineNumber = source.affectsLineNumber;
} else {
this.affectsMinimap = true;
this.affectsOverviewRuler = true;
this.affectsGlyphMargin = true;
+ this.affectsLineNumber = true;
}
}
}
diff --git a/src/vs/editor/contrib/hover/browser/hover.ts b/src/vs/editor/contrib/hover/browser/hover.ts
index 08b2a7bb68d..509eb67e324 100644
--- a/src/vs/editor/contrib/hover/browser/hover.ts
+++ b/src/vs/editor/contrib/hover/browser/hover.ts
@@ -300,6 +300,12 @@ export class HoverController extends Disposable implements IEditorContribution {
glyphWidget.startShowingAt(target.position.lineNumber, target.detail.glyphMarginLane);
return;
}
+ if (target.type === MouseTargetType.GUTTER_LINE_NUMBERS && target.position) {
+ this._contentWidget?.hide();
+ const glyphWidget = this._getOrCreateGlyphWidget();
+ glyphWidget.startShowingAt(target.position.lineNumber, 'lineNo');
+ return;
+ }
if (_sticky) {
return;
}
diff --git a/src/vs/editor/contrib/hover/browser/marginHover.ts b/src/vs/editor/contrib/hover/browser/marginHover.ts
index 98af0a06cda..e9eb33604c1 100644
--- a/src/vs/editor/contrib/hover/browser/marginHover.ts
+++ b/src/vs/editor/contrib/hover/browser/marginHover.ts
@@ -22,6 +22,8 @@ export interface IHoverMessage {
value: IMarkdownString;
}
+type LaneOrLineNumber = GlyphMarginLane | 'lineNo';
+
export class MarginHoverWidget extends Disposable implements IOverlayWidget {
public static readonly ID = 'editor.contrib.modesGlyphHoverWidget';
@@ -99,8 +101,8 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget {
}
}
- public startShowingAt(lineNumber: number, lane: GlyphMarginLane): void {
- if (this._computer.lineNumber === lineNumber && this._computer.lane === lane) {
+ public startShowingAt(lineNumber: number, laneOrLine: LaneOrLineNumber): void {
+ if (this._computer.lineNumber === lineNumber && this._computer.lane === laneOrLine) {
// We have to show the widget at the exact same line number as before, so no work is needed
return;
}
@@ -110,7 +112,7 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget {
this.hide();
this._computer.lineNumber = lineNumber;
- this._computer.lane = lane;
+ this._computer.lane = laneOrLine;
this._hoverOperation.start(HoverStartMode.Delayed);
}
@@ -169,8 +171,8 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget {
const lineHeight = this._editor.getOption(EditorOption.lineHeight);
const nodeHeight = this._hover.containerDomNode.clientHeight;
const top = topForLineNumber - editorScrollTop - ((nodeHeight - lineHeight) / 2);
-
- this._hover.containerDomNode.style.left = `${editorLayout.glyphMarginLeft + editorLayout.glyphMarginWidth}px`;
+ const left = editorLayout.glyphMarginLeft + editorLayout.glyphMarginWidth + (this._computer.lane === 'lineNo' ? editorLayout.lineNumbersWidth : 0);
+ this._hover.containerDomNode.style.left = `${left}px`;
this._hover.containerDomNode.style.top = `${Math.max(Math.round(top), 0)}px`;
}
}
@@ -178,7 +180,7 @@ export class MarginHoverWidget extends Disposable implements IOverlayWidget {
class MarginHoverComputer implements IHoverComputer {
private _lineNumber: number = -1;
- private _lane = GlyphMarginLane.Center;
+ private _laneOrLine: LaneOrLineNumber = GlyphMarginLane.Center;
public get lineNumber(): number {
return this._lineNumber;
@@ -188,12 +190,12 @@ class MarginHoverComputer implements IHoverComputer {
this._lineNumber = value;
}
- public get lane(): number {
- return this._lane;
+ public get lane(): LaneOrLineNumber {
+ return this._laneOrLine;
}
- public set lane(value: GlyphMarginLane) {
- this._lane = value;
+ public set lane(value: LaneOrLineNumber) {
+ this._laneOrLine = value;
}
constructor(
@@ -212,21 +214,18 @@ class MarginHoverComputer implements IHoverComputer {
const lineDecorations = this._editor.getLineDecorations(this._lineNumber);
const result: IHoverMessage[] = [];
+ const isLineHover = this._laneOrLine === 'lineNo';
if (!lineDecorations) {
return result;
}
for (const d of lineDecorations) {
- if (!d.options.glyphMarginClassName) {
- continue;
- }
-
const lane = d.options.glyphMargin?.position ?? GlyphMarginLane.Center;
- if (lane !== this._lane) {
+ if (!isLineHover && lane !== this._laneOrLine) {
continue;
}
- const hoverMessage = d.options.glyphMarginHoverMessage;
+ const hoverMessage = isLineHover ? d.options.lineNumberHoverMessage : d.options.glyphMarginHoverMessage;
if (!hoverMessage || isEmptyMarkdownString(hoverMessage)) {
continue;
}
diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts
index ac4a31bd48e..f0028ac35dc 100644
--- a/src/vs/monaco.d.ts
+++ b/src/vs/monaco.d.ts
@@ -1691,6 +1691,10 @@ declare namespace monaco.editor {
* Array of MarkdownString to render as the decoration message.
*/
hoverMessage?: IMarkdownString | IMarkdownString[] | null;
+ /**
+ * Array of MarkdownString to render as the line number message.
+ */
+ lineNumberHoverMessage?: IMarkdownString | IMarkdownString[] | null;
/**
* Should the decoration expand to encompass a whole line.
*/
@@ -1730,6 +1734,10 @@ declare namespace monaco.editor {
* Controls the tooltip text of the line decoration.
*/
linesDecorationsTooltip?: string | null;
+ /**
+ * If set, the decoration will be rendered on the line number.
+ */
+ lineNumberClassName?: string | null;
/**
* If set, the decoration will be rendered in the lines decorations with this CSS class name, but only for the first line in case of line wrapping.
*/
@@ -2930,6 +2938,7 @@ declare namespace monaco.editor {
readonly affectsMinimap: boolean;
readonly affectsOverviewRuler: boolean;
readonly affectsGlyphMargin: boolean;
+ readonly affectsLineNumber: boolean;
}
export interface IModelOptionsChangedEvent {
diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts
index 856c7d4e6ae..7fb11557806 100644
--- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts
+++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts
@@ -11,14 +11,13 @@ import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
-import { GlyphMarginLane, IModelDecorationOptions, ITextModel } from 'vs/editor/common/model';
+import { IModelDecorationOptions, ITextModel } from 'vs/editor/common/model';
import { localize } from 'vs/nls';
import { ILogService } from 'vs/platform/log/common/log';
import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService';
import { CoverageDetails, DetailType, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes';
-const GLYPH_LANE = GlyphMarginLane.Left;
const MAX_HOVERED_LINES = 30;
const CLASS_HIT = 'coverage-deco-hit';
const CLASS_MISS = 'coverage-deco-miss';
@@ -66,8 +65,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
}));
this._register(editor.onMouseMove(e => {
- if (e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN
- && e.target.detail.glyphMarginLane === GLYPH_LANE) {
+ if (e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS) {
this.hoverLineNumber(editor.getModel()!, e.target.position.lineNumber);
} else {
this.hoveredStore.clear();
@@ -143,11 +141,10 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
const opts: IModelDecorationOptions = {
showIfCollapsed: false,
description: 'coverage-gutter',
- glyphMargin: { position: GlyphMarginLane.Left, persistLane: true },
- glyphMarginHoverMessage: new MarkdownString()
+ lineNumberHoverMessage: new MarkdownString()
.appendCodeblock(model.getLanguageId(), model.getValueInRange(range))
.appendText(localize('testing.branchHitCount', 'Branch hit count: {0}', hits)),
- glyphMarginClassName: `coverage-deco-gutter ${cls}`,
+ lineNumberClassName: `coverage-deco-gutter ${cls}`,
};
this.decorationIds.set(e.addDecoration(range, opts), {
@@ -161,11 +158,10 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri
const opts: IModelDecorationOptions = {
showIfCollapsed: false,
description: 'coverage-inline',
- glyphMargin: { position: GlyphMarginLane.Left, persistLane: true },
- glyphMarginHoverMessage: new MarkdownString()
+ lineNumberHoverMessage: new MarkdownString()
.appendCodeblock(model.getLanguageId(), model.getValueInRange(range))
.appendText(localize('testing.hitCount', 'Hit count: {0}', detail.count)),
- glyphMarginClassName: `coverage-deco-gutter ${cls}`,
+ lineNumberClassName: `coverage-deco-gutter ${cls}`,
};
this.decorationIds.set(e.addDecoration(range, opts), {
diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css
index 6168c064211..d40e482a1c7 100644
--- a/src/vs/workbench/contrib/testing/browser/media/testing.css
+++ b/src/vs/workbench/contrib/testing/browser/media/testing.css
@@ -417,12 +417,14 @@
/** -- coverage decorations */
+.coverage-deco-gutter {
+ z-index: 0;
+}
.coverage-deco-gutter::before {
content: '';
position: absolute;
inset: 0;
- right: 25%;
- left: 25%;
+ z-index: -1;
}
.coverage-deco-gutter.coverage-deco-hit::before {