diff --git a/src/parser.ts b/src/parser.ts index 247fe41b..95916913 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1281,42 +1281,46 @@ function sectionsToHeadingSymbols( })) } -function extractSymbolsNoTreeSitter( - content: string, - filePath: string, - language: Language, -): SymbolEntry[] { - if (language === 'markdown') return extractMarkdownSymbols(content, filePath) - if (language === 'json') return extractJsonSymbols(content, filePath) - if (language === 'yaml') return extractYamlSymbols(content, filePath) - if (language === 'toml') return extractTomlSymbols(content, filePath) - if (language === 'css') return extractCssSymbols(content, filePath) - if (language === 'dockerfile') return extractDockerfileSymbols(content, filePath) - - // New language adapters from ./languages/ - if (language === 'csharp') return extractCsharp(content, filePath).symbols - if (language === 'php') return extractPhp(content, filePath).symbols - if (language === 'html') { +type SymbolExtractor = (content: string, filePath: string) => SymbolEntry[] + +// One entry per non-tree-sitter Language. Adding a new adapter is one map entry rather than +// a new `if` branch; html/liquid keep their extra sectionsToHeadingSymbols composition inline. +const NO_TREE_SITTER_EXTRACTORS: Partial> = { + markdown: extractMarkdownSymbols, + json: extractJsonSymbols, + yaml: extractYamlSymbols, + toml: extractTomlSymbols, + css: extractCssSymbols, + dockerfile: extractDockerfileSymbols, + csharp: (content, filePath) => extractCsharp(content, filePath).symbols, + php: (content, filePath) => extractPhp(content, filePath).symbols, + html: (content, filePath) => { const r = extractHtml(content, filePath) return [...r.symbols, ...sectionsToHeadingSymbols(r.sections, filePath)] - } - if (language === 'liquid') { + }, + liquid: (content, filePath) => { const r = extractLiquid(content, filePath) return [...r.symbols, ...sectionsToHeadingSymbols(r.sections, filePath)] - } - if (language === 'kotlin') return extractKotlin(content, filePath).symbols - if (language === 'graphql') return extractGraphql(content, filePath).symbols - if (language === 'sql') return extractSql(content, filePath) - if (language === 'ini') return extractIni(content, filePath) - if (language === 'makefile') return extractMakefile(content, filePath) - if (language === 'proto') return extractProto(content, filePath).symbols - if (language === 'powershell') return extractPowershell(content, filePath).symbols - if (language === 'apex') return extractApex(content, filePath).symbols - if (language === 'salesforce_metadata') return extractSalesforceMetadata(content, filePath).symbols - if (language === 'env_file') return extractEnv(content, filePath) + }, + kotlin: (content, filePath) => extractKotlin(content, filePath).symbols, + graphql: (content, filePath) => extractGraphql(content, filePath).symbols, + sql: extractSql, + ini: extractIni, + makefile: extractMakefile, + proto: (content, filePath) => extractProto(content, filePath).symbols, + powershell: (content, filePath) => extractPowershell(content, filePath).symbols, + apex: (content, filePath) => extractApex(content, filePath).symbols, + salesforce_metadata: (content, filePath) => extractSalesforceMetadata(content, filePath).symbols, + env_file: extractEnv, +} +function extractSymbolsNoTreeSitter( + content: string, + filePath: string, + language: Language, +): SymbolEntry[] { if (language === 'unknown') return [] - return extractWithRegex(content, filePath) + return (NO_TREE_SITTER_EXTRACTORS[language] ?? extractWithRegex)(content, filePath) } /** diff --git a/src/read_commands.ts b/src/read_commands.ts index 4b65a0dc..a773d2ab 100644 --- a/src/read_commands.ts +++ b/src/read_commands.ts @@ -151,7 +151,17 @@ export function runSymbol(opts: SymbolOptions): { text: string; code: number } { // Header + short body preview per match (mirrors the richer surface that the native CLI handler used before the two read surfaces were consolidated). const blocks = results.map((sym) => { const header = `# ${sym.name} (${sym.kind}) — ${sym.filePath}:${sym.lineStart}-${sym.lineEnd}` - const preview = sym.body.split(/\r?\n/).slice(0, 5).join('\n') + let body = sym.body + if (body === '') { + const source = readFileText(sym.filePath) + if (source !== null) { + body = source + .split(/\r?\n/) + .slice(Math.max(0, sym.lineStart - 1), sym.lineEnd) + .join('\n') + } + } + const preview = body.split(/\r?\n/).slice(0, 5).join('\n') return preview.trim() !== '' ? `${header}\n${preview}` : header }) return { text: blocks.join('\n\n'), code: 0 } diff --git a/tests/read_commands.test.ts b/tests/read_commands.test.ts index ee73f745..e67c2385 100644 --- a/tests/read_commands.test.ts +++ b/tests/read_commands.test.ts @@ -155,6 +155,27 @@ describe('read_commands', () => { expect(Array.isArray(parsed)).toBe(true) }) + it('reconstructs an empty indexed body from its source span for the preview', () => { + const filePath = path.join(tempDir, 'Example.profile-meta.xml') + fs.writeFileSync(filePath, '\n \n\n') + const sym: MockSymbol = { + name: 'Example', + kind: 'sf_profile', + filePath, + lineStart: 1, + lineEnd: 3, + body: '', + docstring: '', + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockQuerySymbols.mockReturnValue([sym as any]) + + const { text: stdout } = runSymbol({ name: 'Example' }) + + expect(stdout).toContain('') + expect(stdout).toContain('') + }) + it('resolves the filePath filter to the index key before querying', () => { mockQuerySymbols.mockReturnValue([]) runSymbol({ name: 'x', file: 'src/bar.ts' })