Path: blob/main/src/vs/editor/test/common/services/findSectionHeaders.test.ts
3296 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 * as assert from 'assert';6import { FindSectionHeaderOptions, ISectionHeaderFinderTarget, findSectionHeaders } from '../../../common/services/findSectionHeaders.js';7import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';89class TestSectionHeaderFinderTarget implements ISectionHeaderFinderTarget {10constructor(private readonly lines: string[]) { }1112getLineCount(): number {13return this.lines.length;14}1516getLineContent(lineNumber: number): string {17return this.lines[lineNumber - 1];18}19}2021suite('FindSectionHeaders', () => {2223ensureNoDisposablesAreLeakedInTestSuite();2425test('finds simple section headers', () => {26const model = new TestSectionHeaderFinderTarget([27'regular line',28'MARK: My Section',29'another line',30'MARK: Another Section',31'last line'32]);3334const options: FindSectionHeaderOptions = {35findRegionSectionHeaders: false,36findMarkSectionHeaders: true,37markSectionHeaderRegex: 'MARK:\\s*(?<label>.*)$'38};3940const headers = findSectionHeaders(model, options);41assert.strictEqual(headers.length, 2);4243assert.strictEqual(headers[0].text, 'My Section');44assert.strictEqual(headers[0].range.startLineNumber, 2);45assert.strictEqual(headers[0].range.endLineNumber, 2);4647assert.strictEqual(headers[1].text, 'Another Section');48assert.strictEqual(headers[1].range.startLineNumber, 4);49assert.strictEqual(headers[1].range.endLineNumber, 4);50});5152test('finds section headers with separators', () => {53const model = new TestSectionHeaderFinderTarget([54'regular line',55'MARK: -My Section',56'another line',57'MARK: - Another Section',58'last line'59]);6061const options: FindSectionHeaderOptions = {62findRegionSectionHeaders: false,63findMarkSectionHeaders: true,64markSectionHeaderRegex: 'MARK:\\s*(?<separator>-?)\\s*(?<label>.*)$'65};6667const headers = findSectionHeaders(model, options);68assert.strictEqual(headers.length, 2);6970assert.strictEqual(headers[0].text, 'My Section');71assert.strictEqual(headers[0].hasSeparatorLine, true);7273assert.strictEqual(headers[1].text, 'Another Section');74assert.strictEqual(headers[1].hasSeparatorLine, true);75});7677test('finds multi-line section headers with separators', () => {78const model = new TestSectionHeaderFinderTarget([79'regular line',80'// ==========',81'// My Section',82'// ==========',83'code...',84'// ==========',85'// Another Section',86'// ==========',87'more code...'88]);8990const options: FindSectionHeaderOptions = {91findRegionSectionHeaders: false,92findMarkSectionHeaders: true,93markSectionHeaderRegex: '^\/\/ =+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ =+$'94};9596const headers = findSectionHeaders(model, options);97assert.strictEqual(headers.length, 2);9899assert.strictEqual(headers[0].text, 'My Section');100assert.strictEqual(headers[0].range.startLineNumber, 2);101assert.strictEqual(headers[0].range.endLineNumber, 4);102103assert.strictEqual(headers[1].text, 'Another Section');104assert.strictEqual(headers[1].range.startLineNumber, 6);105assert.strictEqual(headers[1].range.endLineNumber, 8);106});107108test('handles overlapping multi-line section headers correctly', () => {109const model = new TestSectionHeaderFinderTarget([110'// ==========',111'// Section 1',112'// ==========',113'// ==========', // This line starts another header114'// Section 2',115'// ==========',116]);117118const options: FindSectionHeaderOptions = {119findRegionSectionHeaders: false,120findMarkSectionHeaders: true,121markSectionHeaderRegex: '^\/\/ =+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ =+$'122};123124const headers = findSectionHeaders(model, options);125assert.strictEqual(headers.length, 2);126127assert.strictEqual(headers[0].text, 'Section 1');128assert.strictEqual(headers[0].range.startLineNumber, 1);129assert.strictEqual(headers[0].range.endLineNumber, 3);130131assert.strictEqual(headers[1].text, 'Section 2');132assert.strictEqual(headers[1].range.startLineNumber, 4);133assert.strictEqual(headers[1].range.endLineNumber, 6);134});135136test('section headers must be in comments when specified', () => {137const model = new TestSectionHeaderFinderTarget([138'// ==========',139'// Section 1', // This one is in a comment140'// ==========',141'==========', // This one isn't142'Section 2',143'=========='144]);145146const options: FindSectionHeaderOptions = {147findRegionSectionHeaders: false,148findMarkSectionHeaders: true,149markSectionHeaderRegex: '^(?:\/\/ )?=+\\n^(?:\/\/ )?(?<label>[^\\n]+?)\\n^(?:\/\/ )?=+$'150};151152// Both patterns match, but the second one should be filtered out by the token check153const headers = findSectionHeaders(model, options);154assert.strictEqual(headers[0].shouldBeInComments, true);155});156157test('handles section headers at chunk boundaries', () => {158// Create enough lines to ensure we cross chunk boundaries159const lines: string[] = [];160for (let i = 0; i < 150; i++) {161lines.push('line ' + i);162}163164// Add headers near the chunk boundary (chunk size is 100)165lines[97] = '// ==========';166lines[98] = '// Section 1';167lines[99] = '// ==========';168lines[100] = '// ==========';169lines[101] = '// Section 2';170lines[102] = '// ==========';171172const model = new TestSectionHeaderFinderTarget(lines);173174const options: FindSectionHeaderOptions = {175findRegionSectionHeaders: false,176findMarkSectionHeaders: true,177markSectionHeaderRegex: '^\/\/ =+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ =+$'178};179180const headers = findSectionHeaders(model, options);181assert.strictEqual(headers.length, 2);182183assert.strictEqual(headers[0].text, 'Section 1');184assert.strictEqual(headers[0].range.startLineNumber, 98);185assert.strictEqual(headers[0].range.endLineNumber, 100);186187assert.strictEqual(headers[1].text, 'Section 2');188assert.strictEqual(headers[1].range.startLineNumber, 101);189assert.strictEqual(headers[1].range.endLineNumber, 103);190});191192test('handles empty regex gracefully without infinite loop', () => {193const model = new TestSectionHeaderFinderTarget([194'line 1',195'line 2',196'line 3'197]);198199const options: FindSectionHeaderOptions = {200findRegionSectionHeaders: false,201findMarkSectionHeaders: true,202markSectionHeaderRegex: '' // Empty string that would cause infinite loop203};204205const headers = findSectionHeaders(model, options);206assert.strictEqual(headers.length, 0, 'Should return no headers for empty regex');207});208209test('handles whitespace-only regex gracefully without infinite loop', () => {210const model = new TestSectionHeaderFinderTarget([211'line 1',212'line 2',213'line 3'214]);215216const options: FindSectionHeaderOptions = {217findRegionSectionHeaders: false,218findMarkSectionHeaders: true,219markSectionHeaderRegex: ' ' // Whitespace that would cause infinite loop220};221222const headers = findSectionHeaders(model, options);223assert.strictEqual(headers.length, 0, 'Should return no headers for whitespace-only regex');224});225226test('correctly advances past matches without infinite loop', () => {227const model = new TestSectionHeaderFinderTarget([228'// ==========',229'// Section 1',230'// ==========',231'some code',232'// ==========',233'// Section 2',234'// ==========',235'more code',236'// ==========',237'// Section 3',238'// ==========',239]);240241const options: FindSectionHeaderOptions = {242findRegionSectionHeaders: false,243findMarkSectionHeaders: true,244markSectionHeaderRegex: '^\/\/ =+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ =+$'245};246247const headers = findSectionHeaders(model, options);248assert.strictEqual(headers.length, 3, 'Should find all three section headers');249assert.strictEqual(headers[0].text, 'Section 1');250assert.strictEqual(headers[1].text, 'Section 2');251assert.strictEqual(headers[2].text, 'Section 3');252});253254test('handles consecutive section headers correctly', () => {255const model = new TestSectionHeaderFinderTarget([256'// ==========',257'// Section 1',258'// ==========',259'// ==========', // This line is both the end of Section 1 and start of Section 2260'// Section 2',261'// ==========',262]);263264const options: FindSectionHeaderOptions = {265findRegionSectionHeaders: false,266findMarkSectionHeaders: true,267markSectionHeaderRegex: '^\/\/ =+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ =+$'268};269270const headers = findSectionHeaders(model, options);271assert.strictEqual(headers.length, 2, 'Should find both section headers');272assert.strictEqual(headers[0].text, 'Section 1');273assert.strictEqual(headers[1].text, 'Section 2');274});275276test('handles nested separators correctly', () => {277const model = new TestSectionHeaderFinderTarget([278'// ==============',279'// Major Section',280'// ==============',281'',282'// ----------',283'// Subsection',284'// ----------',285]);286287const options: FindSectionHeaderOptions = {288findRegionSectionHeaders: false,289findMarkSectionHeaders: true,290markSectionHeaderRegex: '^\/\/ [-=]+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ [-=]+$'291};292293const headers = findSectionHeaders(model, options);294assert.strictEqual(headers.length, 2, 'Should find both section headers');295assert.strictEqual(headers[0].text, 'Major Section');296assert.strictEqual(headers[1].text, 'Subsection');297});298299test('handles section headers at chunk boundaries correctly', () => {300const lines: string[] = [];301// Fill up to near the chunk boundary (chunk size is 100)302for (let i = 0; i < 97; i++) {303lines.push(`line ${i}`);304}305306// Add a section header that would cross the chunk boundary307lines.push('// =========='); // line 97308lines.push('// Section 1'); // line 98309lines.push('// =========='); // line 99310lines.push('// =========='); // line 100 (chunk boundary)311lines.push('// Section 2'); // line 101312lines.push('// =========='); // line 102313314// Add more content after315for (let i = 103; i < 150; i++) {316lines.push(`line ${i}`);317}318319const model = new TestSectionHeaderFinderTarget(lines);320321const options: FindSectionHeaderOptions = {322findRegionSectionHeaders: false,323findMarkSectionHeaders: true,324markSectionHeaderRegex: '^\/\/ =+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ =+$'325};326327const headers = findSectionHeaders(model, options);328assert.strictEqual(headers.length, 2, 'Should find both section headers across chunk boundary');329330assert.strictEqual(headers[0].text, 'Section 1');331assert.strictEqual(headers[0].range.startLineNumber, 98);332assert.strictEqual(headers[0].range.endLineNumber, 100);333334assert.strictEqual(headers[1].text, 'Section 2');335assert.strictEqual(headers[1].range.startLineNumber, 101);336assert.strictEqual(headers[1].range.endLineNumber, 103);337});338339test('handles overlapping section headers without duplicates', () => {340const model = new TestSectionHeaderFinderTarget([341'// ==========', // Line 1342'// Section 1', // Line 2 - This is part of first header343'// ==========', // Line 3 - This is the end of first344'// Section 2', // Line 4 - This is not a header345'// ==========', // Line 5346'// ==========', // Line 6 - Start of second header347'// Section 3', // Line 7348'// ===========' // Line 8349]);350351const options: FindSectionHeaderOptions = {352findRegionSectionHeaders: false,353findMarkSectionHeaders: true,354markSectionHeaderRegex: '^\/\/ =+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ =+$'355};356357const headers = findSectionHeaders(model, options);358assert.strictEqual(headers.length, 2);359360assert.strictEqual(headers[0].text, 'Section 1');361assert.strictEqual(headers[0].range.startLineNumber, 1);362assert.strictEqual(headers[0].range.endLineNumber, 3);363364// assert.strictEqual(headers[1].text, 'Section 2');365// assert.strictEqual(headers[1].range.startLineNumber, 3);366// assert.strictEqual(headers[1].range.endLineNumber, 5);367368assert.strictEqual(headers[1].text, 'Section 3');369assert.strictEqual(headers[1].range.startLineNumber, 6);370assert.strictEqual(headers[1].range.endLineNumber, 8);371});372373test('handles partially overlapping multiline section headers correctly', () => {374const model = new TestSectionHeaderFinderTarget([375'// ================', // Line 1376'// Major Section 1', // Line 2377'// ================', // Line 3378'// --------', // Line 4 - Start of subsection that overlaps with end of major section379'// Subsection 1.1', // Line 5380'// --------', // Line 6381'// ================', // Line 7382'// Major Section 2', // Line 8383'// ================', // Line 9384]);385386const options: FindSectionHeaderOptions = {387findRegionSectionHeaders: false,388findMarkSectionHeaders: true,389markSectionHeaderRegex: '^\/\/ [-=]+\\n^\/\/ (?<label>[^\\n]+?)\\n^\/\/ [-=]+$'390};391392const headers = findSectionHeaders(model, options);393assert.strictEqual(headers.length, 3);394395assert.strictEqual(headers[0].text, 'Major Section 1');396assert.strictEqual(headers[0].range.startLineNumber, 1);397assert.strictEqual(headers[0].range.endLineNumber, 3);398399assert.strictEqual(headers[1].text, 'Subsection 1.1');400assert.strictEqual(headers[1].range.startLineNumber, 4);401assert.strictEqual(headers[1].range.endLineNumber, 6);402403assert.strictEqual(headers[2].text, 'Major Section 2');404assert.strictEqual(headers[2].range.startLineNumber, 7);405assert.strictEqual(headers[2].range.endLineNumber, 9);406});407});408409410