mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-18 05:06:39 +01:00
editor: support line number decorations
Supports setting a class name and hover message for line numbers
This commit is contained in:
@@ -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] = (
|
||||
`<div class="${LineNumbersOverlay.CLASS_NAME}${lineHeightClassName}${extraClassName}" style="left:${this._lineNumbersLeft}px;width:${this._lineNumbersWidth}px;">${renderLineNumber}</div>`
|
||||
`<div class="${LineNumbersOverlay.CLASS_NAME}${lineHeightClassName}${extraClassNames}" style="left:${this._lineNumbersLeft}px;width:${this._lineNumbersWidth}px;">${renderLineNumber}</div>`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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<number> | null = null;
|
||||
private _affectsGlyphMargin: boolean;
|
||||
private _affectsLineNumber: boolean;
|
||||
|
||||
constructor(private readonly handleBeforeFire: (affectedInjectedTextLines: Set<number> | 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;
|
||||
|
||||
@@ -91,6 +91,7 @@ export interface IModelDecorationsChangedEvent {
|
||||
readonly affectsMinimap: boolean;
|
||||
readonly affectsOverviewRuler: boolean;
|
||||
readonly affectsGlyphMargin: boolean;
|
||||
readonly affectsLineNumber: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<IHoverMessage> {
|
||||
|
||||
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<IHoverMessage> {
|
||||
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<IHoverMessage> {
|
||||
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;
|
||||
}
|
||||
|
||||
Vendored
+9
@@ -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 {
|
||||
|
||||
@@ -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), {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user