Path: blob/main/src/vs/editor/common/services/findSectionHeaders.ts
3295 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IRange } from '../core/range.js';6import { FoldingRules } from '../languages/languageConfiguration.js';7import { isMultilineRegexSource } from '../model/textModelSearch.js';8import { regExpLeadsToEndlessLoop } from '../../../base/common/strings.js';910export interface ISectionHeaderFinderTarget {11getLineCount(): number;12getLineContent(lineNumber: number): string;13}1415export interface FindSectionHeaderOptions {16foldingRules?: FoldingRules;17findRegionSectionHeaders: boolean;18findMarkSectionHeaders: boolean;19markSectionHeaderRegex: string;20}2122export interface SectionHeader {23/**24* The location of the header text in the text model.25*/26range: IRange;27/**28* The section header text.29*/30text: string;31/**32* Whether the section header includes a separator line.33*/34hasSeparatorLine: boolean;35/**36* This section should be omitted before rendering if it's not in a comment.37*/38shouldBeInComments: boolean;39}4041const trimDashesRegex = /^-+|-+$/g;4243const CHUNK_SIZE = 100;44const MAX_SECTION_LINES = 5;4546/**47* Find section headers in the model.48*49* @param model the text model to search in50* @param options options to search with51* @returns an array of section headers52*/53export function findSectionHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] {54let headers: SectionHeader[] = [];55if (options.findRegionSectionHeaders && options.foldingRules?.markers) {56const regionHeaders = collectRegionHeaders(model, options);57headers = headers.concat(regionHeaders);58}59if (options.findMarkSectionHeaders) {60const markHeaders = collectMarkHeaders(model, options);61headers = headers.concat(markHeaders);62}63return headers;64}6566function collectRegionHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] {67const regionHeaders: SectionHeader[] = [];68const endLineNumber = model.getLineCount();69for (let lineNumber = 1; lineNumber <= endLineNumber; lineNumber++) {70const lineContent = model.getLineContent(lineNumber);71const match = lineContent.match(options.foldingRules!.markers!.start);72if (match) {73const range = { startLineNumber: lineNumber, startColumn: match[0].length + 1, endLineNumber: lineNumber, endColumn: lineContent.length + 1 };74if (range.endColumn > range.startColumn) {75const sectionHeader = {76range,77...getHeaderText(lineContent.substring(match[0].length)),78shouldBeInComments: false79};80if (sectionHeader.text || sectionHeader.hasSeparatorLine) {81regionHeaders.push(sectionHeader);82}83}84}85}86return regionHeaders;87}8889export function collectMarkHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] {90const markHeaders: SectionHeader[] = [];91const endLineNumber = model.getLineCount();9293// Validate regex to prevent infinite loops94if (!options.markSectionHeaderRegex || options.markSectionHeaderRegex.trim() === '') {95return markHeaders;96}9798// Create regex with flags for:99// - 'd' for indices to get proper match positions100// - 'm' for multi-line mode so ^ and $ match line starts/ends101// - 's' for dot-all mode so . matches newlines102const multiline = isMultilineRegexSource(options.markSectionHeaderRegex);103const regex = new RegExp(options.markSectionHeaderRegex, `gdm${multiline ? 's' : ''}`);104105// Check if the regex would lead to an endless loop106if (regExpLeadsToEndlessLoop(regex)) {107return markHeaders;108}109110// Process text in overlapping chunks for better performance111for (let startLine = 1; startLine <= endLineNumber; startLine += CHUNK_SIZE - MAX_SECTION_LINES) {112const endLine = Math.min(startLine + CHUNK_SIZE - 1, endLineNumber);113const lines: string[] = [];114115// Collect lines for the current chunk116for (let i = startLine; i <= endLine; i++) {117lines.push(model.getLineContent(i));118}119120const text = lines.join('\n');121regex.lastIndex = 0;122123let match: RegExpExecArray | null;124while ((match = regex.exec(text)) !== null) {125// Calculate which line this match starts on by counting newlines before it126const precedingText = text.substring(0, match.index);127const lineOffset = (precedingText.match(/\n/g) || []).length;128const lineNumber = startLine + lineOffset;129130// Calculate match height to check overlap properly131const matchLines = match[0].split('\n');132const matchHeight = matchLines.length;133const matchEndLine = lineNumber + matchHeight - 1;134135// Calculate start column - need to find the start of the line containing the match136const lineStartIndex = precedingText.lastIndexOf('\n') + 1;137const startColumn = match.index - lineStartIndex + 1;138139// Calculate end column - need to handle multi-line matches140const lastMatchLine = matchLines[matchLines.length - 1];141const endColumn = matchHeight === 1 ? startColumn + match[0].length : lastMatchLine.length + 1;142143const range = {144startLineNumber: lineNumber,145startColumn,146endLineNumber: matchEndLine,147endColumn148};149150const text2 = (match.groups ?? {})['label'] ?? '';151const hasSeparatorLine = ((match.groups ?? {})['separator'] ?? '') !== '';152153const sectionHeader = {154range,155text: text2,156hasSeparatorLine,157shouldBeInComments: true158};159160if (sectionHeader.text || sectionHeader.hasSeparatorLine) {161// only push if the previous one doesn't have this same linbe162if (markHeaders.length === 0 || markHeaders[markHeaders.length - 1].range.endLineNumber < sectionHeader.range.startLineNumber) {163markHeaders.push(sectionHeader);164}165}166167// Move lastIndex past the current match to avoid infinite loop168regex.lastIndex = match.index + match[0].length;169}170}171172return markHeaders;173}174175function getHeaderText(text: string): { text: string; hasSeparatorLine: boolean } {176text = text.trim();177const hasSeparatorLine = text.startsWith('-');178text = text.replace(trimDashesRegex, '');179return { text, hasSeparatorLine };180}181182183