handle nested regions

pull/4287/head
Divyansh Singh 7 months ago
parent c9ce4e0c81
commit bd8b8e2772

@ -99,145 +99,228 @@ describe('node/markdown/plugins/snippet', () => {
}) })
}) })
test('rawPathToToken', () => { describe('rawPathToToken', () => {
rawPathTokenMap.forEach(([rawPath, token]) => { test.each(rawPathTokenMap)('%s', (rawPath, token) => {
expect(removeEmptyKeys(rawPathToToken(rawPath))).toEqual(token) expect(removeEmptyKeys(rawPathToToken(rawPath))).toEqual(token)
}) })
}) })
describe('findRegion', () => { describe('findRegion', () => {
test('when c# region with matching tag', () => { it('returns null when no region markers are present', () => {
const lines = `Console.WriteLine("Before region"); const lines = ['function foo() {', ' console.log("hello");', '}']
#region hello expect(findRegion(lines, 'foo')).toBeNull()
Console.WriteLine("Hello, World!"); })
#endregion hello
Console.WriteLine("After region");`.split('\n')
const result = findRegion(lines, 'hello')
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( it('ignores non-matching region names', () => {
'Console.WriteLine("Hello, World!");' const lines = [
) '// #region regionA',
'some code here',
'// #endregion regionA'
]
expect(findRegion(lines, 'regionC')).toBeNull()
}) })
test('when c# region is not indented with spaces and no matching tag', () => {
const lines = `Console.WriteLine("Before region");
#region hello
Console.WriteLine("Hello, World!");
#endregion
Console.WriteLine("After region");`.split('\n')
const result = findRegion(lines, 'hello')
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( it('returns null if a region start marker exists without a matching end marker', () => {
'Console.WriteLine("Hello, World!");' const lines = [
) '// #region missingEnd',
'console.log("inside region");',
'console.log("still inside");'
]
expect(findRegion(lines, 'missingEnd')).toBeNull()
}) })
test('when c# region is indented with spaces and no matching tag', () => {
const lines = ` Console.WriteLine("Before region");
#region hello
Console.WriteLine("Hello, World!");
#endregion hello
Console.WriteLine("After region");`.split('\n')
const result = findRegion(lines, 'hello')
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( it('returns null if an end marker exists without a preceding start marker', () => {
const lines = [
'// #endregion ghostRegion',
'console.log("stray end marker");'
]
expect(findRegion(lines, 'ghostRegion')).toBeNull()
})
it('detects C#/JavaScript style region markers with matching tags', () => {
const lines = [
'Console.WriteLine("Before region");',
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion hello',
'Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");' 'Console.WriteLine("Hello, World!");'
) )
}
}) })
test('when c# region with matching tag', () => {
const lines = `Console.WriteLine("Before region");
#region hello
Console.WriteLine("Hello, World!");
#endregion hello
Console.WriteLine("After region");`.split('\n')
const result = findRegion(lines, 'hello')
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( it('detects region markers even when the end marker omits the region name', () => {
const lines = [
'Console.WriteLine("Before region");',
'#region hello',
'Console.WriteLine("Hello, World!");',
'#endregion',
'Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'Console.WriteLine("Hello, World!");' 'Console.WriteLine("Hello, World!");'
) )
}
}) })
test('when c# region is not indented with spaces and no matching tag', () => {
const lines = `Console.WriteLine("Before region");
#region hello
Console.WriteLine("Hello, World!");
#endregion
Console.WriteLine("After region");`.split('\n')
const result = findRegion(lines, 'hello')
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( it('handles indented region markers correctly', () => {
const lines = [
' Console.WriteLine("Before region");',
' #region hello',
' Console.WriteLine("Hello, World!");',
' #endregion hello',
' Console.WriteLine("After region");'
]
const result = findRegion(lines, 'hello')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Hello, World!");' ' Console.WriteLine("Hello, World!");'
) )
}
}) })
test('when typescript region has matching tag', () => { it('detects TypeScript style region markers', () => {
const lines = `let regexp: RegExp[] = [] const lines = [
// #region foo 'let regexp: RegExp[] = [];',
let start = -1 '// #region foo',
// #endregion foo`.split('\n') 'let start = -1;',
'// #endregion foo'
]
const result = findRegion(lines, 'foo') const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( if (result) {
'let start = -1' expect(lines.slice(result.start, result.end).join('\n')).toBe(
'let start = -1;'
) )
}
}) })
test('when typescript region is indented with spaces and no matching tag', () => {
const lines = ` let regexp: RegExp[] = []
// #region foo
let start = -1
// #endregion`.split('\n')
const result = findRegion(lines, 'foo')
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( it('detects CSS style region markers', () => {
' let start = -1' const lines = [
'.body-content {',
'/* #region foo */',
' padding-left: 15px;',
'/* #endregion foo */',
' padding-right: 15px;',
'}'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' padding-left: 15px;'
) )
}
}) })
test('when css region has matching tag', () => { it('detects HTML style region markers', () => {
const lines = `.body-content { const lines = [
/* #region foo */ '<div>Some content</div>',
padding-left: 15px; '<!-- #region foo -->',
/* #endregion foo */ ' <h1>Hello world</h1>',
padding-right: 15px; '<!-- #endregion foo -->',
}`.split('\n') '<div>Other content</div>'
]
const result = findRegion(lines, 'foo') const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( if (result) {
' padding-left: 15px;' expect(lines.slice(result.start, result.end).join('\n')).toBe(
' <h1>Hello world</h1>'
) )
}
}) })
test('when css region is indented with spaces and no matching tag', () => {
const lines = `.body-content {
/* #region foo */
padding-left: 15px;
/* #endregion */
padding-right: 15px;
}`.split('\n')
const result = findRegion(lines, 'foo')
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( it('detects Visual Basic style region markers (with case-insensitive "End")', () => {
' padding-left: 15px;' const lines = [
'Console.WriteLine("VB")',
'#Region VBRegion',
' Console.WriteLine("Inside region")',
'#End Region VBRegion',
'Console.WriteLine("Done")'
]
const result = findRegion(lines, 'VBRegion')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
' Console.WriteLine("Inside region")'
) )
}
}) })
test('when html region has matching tag', () => { it('detects Bat style region markers', () => {
const lines = `<!-- #region foo --> const lines = ['::#region foo', 'echo off', '::#endregion foo']
<h1>Hello world</h1>
<!-- #endregion foo -->
<p>more text</p>`.split('\n')
const result = findRegion(lines, 'foo') const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( if (result) {
' <h1>Hello world</h1>' expect(lines.slice(result.start, result.end).join('\n')).toBe(
'echo off'
) )
}
}) })
test('when html region is indented with spaces and no matching tag', () => {
const lines = ` <!-- #region foo --> it('detects C/C++ style region markers using #pragma', () => {
<h1>Hello world</h1> const lines = [
<!-- #endregion foo --> '#pragma region foo',
<p>more text</p>`.split('\n') 'int a = 1;',
'#pragma endregion foo'
]
const result = findRegion(lines, 'foo') const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'int a = 1;'
)
}
})
expect(lines.slice(result?.start, result?.end).join('\n')).toBe( it('returns the first complete region when multiple regions exist', () => {
' <h1>Hello world</h1>' const lines = [
'// #region foo',
'first region content',
'// #endregion foo',
'// #region foo',
'second region content',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
expect(lines.slice(result.start, result.end).join('\n')).toBe(
'first region content'
) )
}
})
it('handles nested regions with different names properly', () => {
const lines = [
'// #region foo',
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar',
'// #endregion foo'
]
const result = findRegion(lines, 'foo')
expect(result).not.toBeNull()
if (result) {
const extracted = lines.slice(result.start, result.end).join('\n')
const expected = [
"console.log('line before nested');",
'// #region bar',
"console.log('nested content');",
'// #endregion bar'
].join('\n')
expect(extracted).toBe(expected)
}
}) })
}) })
}) })

@ -51,22 +51,8 @@ export function dedent(text: string): string {
return text return text
} }
function testLine(
line: string,
regexp: RegExp,
regionName: string,
end: boolean = false
) {
const [full, tag, name] = regexp.exec(line.trim()) || []
return full && tag && end
? true
: name === regionName &&
tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/)
}
export function findRegion(lines: Array<string>, regionName: string) { export function findRegion(lines: Array<string>, regionName: string) {
const regionRegexps = [ const regionRegexps: [RegExp, RegExp][] = [
[ [
/^[ \t]*\/\/ ?#?(region) ([\w*-]+)$/, /^[ \t]*\/\/ ?#?(region) ([\w*-]+)$/,
/^[ \t]*\/\/ ?#?(endregion) ?([\w*-]*)$/ /^[ \t]*\/\/ ?#?(endregion) ?([\w*-]*)$/
@ -82,20 +68,54 @@ export function findRegion(lines: Array<string>, regionName: string) {
[/^[ \t]*# ?(region) ([\w*-]+)$/, /^[ \t]*# ?(endregion) ?([\w*-]*)$/] // C#, PHP, Powershell, Python, perl & misc [/^[ \t]*# ?(region) ([\w*-]+)$/, /^[ \t]*# ?(endregion) ?([\w*-]*)$/] // C#, PHP, Powershell, Python, perl & misc
] ]
let regexp: RegExp[] = [] let chosenRegex: [RegExp, RegExp] | null = null
let start = -1 let startLine = -1
// find the regex pair for a start marker that matches the given region name
for (const [lineId, line] of lines.entries()) { for (let i = 0; i < lines.length; i++) {
if (regexp.length === 0) { const line = lines[i].trim()
for (const reg of regionRegexps) { for (const [startRegex, endRegex] of regionRegexps) {
if (testLine(line, reg[0], regionName)) { const startMatch = startRegex.exec(line)
start = lineId + 1 if (
regexp = reg startMatch &&
startMatch[2] === regionName &&
/^[rR]egion$/.test(startMatch[1])
) {
chosenRegex = [startRegex, endRegex]
startLine = i + 1
break break
} }
} }
} else if (testLine(line, regexp[1], regionName, true)) { if (chosenRegex) break
return { start, end: lineId, regexp } }
if (!chosenRegex) return null
const [startRegex, endRegex] = chosenRegex
let counter = 1
// scan the rest of the lines to find the matching end marker, handling nested markers
for (let i = startLine; i < lines.length; i++) {
const trimmed = lines[i].trim()
// check for an inner start marker for the same region
const startMatch = startRegex.exec(trimmed)
if (
startMatch &&
startMatch[2] === regionName &&
/^[rR]egion$/.test(startMatch[1])
) {
counter++
continue
}
// check for an end marker for the same region
const endMatch = endRegex.exec(trimmed)
if (
endMatch &&
// allow empty region name on the end marker as a fallback
(endMatch[2] === regionName || endMatch[2] === '') &&
/^[Ee]nd ?[rR]egion$/.test(endMatch[1])
) {
counter--
if (counter === 0) {
return { start: startLine, end: i, regexp: chosenRegex }
}
} }
} }

Loading…
Cancel
Save