Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/services/findSectionHeaders.ts
3295 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IRange } from '../core/range.js';
7
import { FoldingRules } from '../languages/languageConfiguration.js';
8
import { isMultilineRegexSource } from '../model/textModelSearch.js';
9
import { regExpLeadsToEndlessLoop } from '../../../base/common/strings.js';
10
11
export interface ISectionHeaderFinderTarget {
12
getLineCount(): number;
13
getLineContent(lineNumber: number): string;
14
}
15
16
export interface FindSectionHeaderOptions {
17
foldingRules?: FoldingRules;
18
findRegionSectionHeaders: boolean;
19
findMarkSectionHeaders: boolean;
20
markSectionHeaderRegex: string;
21
}
22
23
export interface SectionHeader {
24
/**
25
* The location of the header text in the text model.
26
*/
27
range: IRange;
28
/**
29
* The section header text.
30
*/
31
text: string;
32
/**
33
* Whether the section header includes a separator line.
34
*/
35
hasSeparatorLine: boolean;
36
/**
37
* This section should be omitted before rendering if it's not in a comment.
38
*/
39
shouldBeInComments: boolean;
40
}
41
42
const trimDashesRegex = /^-+|-+$/g;
43
44
const CHUNK_SIZE = 100;
45
const MAX_SECTION_LINES = 5;
46
47
/**
48
* Find section headers in the model.
49
*
50
* @param model the text model to search in
51
* @param options options to search with
52
* @returns an array of section headers
53
*/
54
export function findSectionHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] {
55
let headers: SectionHeader[] = [];
56
if (options.findRegionSectionHeaders && options.foldingRules?.markers) {
57
const regionHeaders = collectRegionHeaders(model, options);
58
headers = headers.concat(regionHeaders);
59
}
60
if (options.findMarkSectionHeaders) {
61
const markHeaders = collectMarkHeaders(model, options);
62
headers = headers.concat(markHeaders);
63
}
64
return headers;
65
}
66
67
function collectRegionHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] {
68
const regionHeaders: SectionHeader[] = [];
69
const endLineNumber = model.getLineCount();
70
for (let lineNumber = 1; lineNumber <= endLineNumber; lineNumber++) {
71
const lineContent = model.getLineContent(lineNumber);
72
const match = lineContent.match(options.foldingRules!.markers!.start);
73
if (match) {
74
const range = { startLineNumber: lineNumber, startColumn: match[0].length + 1, endLineNumber: lineNumber, endColumn: lineContent.length + 1 };
75
if (range.endColumn > range.startColumn) {
76
const sectionHeader = {
77
range,
78
...getHeaderText(lineContent.substring(match[0].length)),
79
shouldBeInComments: false
80
};
81
if (sectionHeader.text || sectionHeader.hasSeparatorLine) {
82
regionHeaders.push(sectionHeader);
83
}
84
}
85
}
86
}
87
return regionHeaders;
88
}
89
90
export function collectMarkHeaders(model: ISectionHeaderFinderTarget, options: FindSectionHeaderOptions): SectionHeader[] {
91
const markHeaders: SectionHeader[] = [];
92
const endLineNumber = model.getLineCount();
93
94
// Validate regex to prevent infinite loops
95
if (!options.markSectionHeaderRegex || options.markSectionHeaderRegex.trim() === '') {
96
return markHeaders;
97
}
98
99
// Create regex with flags for:
100
// - 'd' for indices to get proper match positions
101
// - 'm' for multi-line mode so ^ and $ match line starts/ends
102
// - 's' for dot-all mode so . matches newlines
103
const multiline = isMultilineRegexSource(options.markSectionHeaderRegex);
104
const regex = new RegExp(options.markSectionHeaderRegex, `gdm${multiline ? 's' : ''}`);
105
106
// Check if the regex would lead to an endless loop
107
if (regExpLeadsToEndlessLoop(regex)) {
108
return markHeaders;
109
}
110
111
// Process text in overlapping chunks for better performance
112
for (let startLine = 1; startLine <= endLineNumber; startLine += CHUNK_SIZE - MAX_SECTION_LINES) {
113
const endLine = Math.min(startLine + CHUNK_SIZE - 1, endLineNumber);
114
const lines: string[] = [];
115
116
// Collect lines for the current chunk
117
for (let i = startLine; i <= endLine; i++) {
118
lines.push(model.getLineContent(i));
119
}
120
121
const text = lines.join('\n');
122
regex.lastIndex = 0;
123
124
let match: RegExpExecArray | null;
125
while ((match = regex.exec(text)) !== null) {
126
// Calculate which line this match starts on by counting newlines before it
127
const precedingText = text.substring(0, match.index);
128
const lineOffset = (precedingText.match(/\n/g) || []).length;
129
const lineNumber = startLine + lineOffset;
130
131
// Calculate match height to check overlap properly
132
const matchLines = match[0].split('\n');
133
const matchHeight = matchLines.length;
134
const matchEndLine = lineNumber + matchHeight - 1;
135
136
// Calculate start column - need to find the start of the line containing the match
137
const lineStartIndex = precedingText.lastIndexOf('\n') + 1;
138
const startColumn = match.index - lineStartIndex + 1;
139
140
// Calculate end column - need to handle multi-line matches
141
const lastMatchLine = matchLines[matchLines.length - 1];
142
const endColumn = matchHeight === 1 ? startColumn + match[0].length : lastMatchLine.length + 1;
143
144
const range = {
145
startLineNumber: lineNumber,
146
startColumn,
147
endLineNumber: matchEndLine,
148
endColumn
149
};
150
151
const text2 = (match.groups ?? {})['label'] ?? '';
152
const hasSeparatorLine = ((match.groups ?? {})['separator'] ?? '') !== '';
153
154
const sectionHeader = {
155
range,
156
text: text2,
157
hasSeparatorLine,
158
shouldBeInComments: true
159
};
160
161
if (sectionHeader.text || sectionHeader.hasSeparatorLine) {
162
// only push if the previous one doesn't have this same linbe
163
if (markHeaders.length === 0 || markHeaders[markHeaders.length - 1].range.endLineNumber < sectionHeader.range.startLineNumber) {
164
markHeaders.push(sectionHeader);
165
}
166
}
167
168
// Move lastIndex past the current match to avoid infinite loop
169
regex.lastIndex = match.index + match[0].length;
170
}
171
}
172
173
return markHeaders;
174
}
175
176
function getHeaderText(text: string): { text: string; hasSeparatorLine: boolean } {
177
text = text.trim();
178
const hasSeparatorLine = text.startsWith('-');
179
text = text.replace(trimDashesRegex, '');
180
return { text, hasSeparatorLine };
181
}
182
183