Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce minimap section headers, a la Xcode #190759

Merged
merged 28 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8a7909c
WIP for adding minimap section headers for #74843
dgileadi Aug 14, 2023
859b514
Get section headers rendering
dgileadi Aug 18, 2023
1e7d320
Fix default value of section header font size
dgileadi Aug 18, 2023
84c4b26
Fix tests
dgileadi Aug 18, 2023
7f82bcc
Improve section header position
dgileadi Aug 18, 2023
25ba766
Merge branch 'main' into minimap-sections
dgileadi Aug 18, 2023
e4a2743
Fix separator display, update after config change
dgileadi Aug 18, 2023
665c18b
Split too-long headers with an ellipsis
dgileadi Aug 18, 2023
e44daec
Render section headers on the decorations canvas
dgileadi Aug 18, 2023
a72fc06
Support MARK with just a separator line
dgileadi Aug 18, 2023
cf32c66
Merge branch 'main' into minimap-sections
dgileadi Aug 30, 2023
8b94127
Merge branch 'main' into minimap-sections
alexdima Aug 31, 2023
9bece88
Merge branch 'main' into minimap-sections
alexdima Sep 14, 2023
649cbe6
Merge branch 'main' into minimap-sections
dgileadi Sep 26, 2023
9f872a4
Calculate minimap section headers asynchronously
dgileadi Oct 5, 2023
ab6c78f
Merge branch 'main' into minimap-sections
dgileadi Nov 1, 2023
5172d10
Merge branch 'main' into minimap-sections
dgileadi Jan 10, 2024
bcb91d1
Merge branch 'main' into minimap-sections
dgileadi Feb 1, 2024
7e0a991
Merge branch 'main' into minimap-sections
dgileadi Feb 26, 2024
28ccae6
Merge branch 'main' into minimap-sections
dgileadi Mar 7, 2024
d1f5aeb
Merge branch 'main' into minimap-sections
dgileadi Mar 15, 2024
c6d4017
Merge branch 'main' into minimap-sections
alexdima Mar 16, 2024
21ccd26
Simplify change
alexdima Mar 16, 2024
fd24961
Avoid font variable duplication
alexdima Mar 18, 2024
0eb9c78
Fix issue introduced earlier
alexdima Mar 18, 2024
27c1186
Recompute section headers when the language configuration changes
alexdima Mar 18, 2024
81dd321
Fix problem in constructing region header range
alexdima Mar 18, 2024
f5f7032
Parse mark headers in the entire file and then filter out the ones no…
alexdima Mar 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/vs/base/browser/fonts.ts
Original file line number Diff line number Diff line change
@@ -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';
11 changes: 11 additions & 0 deletions src/vs/editor/browser/services/editorWorkerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
178 changes: 166 additions & 12 deletions src/vs/editor/browser/viewParts/minimap/minimap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
) {
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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[] = [];
Expand All @@ -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();
}
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
Expand Down
33 changes: 33 additions & 0 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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,
Expand Down Expand Up @@ -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.")
}
}
);
Expand All @@ -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),
};
}
}
Expand Down
Loading
Loading