* Finds collapsible regions in JSON code by matching braces and detecting long strings. * A region is collapsible if it spans multiple lines OR contains a long string value. * Properly handles braces inside JSON strings by tracking string boundaries with correct * escape sequence handling (counts
(lines: string[])
| 176 | * @returns Map of start line index to CollapsibleRegion |
| 177 | */ |
| 178 | function findCollapsibleRegions(lines: string[]): Map<number, CollapsibleRegion> { |
| 179 | const regions = new Map<number, CollapsibleRegion>() |
| 180 | const stringRegions = new Map<number, CollapsibleRegion>() |
| 181 | const stack: { char: '{' | '['; line: number }[] = [] |
| 182 | |
| 183 | for (let i = 0; i < lines.length; i++) { |
| 184 | const line = lines[i] |
| 185 | |
| 186 | // Detect collapsible string values (long strings on a single line) |
| 187 | const stringMatch = line.match(STRING_VALUE_REGEX) |
| 188 | if (stringMatch) { |
| 189 | const colonIdx = line.indexOf('":') |
| 190 | if (colonIdx !== -1) { |
| 191 | const valueStart = line.indexOf('"', colonIdx + 1) |
| 192 | const valueEnd = line.lastIndexOf('"') |
| 193 | if (valueStart !== -1 && valueEnd > valueStart) { |
| 194 | const stringValue = line.slice(valueStart + 1, valueEnd) |
| 195 | if (stringValue.length >= MIN_COLLAPSIBLE_STRING_LENGTH || stringValue.includes('\\n')) { |
| 196 | // Store separately to avoid conflicts with block regions |
| 197 | stringRegions.set(i, { startLine: i, endLine: i, type: 'string' }) |
| 198 | } |
| 199 | } |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | // Check for block regions, skipping characters inside strings |
| 204 | let inString = false |
| 205 | for (let j = 0; j < line.length; j++) { |
| 206 | const char = line[j] |
| 207 | |
| 208 | // Toggle string state on unescaped quotes |
| 209 | // Must count consecutive backslashes: odd = escaped quote, even = unescaped quote |
| 210 | if (char === '"') { |
| 211 | let backslashCount = 0 |
| 212 | let k = j - 1 |
| 213 | while (k >= 0 && line[k] === '\\') { |
| 214 | backslashCount++ |
| 215 | k-- |
| 216 | } |
| 217 | // Only toggle if quote is not escaped (even number of preceding backslashes) |
| 218 | if (backslashCount % 2 === 0) { |
| 219 | inString = !inString |
| 220 | } |
| 221 | continue |
| 222 | } |
| 223 | |
| 224 | // Skip braces inside strings |
| 225 | if (inString) continue |
| 226 | |
| 227 | if (char === '{' || char === '[') { |
| 228 | stack.push({ char, line: i }) |
| 229 | } else if (char === '}' || char === ']') { |
| 230 | const expected = char === '}' ? '{' : '[' |
| 231 | if (stack.length > 0 && stack[stack.length - 1].char === expected) { |
| 232 | const start = stack.pop()! |
| 233 | if (i > start.line) { |
| 234 | regions.set(start.line, { |
| 235 | startLine: start.line, |
no test coverage detected