diff --git a/src/vs/base/browser/fonts.ts b/src/vs/base/browser/fonts.ts new file mode 100644 index 0000000000000..a5e78d00bef5b --- /dev/null +++ b/src/vs/base/browser/fonts.ts @@ -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'; diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index 22d7bd9cb9105..aa1e41c89850a 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -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 { + 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 { + return this._withSyncedResources([uri]).then(proxy => { + return proxy.findSectionHeaders(uri.toString(), options); + }); + } + override dispose(): void { super.dispose(); this._disposed = true; diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 789b68836b988..427df7cd3d8f1 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -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(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(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; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 78c12001b9b87..b2a486bce5c20 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -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 { + 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 { diff --git a/src/vs/editor/common/services/editorWorker.ts b/src/vs/editor/common/services/editorWorker.ts index 9e1cca8a460de..7e87024cafce8 100644 --- a/src/vs/editor/common/services/editorWorker.ts +++ b/src/vs/editor/common/services/editorWorker.ts @@ -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('editorWorkerService'); @@ -36,6 +37,8 @@ export interface IEditorWorkerService { canNavigateValueSet(resource: URI): boolean; navigateValueSet(resource: URI, range: IRange, up: boolean): Promise; + + findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise; } export interface IDiffComputationResult { diff --git a/src/vs/editor/common/services/findSectionHeaders.ts b/src/vs/editor/common/services/findSectionHeaders.ts new file mode 100644 index 0000000000000..08bd37097412c --- /dev/null +++ b/src/vs/editor/common/services/findSectionHeaders.ts @@ -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 }; +} diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index b9d2c719d49fe..d01db6500b346 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -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. */ diff --git a/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts new file mode 100644 index 0000000000000..f3296062d8146 --- /dev/null +++ b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts @@ -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 | 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>): 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); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 37ed9f9f4fdb0..a84a6bdcb3f73 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -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'; diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 0fa038765afbf..d1a70eba3d206 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -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, diff --git a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts index aef1e1cd2fa07..4f644203ef431 100644 --- a/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/browser/config/editorLayoutProvider.test.ts @@ -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 = { diff --git a/src/vs/editor/test/common/services/testEditorWorkerService.ts b/src/vs/editor/test/common/services/testEditorWorkerService.ts index e6693d821e99d..e7d5154f9f68e 100644 --- a/src/vs/editor/test/common/services/testEditorWorkerService.ts +++ b/src/vs/editor/test/common/services/testEditorWorkerService.ts @@ -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 { return null; } + async findSectionHeaders(uri: URI): Promise { return []; } } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 3d46d74e77288..1fef0cf2bfb47 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -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; } /** diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 23f2e2bac99dc..8fab9bc5b71cb 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -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'; diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d05d504ef77fe..ca64eb2a9303f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -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'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 60e1339879082..6fbc04ff214b9 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -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'; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts index ca476d86577d7..7743eb68f07df 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatInputWidget.ts @@ -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'; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index f35fe3e0edade..3f07223aa5222 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -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'; diff --git a/src/vs/workbench/contrib/webview/browser/themeing.ts b/src/vs/workbench/contrib/webview/browser/themeing.ts index 4fd074d18aeec..b63bfffca2d4c 100644 --- a/src/vs/workbench/contrib/webview/browser/themeing.ts +++ b/src/vs/workbench/contrib/webview/browser/themeing.ts @@ -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 {