Introduce minimap section headers, a la Xcode (#190759)

* WIP for adding minimap section headers for #74843

* Get section headers rendering

* Fix default value of section header font size

* Fix tests

* Improve section header position

* Fix separator display, update after config change

* Split too-long headers with an ellipsis

* Render section headers on the decorations canvas

* Support MARK with just a separator line

* Calculate minimap section headers asynchronously

* Simplify change

* Avoid font variable duplication

* Fix issue introduced earlier

* Recompute section headers when the language configuration changes

* Fix problem in constructing region header range

* Parse mark headers in the entire file and then filter out the ones not appearing in comments on the UI side, where tokens info is available

---------

Co-authored-by: Alexandru Dima <alexdima@microsoft.com>
This commit is contained in:
David Gileadi
2024-03-18 08:49:27 +00:00
committed by GitHub
co-authored by Alexandru Dima
parent cda53a0627
commit c800bf9216
22 changed files with 637 additions and 31 deletions
+16
View File
@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isMacintosh, isWindows } from 'vs/base/common/platform';
/**
* The best font-family to be used in CSS based on the platform:
* - Windows: Segoe preferred, fallback to sans-serif
* - macOS: standard system font, fallback to sans-serif
* - Linux: standard system font preferred, fallback to Ubuntu fonts
*
* Note: this currently does not adjust for different locales.
*/
export const DEFAULT_FONT_FAMILY = isWindows ? '"Segoe WPC", "Segoe UI", sans-serif' : isMacintosh ? '-apple-system, BlinkMacSystemFont, sans-serif' : 'system-ui, "Ubuntu", "Droid Sans", sans-serif';
@@ -29,6 +29,7 @@ import { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/di
import { ILinesDiffComputerOptions, MovedText } from 'vs/editor/common/diff/linesDiffComputer';
import { DetailedLineRangeMapping, RangeMapping, LineRangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { SectionHeader, FindSectionHeaderOptions } from 'vs/editor/common/services/findSectionHeaders';
import { mainWindow } from 'vs/base/browser/window';
import { WindowIntervalTimer } from 'vs/base/browser/dom';
@@ -190,6 +191,10 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> {
return this._workerManager.withWorker().then(client => client.computeWordRanges(resource, range));
}
public findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise<SectionHeader[]> {
return this._workerManager.withWorker().then(client => client.findSectionHeaders(uri, options));
}
}
class WordBasedCompletionItemProvider implements languages.CompletionItemProvider {
@@ -613,6 +618,12 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien
});
}
public findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise<SectionHeader[]> {
return this._withSyncedResources([uri]).then(proxy => {
return proxy.findSectionHeaders(uri.toString(), options);
});
}
override dispose(): void {
super.dispose();
this._disposed = true;
@@ -27,14 +27,16 @@ import { ViewContext } from 'vs/editor/common/viewModel/viewContext';
import { EditorTheme } from 'vs/editor/common/editorTheme';
import * as viewEvents from 'vs/editor/common/viewEvents';
import { ViewLineData, ViewModelDecoration } from 'vs/editor/common/viewModel';
import { minimapSelection, minimapBackground, minimapForegroundOpacity } from 'vs/platform/theme/common/colorRegistry';
import { minimapSelection, minimapBackground, minimapForegroundOpacity, editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel';
import { Selection } from 'vs/editor/common/core/selection';
import { Color } from 'vs/base/common/color';
import { GestureEvent, EventType, Gesture } from 'vs/base/browser/touch';
import { MinimapCharRendererFactory } from 'vs/editor/browser/viewParts/minimap/minimapCharRendererFactory';
import { MinimapPosition, TextModelResolvedOptions } from 'vs/editor/common/model';
import { MinimapPosition, MinimapSectionHeaderStyle, TextModelResolvedOptions } from 'vs/editor/common/model';
import { createSingleCallFunction } from 'vs/base/common/functional';
import { LRUCache } from 'vs/base/common/map';
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
/**
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
@@ -90,6 +92,9 @@ class MinimapOptions {
public readonly fontScale: number;
public readonly minimapLineHeight: number;
public readonly minimapCharWidth: number;
public readonly sectionHeaderFontFamily: string;
public readonly sectionHeaderFontSize: number;
public readonly sectionHeaderFontColor: RGBA8;
public readonly charRenderer: () => MinimapCharRenderer;
public readonly defaultBackgroundColor: RGBA8;
@@ -132,6 +137,9 @@ class MinimapOptions {
this.fontScale = minimapLayout.minimapScale;
this.minimapLineHeight = minimapLayout.minimapLineHeight;
this.minimapCharWidth = Constants.BASE_CHAR_WIDTH * this.fontScale;
this.sectionHeaderFontFamily = DEFAULT_FONT_FAMILY;
this.sectionHeaderFontSize = minimapOpts.sectionHeaderFontSize * pixelRatio;
this.sectionHeaderFontColor = MinimapOptions._getSectionHeaderColor(theme, tokensColorTracker.getColor(ColorId.DefaultForeground));
this.charRenderer = createSingleCallFunction(() => MinimapCharRendererFactory.create(this.fontScale, fontInfo.fontFamily));
this.defaultBackgroundColor = tokensColorTracker.getColor(ColorId.DefaultBackground);
@@ -155,6 +163,14 @@ class MinimapOptions {
return 255;
}
private static _getSectionHeaderColor(theme: EditorTheme, defaultForegroundColor: RGBA8): RGBA8 {
const themeColor = theme.getColor(editorForeground);
if (themeColor) {
return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, Math.round(255 * themeColor.rgba.a));
}
return defaultForegroundColor;
}
public equals(other: MinimapOptions): boolean {
return (this.renderMinimap === other.renderMinimap
&& this.size === other.size
@@ -179,6 +195,7 @@ class MinimapOptions {
&& this.fontScale === other.fontScale
&& this.minimapLineHeight === other.minimapLineHeight
&& this.minimapCharWidth === other.minimapCharWidth
&& this.sectionHeaderFontSize === other.sectionHeaderFontSize
&& this.defaultBackgroundColor && this.defaultBackgroundColor.equals(other.defaultBackgroundColor)
&& this.backgroundColor && this.backgroundColor.equals(other.backgroundColor)
&& this.foregroundAlpha === other.foregroundAlpha
@@ -544,6 +561,8 @@ export interface IMinimapModel {
getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): (ViewLineData | null)[];
getSelections(): Selection[];
getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[];
getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[];
getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null;
getOptions(): TextModelResolvedOptions;
revealLineNumber(lineNumber: number): void;
setScrollTop(scrollTop: number): void;
@@ -697,7 +716,7 @@ class MinimapSamplingState {
constructor(
public readonly samplingRatio: number,
public readonly minimapLines: number[]
public readonly minimapLines: number[] // a map of 0-based minimap line indexes to 1-based view line numbers
) {
}
@@ -790,6 +809,8 @@ export class Minimap extends ViewPart implements IMinimapModel {
private _samplingState: MinimapSamplingState | null;
private _shouldCheckSampling: boolean;
private _sectionHeaderCache = new LRUCache<string, string>(10, 1.5);
private _actual: InnerMinimap;
constructor(context: ViewContext) {
@@ -1037,15 +1058,8 @@ export class Minimap extends ViewPart implements IMinimapModel {
}
public getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] {
let visibleRange: Range;
if (this._samplingState) {
const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1];
const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1];
visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.viewModel.getLineMaxColumn(modelEndLineNumber));
} else {
visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.viewModel.getLineMaxColumn(endLineNumber));
}
const decorations = this._context.viewModel.getMinimapDecorationsInRange(visibleRange);
const decorations = this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber)
.filter(decoration => !decoration.options.minimap?.sectionHeaderStyle);
if (this._samplingState) {
const result: ViewModelDecoration[] = [];
@@ -1063,6 +1077,41 @@ export class Minimap extends ViewPart implements IMinimapModel {
return decorations;
}
public getSectionHeaderDecorationsInViewport(startLineNumber: number, endLineNumber: number): ViewModelDecoration[] {
const minimapLineHeight = this.options.minimapLineHeight;
const sectionHeaderFontSize = this.options.sectionHeaderFontSize;
const headerHeightInMinimapLines = sectionHeaderFontSize / minimapLineHeight;
startLineNumber = Math.floor(Math.max(1, startLineNumber - headerHeightInMinimapLines));
return this._getMinimapDecorationsInViewport(startLineNumber, endLineNumber)
.filter(decoration => !!decoration.options.minimap?.sectionHeaderStyle);
}
private _getMinimapDecorationsInViewport(startLineNumber: number, endLineNumber: number) {
let visibleRange: Range;
if (this._samplingState) {
const modelStartLineNumber = this._samplingState.minimapLines[startLineNumber - 1];
const modelEndLineNumber = this._samplingState.minimapLines[endLineNumber - 1];
visibleRange = new Range(modelStartLineNumber, 1, modelEndLineNumber, this._context.viewModel.getLineMaxColumn(modelEndLineNumber));
} else {
visibleRange = new Range(startLineNumber, 1, endLineNumber, this._context.viewModel.getLineMaxColumn(endLineNumber));
}
return this._context.viewModel.getMinimapDecorationsInRange(visibleRange);
}
public getSectionHeaderText(decoration: ViewModelDecoration, fitWidth: (s: string) => string): string | null {
const headerText = decoration.options.minimap?.sectionHeaderText;
if (!headerText) {
return null;
}
const cachedText = this._sectionHeaderCache.get(headerText);
if (cachedText) {
return cachedText;
}
const fittedText = fitWidth(headerText);
this._sectionHeaderCache.set(headerText, fittedText);
return fittedText;
}
public getOptions(): TextModelResolvedOptions {
return this._context.viewModel.model.getOptions();
}
@@ -1469,6 +1518,7 @@ class InnerMinimap extends Disposable {
const lineOffsetMap = new ContiguousLineMap<number[] | null>(layout.startLineNumber, layout.endLineNumber, null);
this._renderSelectionsHighlights(canvasContext, selections, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth);
this._renderDecorationsHighlights(canvasContext, decorations, lineOffsetMap, layout, minimapLineHeight, tabSize, minimapCharWidth, canvasInnerWidth);
this._renderSectionHeaders(layout);
}
}
@@ -1735,6 +1785,110 @@ class InnerMinimap extends Disposable {
canvasContext.fillRect(x, y, width, height);
}
private _renderSectionHeaders(layout: MinimapLayout) {
const minimapLineHeight = this._model.options.minimapLineHeight;
const sectionHeaderFontSize = this._model.options.sectionHeaderFontSize;
const backgroundFillHeight = sectionHeaderFontSize * 1.5;
const { canvasInnerWidth } = this._model.options;
const backgroundColor = this._model.options.backgroundColor;
const backgroundFill = `rgb(${backgroundColor.r} ${backgroundColor.g} ${backgroundColor.b} / .7)`;
const foregroundColor = this._model.options.sectionHeaderFontColor;
const foregroundFill = `rgb(${foregroundColor.r} ${foregroundColor.g} ${foregroundColor.b})`;
const separatorStroke = foregroundFill;
const canvasContext = this._decorationsCanvas.domNode.getContext('2d')!;
canvasContext.font = sectionHeaderFontSize + 'px ' + this._model.options.sectionHeaderFontFamily;
canvasContext.strokeStyle = separatorStroke;
canvasContext.lineWidth = 0.2;
const decorations = this._model.getSectionHeaderDecorationsInViewport(layout.startLineNumber, layout.endLineNumber);
decorations.sort((a, b) => a.range.startLineNumber - b.range.startLineNumber);
const fitWidth = InnerMinimap._fitSectionHeader.bind(null, canvasContext,
canvasInnerWidth - MINIMAP_GUTTER_WIDTH);
for (const decoration of decorations) {
const y = layout.getYForLineNumber(decoration.range.startLineNumber, minimapLineHeight) + sectionHeaderFontSize;
const backgroundFillY = y - sectionHeaderFontSize;
const separatorY = backgroundFillY + 2;
const headerText = this._model.getSectionHeaderText(decoration, fitWidth);
InnerMinimap._renderSectionLabel(
canvasContext,
headerText,
decoration.options.minimap?.sectionHeaderStyle === MinimapSectionHeaderStyle.Underlined,
backgroundFill,
foregroundFill,
canvasInnerWidth,
backgroundFillY,
backgroundFillHeight,
y,
separatorY);
}
}
private static _fitSectionHeader(
target: CanvasRenderingContext2D,
maxWidth: number,
headerText: string,
): string {
if (!headerText) {
return headerText;
}
const ellipsis = '…';
const width = target.measureText(headerText).width;
const ellipsisWidth = target.measureText(ellipsis).width;
if (width <= maxWidth || width <= ellipsisWidth) {
return headerText;
}
const len = headerText.length;
const averageCharWidth = width / headerText.length;
const maxCharCount = Math.floor((maxWidth - ellipsisWidth) / averageCharWidth) - 1;
// Find a halfway point that isn't after whitespace
let halfCharCount = Math.ceil(maxCharCount / 2);
while (halfCharCount > 0 && /\s/.test(headerText[halfCharCount - 1])) {
--halfCharCount;
}
// Split with ellipsis
return headerText.substring(0, halfCharCount)
+ ellipsis + headerText.substring(len - (maxCharCount - halfCharCount));
}
private static _renderSectionLabel(
target: CanvasRenderingContext2D,
headerText: string | null,
hasSeparatorLine: boolean,
backgroundFill: string,
foregroundFill: string,
minimapWidth: number,
backgroundFillY: number,
backgroundFillHeight: number,
textY: number,
separatorY: number
): void {
if (headerText) {
target.fillStyle = backgroundFill;
target.fillRect(0, backgroundFillY, minimapWidth, backgroundFillHeight);
target.fillStyle = foregroundFill;
target.fillText(headerText, MINIMAP_GUTTER_WIDTH, textY);
}
if (hasSeparatorLine) {
target.beginPath();
target.moveTo(0, separatorY);
target.lineTo(minimapWidth, separatorY);
target.closePath();
target.stroke();
}
}
private renderLines(layout: MinimapLayout): RenderData | null {
const startLineNumber = layout.startLineNumber;
const endLineNumber = layout.endLineNumber;
@@ -3059,6 +3059,18 @@ export interface IEditorMinimapOptions {
* Relative size of the font in the minimap. Defaults to 1.
*/
scale?: number;
/**
* Whether to show named regions as section headers. Defaults to true.
*/
showRegionSectionHeaders?: boolean;
/**
* Whether to show MARK: comments as section headers. Defaults to true.
*/
showMarkSectionHeaders?: boolean;
/**
* Font size of section headers. Defaults to 9.
*/
sectionHeaderFontSize?: number;
}
/**
@@ -3078,6 +3090,9 @@ class EditorMinimap extends BaseEditorOption<EditorOption.minimap, IEditorMinima
renderCharacters: true,
maxColumn: 120,
scale: 1,
showRegionSectionHeaders: true,
showMarkSectionHeaders: true,
sectionHeaderFontSize: 9,
};
super(
EditorOption.minimap, 'minimap', defaults,
@@ -3132,6 +3147,21 @@ class EditorMinimap extends BaseEditorOption<EditorOption.minimap, IEditorMinima
type: 'number',
default: defaults.maxColumn,
description: nls.localize('minimap.maxColumn', "Limit the width of the minimap to render at most a certain number of columns.")
},
'editor.minimap.showRegionSectionHeaders': {
type: 'boolean',
default: defaults.showRegionSectionHeaders,
description: nls.localize('minimap.showRegionSectionHeaders', "Controls whether named regions are shown as section headers in the minimap.")
},
'editor.minimap.showMarkSectionHeaders': {
type: 'boolean',
default: defaults.showMarkSectionHeaders,
description: nls.localize('minimap.showMarkSectionHeaders', "Controls whether MARK: comments are shown as section headers in the minimap.")
},
'editor.minimap.sectionHeaderFontSize': {
type: 'number',
default: defaults.sectionHeaderFontSize,
description: nls.localize('minimap.sectionHeaderFontSize', "Controls the font size of section headers in the minimap.")
}
}
);
@@ -3151,6 +3181,9 @@ class EditorMinimap extends BaseEditorOption<EditorOption.minimap, IEditorMinima
renderCharacters: boolean(input.renderCharacters, this.defaultValue.renderCharacters),
scale: EditorIntOption.clampedInt(input.scale, 1, 1, 3),
maxColumn: EditorIntOption.clampedInt(input.maxColumn, this.defaultValue.maxColumn, 1, 10000),
showRegionSectionHeaders: boolean(input.showRegionSectionHeaders, this.defaultValue.showRegionSectionHeaders),
showMarkSectionHeaders: boolean(input.showMarkSectionHeaders, this.defaultValue.showMarkSectionHeaders),
sectionHeaderFontSize: EditorFloatOption.clamp(input.sectionHeaderFontSize ?? this.defaultValue.sectionHeaderFontSize, 4, 32),
};
}
}
+17 -1
View File
@@ -70,11 +70,19 @@ export interface IGlyphMarginLanesModel {
/**
* Position in the minimap to render the decoration.
*/
export enum MinimapPosition {
export const enum MinimapPosition {
Inline = 1,
Gutter = 2
}
/**
* Section header style.
*/
export const enum MinimapSectionHeaderStyle {
Normal = 1,
Underlined = 2
}
export interface IDecorationOptions {
/**
* CSS color to render.
@@ -119,6 +127,14 @@ export interface IModelDecorationMinimapOptions extends IDecorationOptions {
* The position in the minimap.
*/
position: MinimapPosition;
/**
* If the decoration is for a section header, which header style.
*/
sectionHeaderStyle?: MinimapSectionHeaderStyle | null;
/**
* If the decoration is for a section header, the header text.
*/
sectionHeaderText?: string | null;
}
/**
+4 -1
View File
@@ -2219,12 +2219,15 @@ export class ModelDecorationGlyphMarginOptions {
export class ModelDecorationMinimapOptions extends DecorationOptions {
readonly position: model.MinimapPosition;
readonly sectionHeaderStyle: model.MinimapSectionHeaderStyle | null;
readonly sectionHeaderText: string | null;
private _resolvedColor: Color | undefined;
constructor(options: model.IModelDecorationMinimapOptions) {
super(options);
this.position = options.position;
this.sectionHeaderStyle = options.sectionHeaderStyle ?? null;
this.sectionHeaderText = options.sectionHeaderText ?? null;
}
public getColor(theme: IColorTheme): Color | undefined {
@@ -28,6 +28,7 @@ import { createProxyObject, getAllMethodNames } from 'vs/base/common/objects';
import { IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { BugIndicatingError } from 'vs/base/common/errors';
import { IDocumentColorComputerTarget, computeDefaultDocumentColors } from 'vs/editor/common/languages/defaultDocumentColorsComputer';
import { FindSectionHeaderOptions, SectionHeader, findSectionHeaders } from 'vs/editor/common/services/findSectionHeaders';
export interface IMirrorModel extends IMirrorTextModel {
readonly uri: URI;
@@ -401,6 +402,14 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
return UnicodeTextModelHighlighter.computeUnicodeHighlights(model, options, range);
}
public async findSectionHeaders(url: string, options: FindSectionHeaderOptions): Promise<SectionHeader[]> {
const model = this._getModel(url);
if (!model) {
return [];
}
return findSectionHeaders(model, options);
}
// ---- BEGIN diff --------------------------------------------------------------------------
public async computeDiff(originalUrl: string, modifiedUrl: string, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDiffComputationResult | null> {
@@ -11,6 +11,7 @@ import { IInplaceReplaceSupportResult, TextEdit } from 'vs/editor/common/languag
import { UnicodeHighlighterOptions } from 'vs/editor/common/services/unicodeTextModelHighlighter';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import type { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker';
import { SectionHeader, FindSectionHeaderOptions } from 'vs/editor/common/services/findSectionHeaders';
export const IEditorWorkerService = createDecorator<IEditorWorkerService>('editorWorkerService');
@@ -36,6 +37,8 @@ export interface IEditorWorkerService {
canNavigateValueSet(resource: URI): boolean;
navigateValueSet(resource: URI, range: IRange, up: boolean): Promise<IInplaceReplaceSupportResult | null>;
findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise<SectionHeader[]>;
}
export interface IDiffComputationResult {
@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IRange } from 'vs/editor/common/core/range';
import { FoldingRules } from 'vs/editor/common/languages/languageConfiguration';
export interface ISectionHeaderFinderTarget {
getLineCount(): number;
getLineContent(lineNumber: number): string;
}
export interface FindSectionHeaderOptions {
foldingRules?: FoldingRules;
findRegionSectionHeaders: boolean;
findMarkSectionHeaders: boolean;
}
export interface SectionHeader {
/**
* The location of the header text in the text model.
*/
range: IRange;
/**
* The section header text.
*/
text: string;
/**
* Whether the section header includes a separator line.
*/
hasSeparatorLine: boolean;
/**
* This section should be omitted before rendering if it's not in a comment.
*/
shouldBeInComments: boolean;
}
const markRegex = /\bMARK:\s*(.*)$/d;
const trimDashesRegex = /^-+|-+$/g;
/**
* Find section headers in the model.
*
* @param model the text model to search in
* @param options options to search with
* @returns an array of section headers
*/
export function findSectionHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] {
let headers: SectionHeader[] = [];
if (options.findRegionSectionHeaders && options.foldingRules?.markers) {
const regionHeaders = collectRegionHeaders(model, options);
headers = headers.concat(regionHeaders);
}
if (options.findMarkSectionHeaders) {
const markHeaders = collectMarkHeaders(model);
headers = headers.concat(markHeaders);
}
return headers;
}
function collectRegionHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] {
const regionHeaders: SectionHeader[] = [];
const endLineNumber = model.getLineCount();
for (let lineNumber = 1; lineNumber <= endLineNumber; lineNumber++) {
const lineContent = model.getLineContent(lineNumber);
const match = lineContent.match(options.foldingRules!.markers!.start);
if (match) {
const range = { startLineNumber: lineNumber, startColumn: match[0].length + 1, endLineNumber: lineNumber, endColumn: lineContent.length + 1 };
if (range.endColumn > range.startColumn) {
const sectionHeader = {
range,
...getHeaderText(lineContent.substring(match[0].length)),
shouldBeInComments: false
};
if (sectionHeader.text || sectionHeader.hasSeparatorLine) {
regionHeaders.push(sectionHeader);
}
}
}
}
return regionHeaders;
}
function collectMarkHeaders(model: ISectionHeaderFinderTarget): SectionHeader[] {
const markHeaders: SectionHeader[] = [];
const endLineNumber = model.getLineCount();
for (let lineNumber = 1; lineNumber <= endLineNumber; lineNumber++) {
const lineContent = model.getLineContent(lineNumber);
addMarkHeaderIfFound(lineContent, lineNumber, markHeaders);
}
return markHeaders;
}
function addMarkHeaderIfFound(lineContent: string, lineNumber: number, sectionHeaders: SectionHeader[]) {
markRegex.lastIndex = 0;
const match = markRegex.exec(lineContent);
if (match) {
const column = match.indices![1][0] + 1;
const endColumn = match.indices![1][1] + 1;
const range = { startLineNumber: lineNumber, startColumn: column, endLineNumber: lineNumber, endColumn: endColumn };
if (range.endColumn > range.startColumn) {
const sectionHeader = {
range,
...getHeaderText(match[1]),
shouldBeInComments: true
};
if (sectionHeader.text || sectionHeader.hasSeparatorLine) {
sectionHeaders.push(sectionHeader);
}
}
}
}
function getHeaderText(text: string): { text: string; hasSeparatorLine: boolean } {
text = text.trim();
const hasSeparatorLine = text.startsWith('-');
text = text.replace(trimDashesRegex, '');
return { text, hasSeparatorLine };
}
@@ -647,6 +647,14 @@ export enum MinimapPosition {
Gutter = 2
}
/**
* Section header style.
*/
export enum MinimapSectionHeaderStyle {
Normal = 1,
Underlined = 2
}
/**
* Type of hit element with the mouse in the editor.
*/
@@ -0,0 +1,208 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { EditorOption, IEditorMinimapOptions } from 'vs/editor/common/config/editorOptions';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { IModelDeltaDecoration, MinimapPosition, MinimapSectionHeaderStyle, TrackedRangeStickiness } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { FindSectionHeaderOptions, SectionHeader } from 'vs/editor/common/services/findSectionHeaders';
export class SectionHeaderDetector extends Disposable implements IEditorContribution {
public static readonly ID: string = 'editor.sectionHeaderDetector';
private options: FindSectionHeaderOptions | undefined;
private decorations = this.editor.createDecorationsCollection();
private computeSectionHeaders: RunOnceScheduler;
private computePromise: CancelablePromise<SectionHeader[]> | null;
private currentOccurrences: { [decorationId: string]: SectionHeaderOccurrence };
constructor(
private readonly editor: ICodeEditor,
@ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService,
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
) {
super();
this.options = this.createOptions(editor.getOption(EditorOption.minimap));
this.computePromise = null;
this.currentOccurrences = {};
this._register(editor.onDidChangeModel((e) => {
this.currentOccurrences = {};
this.options = this.createOptions(editor.getOption(EditorOption.minimap));
this.stop();
this.computeSectionHeaders.schedule(0);
}));
this._register(editor.onDidChangeModelLanguage((e) => {
this.currentOccurrences = {};
this.options = this.createOptions(editor.getOption(EditorOption.minimap));
this.stop();
this.computeSectionHeaders.schedule(0);
}));
this._register(languageConfigurationService.onDidChange((e) => {
const editorLanguageId = this.editor.getModel()?.getLanguageId();
if (editorLanguageId && e.affects(editorLanguageId)) {
this.currentOccurrences = {};
this.options = this.createOptions(editor.getOption(EditorOption.minimap));
this.stop();
this.computeSectionHeaders.schedule(0);
}
}));
this._register(editor.onDidChangeConfiguration(e => {
if (this.options && !e.hasChanged(EditorOption.minimap)) {
return;
}
this.options = this.createOptions(editor.getOption(EditorOption.minimap));
// Remove any links (for the getting disabled case)
this.updateDecorations([]);
// Stop any computation (for the getting disabled case)
this.stop();
// Start computing (for the getting enabled case)
this.computeSectionHeaders.schedule(0);
}));
this._register(this.editor.onDidChangeModelContent(e => {
this.computeSectionHeaders.schedule();
}));
this.computeSectionHeaders = this._register(new RunOnceScheduler(() => {
this.findSectionHeaders();
}, 250));
this.computeSectionHeaders.schedule(0);
}
private createOptions(minimap: Readonly<Required<IEditorMinimapOptions>>): FindSectionHeaderOptions | undefined {
if (!minimap || !this.editor.hasModel()) {
return undefined;
}
const languageId = this.editor.getModel().getLanguageId();
if (!languageId) {
return undefined;
}
const commentsConfiguration = this.languageConfigurationService.getLanguageConfiguration(languageId).comments;
const foldingRules = this.languageConfigurationService.getLanguageConfiguration(languageId).foldingRules;
if (!commentsConfiguration && !foldingRules?.markers) {
return undefined;
}
return {
foldingRules,
findMarkSectionHeaders: minimap.showMarkSectionHeaders,
findRegionSectionHeaders: minimap.showRegionSectionHeaders,
};
}
private findSectionHeaders() {
if (!this.editor.hasModel()
|| (!this.options?.findMarkSectionHeaders && !this.options?.findRegionSectionHeaders)) {
return;
}
const model = this.editor.getModel();
if (model.isDisposed() || model.isTooLargeForSyncing()) {
return;
}
const modelVersionId = model.getVersionId();
this.editorWorkerService.findSectionHeaders(model.uri, this.options)
.then((sectionHeaders) => {
if (model.isDisposed() || model.getVersionId() !== modelVersionId) {
// model changed in the meantime
return;
}
this.updateDecorations(sectionHeaders);
});
}
private updateDecorations(sectionHeaders: SectionHeader[]): void {
const model = this.editor.getModel();
if (model) {
// Remove all section headers that should be in comments and are not in comments
sectionHeaders = sectionHeaders.filter((sectionHeader) => {
if (!sectionHeader.shouldBeInComments) {
return true;
}
const validRange = model.validateRange(sectionHeader.range);
const tokens = model.tokenization.getLineTokens(validRange.startLineNumber);
const idx = tokens.findTokenIndexAtOffset(validRange.startColumn - 1);
const tokenType = tokens.getStandardTokenType(idx);
const languageId = tokens.getLanguageId(idx);
return (languageId === model.getLanguageId() && tokenType === StandardTokenType.Comment);
});
}
const oldDecorations = Object.values(this.currentOccurrences).map(occurrence => occurrence.decorationId);
const newDecorations = sectionHeaders.map(sectionHeader => decoration(sectionHeader));
this.editor.changeDecorations((changeAccessor) => {
const decorations = changeAccessor.deltaDecorations(oldDecorations, newDecorations);
this.currentOccurrences = {};
for (let i = 0, len = decorations.length; i < len; i++) {
const occurrence = { sectionHeader: sectionHeaders[i], decorationId: decorations[i] };
this.currentOccurrences[occurrence.decorationId] = occurrence;
}
});
}
private stop(): void {
this.computeSectionHeaders.cancel();
if (this.computePromise) {
this.computePromise.cancel();
this.computePromise = null;
}
}
public override dispose(): void {
super.dispose();
this.stop();
this.decorations.clear();
}
}
interface SectionHeaderOccurrence {
readonly sectionHeader: SectionHeader;
readonly decorationId: string;
}
function decoration(sectionHeader: SectionHeader): IModelDeltaDecoration {
return {
range: sectionHeader.range,
options: ModelDecorationOptions.createDynamic({
description: 'section-header',
stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingAfter,
collapseOnReplaceEdit: true,
minimap: {
color: undefined,
position: MinimapPosition.Inline,
sectionHeaderStyle: sectionHeader.hasSeparatorLine ? MinimapSectionHeaderStyle.Underlined : MinimapSectionHeaderStyle.Normal,
sectionHeaderText: sectionHeader.text,
},
})
};
}
registerEditorContribution(SectionHeaderDetector.ID, SectionHeaderDetector, EditorContributionInstantiation.AfterFirstRender);
+1
View File
@@ -44,6 +44,7 @@ import 'vs/editor/contrib/multicursor/browser/multicursor';
import 'vs/editor/contrib/inlineEdit/browser/inlineEdit.contribution';
import 'vs/editor/contrib/parameterHints/browser/parameterHints';
import 'vs/editor/contrib/rename/browser/rename';
import 'vs/editor/contrib/sectionHeaders/browser/sectionHeaders';
import 'vs/editor/contrib/semanticTokens/browser/documentSemanticTokens';
import 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens';
import 'vs/editor/contrib/smartSelect/browser/smartSelect';
@@ -550,6 +550,7 @@ export function createMonacoEditorAPI(): typeof monaco.editor {
EndOfLinePreference: standaloneEnums.EndOfLinePreference,
EndOfLineSequence: standaloneEnums.EndOfLineSequence,
MinimapPosition: standaloneEnums.MinimapPosition,
MinimapSectionHeaderStyle: standaloneEnums.MinimapSectionHeaderStyle,
MouseTargetType: standaloneEnums.MouseTargetType,
OverlayWidgetPositionPreference: standaloneEnums.OverlayWidgetPositionPreference,
OverviewRulerLane: standaloneEnums.OverviewRulerLane,
@@ -58,6 +58,9 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => {
maxColumn: input.minimapMaxColumn,
showSlider: 'mouseover',
scale: 1,
showRegionSectionHeaders: true,
showMarkSectionHeaders: true,
sectionHeaderFontSize: 9
};
options._write(EditorOption.minimap, minimapOptions);
const scrollbarOptions: InternalEditorScrollbarOptions = {
@@ -9,6 +9,7 @@ import { DiffAlgorithmName, IEditorWorkerService, IUnicodeHighlightsResult } fro
import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/languages';
import { IDocumentDiff, IDocumentDiffProviderOptions } from 'vs/editor/common/diff/documentDiffProvider';
import { IChange } from 'vs/editor/common/diff/legacyLinesDiffComputer';
import { SectionHeader } from 'vs/editor/common/services/findSectionHeaders';
export class TestEditorWorkerService implements IEditorWorkerService {
@@ -25,4 +26,5 @@ export class TestEditorWorkerService implements IEditorWorkerService {
async computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> { return null; }
canNavigateValueSet(resource: URI): boolean { return false; }
async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise<IInplaceReplaceSupportResult | null> { return null; }
async findSectionHeaders(uri: URI): Promise<SectionHeader[]> { return []; }
}
+28
View File
@@ -1613,6 +1613,14 @@ declare namespace monaco.editor {
Gutter = 2
}
/**
* Section header style.
*/
export enum MinimapSectionHeaderStyle {
Normal = 1,
Underlined = 2
}
export interface IDecorationOptions {
/**
* CSS color to render.
@@ -1656,6 +1664,14 @@ declare namespace monaco.editor {
* The position in the minimap.
*/
position: MinimapPosition;
/**
* If the decoration is for a section header, which header style.
*/
sectionHeaderStyle?: MinimapSectionHeaderStyle | null;
/**
* If the decoration is for a section header, the header text.
*/
sectionHeaderText?: string | null;
}
/**
@@ -4290,6 +4306,18 @@ declare namespace monaco.editor {
* Relative size of the font in the minimap. Defaults to 1.
*/
scale?: number;
/**
* Whether to show named regions as section headers. Defaults to true.
*/
showRegionSectionHeaders?: boolean;
/**
* Whether to show MARK: comments as section headers. Defaults to true.
*/
showMarkSectionHeaders?: boolean;
/**
* Font size of section headers. Defaults to 9.
*/
sectionHeaderFontSize?: number;
}
/**
+1 -11
View File
@@ -6,7 +6,7 @@
import 'vs/css!./media/style';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme';
import { isWeb, isIOS, isMacintosh, isWindows } from 'vs/base/common/platform';
import { isWeb, isIOS } from 'vs/base/common/platform';
import { createMetaElement } from 'vs/base/browser/dom';
import { isSafari, isStandalone } from 'vs/base/browser/browser';
import { selectionBackground } from 'vs/platform/theme/common/colorRegistry';
@@ -60,13 +60,3 @@ registerThemingParticipant((theme, collector) => {
collector.addRule(`body { background-color: ${workbenchBackground}; }`);
}
});
/**
* The best font-family to be used in CSS based on the platform:
* - Windows: Segoe preferred, fallback to sans-serif
* - macOS: standard system font, fallback to sans-serif
* - Linux: standard system font preferred, fallback to Ubuntu fonts
*
* Note: this currently does not adjust for different locales.
*/
export const DEFAULT_FONT_FAMILY = isWindows ? '"Segoe WPC", "Segoe UI", sans-serif' : isMacintosh ? '-apple-system, BlinkMacSystemFont, sans-serif' : 'system-ui, "Ubuntu", "Droid Sans", sans-serif';
@@ -33,7 +33,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { defaultCheckboxStyles } from 'vs/platform/theme/browser/defaultStyles';
import { asCssVariableWithDefault, checkboxBorder, inputBackground } from 'vs/platform/theme/common/colorRegistry';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { AccessibilityCommandId } from 'vs/workbench/contrib/accessibility/common/accessibilityCommands';
import { ChatSubmitEditorAction, ChatSubmitSecondaryAgentEditorAction } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
@@ -30,7 +30,7 @@ import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreve
import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
import { HistoryNavigator } from 'vs/base/common/history';
import { registerAndCreateHistoryNavigationContext, IHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget';
import { IHistoryNavigationWidget } from 'vs/base/browser/history';
@@ -15,7 +15,7 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/wi
import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
@@ -67,7 +67,7 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ILabelService } from 'vs/platform/label/common/label';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
import { Codicon } from 'vs/base/common/codicons';
import { ThemeIcon } from 'vs/base/common/themables';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
@@ -3,14 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DEFAULT_FONT_FAMILY } from 'vs/base/browser/fonts';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
import { ColorScheme } from 'vs/platform/theme/common/theme';
import { IWorkbenchThemeService, IWorkbenchColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
import { IWorkbenchColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { WebviewStyles } from 'vs/workbench/contrib/webview/browser/webview';
interface WebviewThemeData {