Skip to main content
Glama
enrichment.ts8.72 kB
/** * Symbol enrichment utilities - adds code snippets to symbols */ import * as fs from 'fs/promises'; import { Range, Location, WorkspaceSymbol, SymbolInformation, DocumentSymbol, FlattenedSymbol, } from '../types/lsp.js'; // Supported symbol types for enrichment export type EnrichableSymbol = | Location | WorkspaceSymbol | SymbolInformation | DocumentSymbol | FlattenedSymbol | { uri: string; range: Range } // Generic location-like object | { location: Location }; // Legacy format with location wrapper export interface EnrichedSymbol<T extends EnrichableSymbol = EnrichableSymbol> { symbol: T; codeSnippet?: string; error?: string; } interface FileCache { [filePath: string]: string[]; } /** * Converts a file:// URI to a local file path with proper URL decoding */ function decodeFileUriToPath(uri: string): string { // Remove file:// prefix let path = uri.replace('file://', ''); // Decode URL encoding (like %40 -> @) try { path = decodeURIComponent(path); } catch { // If decoding fails, use the original path } return path; } /** * Enriches a list of symbols with code snippets by reading their source files */ export async function enrichSymbolsWithCode<T extends EnrichableSymbol>( symbols: T[], options?: { extractFullDeclaration?: boolean } ): Promise<EnrichedSymbol<T>[]> { const fileCache: FileCache = {}; const enrichedSymbols: EnrichedSymbol<T>[] = []; // Group symbols by file to minimize file reads const symbolsByFile = new Map<string, T[]>(); for (const symbol of symbols) { const uri = extractUriFromSymbol(symbol); if (uri) { const filePath = decodeFileUriToPath(uri); if (!symbolsByFile.has(filePath)) { symbolsByFile.set(filePath, []); } symbolsByFile.get(filePath)!.push(symbol); } } // Read each file once and cache the lines for (const [filePath, fileSymbols] of symbolsByFile) { try { const fileContent = await fs.readFile(filePath, 'utf-8'); const lines = fileContent.split('\n'); fileCache[filePath] = lines; // Extract code snippets for all symbols in this file for (const symbol of fileSymbols) { const range = extractRangeFromSymbol(symbol); const snippet = range ? extractCodeSnippet(lines, range, options?.extractFullDeclaration) : null; const enriched: EnrichedSymbol<T> = { symbol }; if (snippet) { enriched.codeSnippet = snippet; } enrichedSymbols.push(enriched); } } catch (error) { // If file reading fails, add symbols without snippets for (const symbol of fileSymbols) { enrichedSymbols.push({ symbol, error: `Could not read file: ${error instanceof Error ? error.message : 'Unknown error'}`, }); } } } return enrichedSymbols; } /** * Type guard and extraction functions for symbol properties */ function extractUriFromSymbol(symbol: EnrichableSymbol): string | null { if ('uri' in symbol && typeof symbol.uri === 'string') { return symbol.uri; } if ('location' in symbol && symbol.location && 'uri' in symbol.location) { return symbol.location.uri; } return null; } function extractRangeFromSymbol(symbol: EnrichableSymbol): Range | null { if ('range' in symbol && symbol.range) { return symbol.range; } if ('location' in symbol && symbol.location && 'range' in symbol.location) { return symbol.location.range; } return null; } /** * Extracts code snippet from file lines using LSP range (0-based) * @param fileLines - Array of file lines * @param range - LSP range (0-based) * @param extractFullDeclaration - If true, extracts from character 0 to get full declaration with modifiers */ function extractCodeSnippet( fileLines: string[], range: Range, extractFullDeclaration?: boolean ): string { const startLine = range.start.line; let endLine = range.end.line; let startChar = range.start.character; let endChar = range.end.character; // For signature mode: extract full declaration starting from column 0 // Get up to 3-4 lines to handle multi-line declarations if (extractFullDeclaration) { startChar = 0; // Get a few lines for multi-line declarations, but cap at the symbol's end or 4 lines endLine = Math.min(startLine + 3, range.end.line); // Get to end of line or reasonable length if (endLine === startLine) { endChar = Math.min(fileLines[startLine]?.length ?? 0, 200); } else { endChar = Math.min(fileLines[endLine]?.length ?? 0, 200); } } // Validate line numbers if ( startLine < 0 || startLine >= fileLines.length || endLine < 0 || endLine >= fileLines.length ) { return '// Code snippet unavailable - invalid range'; } if (startLine === endLine) { // Single line snippet const line = fileLines[startLine]; if (!line) return '// Code snippet unavailable - line not found'; return line.substring(startChar, endChar); } // Multi-line snippet const snippetLines: string[] = []; // First line (from startChar to end) const firstLine = fileLines[startLine]; if (firstLine) { snippetLines.push(firstLine.substring(startChar)); } // Middle lines (full lines) for (let i = startLine + 1; i < endLine; i++) { const line = fileLines[i]; if (line !== undefined) { snippetLines.push(line); } } // Last line (from start to endChar) if (endLine < fileLines.length) { const lastLine = fileLines[endLine]; if (lastLine !== undefined) { snippetLines.push(lastLine.substring(0, endChar)); } } return snippetLines.join('\n'); } /** * Creates a code preview with markdown formatting * If maxLines is 0, shows entire code snippet without truncation */ export function createCodePreview( codeSnippet: string, maxLines: number = 0 ): string { // Trim trailing newlines from the snippet const trimmedSnippet = codeSnippet.replace(/\n+$/, ''); let preview: string; if (maxLines === 0) { // Show entire code snippet preview = trimmedSnippet; } else { // Truncate to maxLines if specified const lines = trimmedSnippet.split('\n'); if (lines.length <= maxLines) { preview = trimmedSnippet; } else { const previewLines = lines.slice(0, maxLines); const remainingLines = lines.length - maxLines; preview = `${previewLines.join('\n')}\n// ... ${remainingLines} more lines`; } } // Wrap in markdown code block return `\`\`\`\n${preview}\n\`\`\``; } /** * Creates a single-line signature preview from a code snippet * Condenses whitespace and limits to specified character count */ export function createSignaturePreview( codeSnippet: string, maxChars: number = 100 ): string { // Normalize whitespace: replace newlines with spaces, collapse multiple spaces const normalized = codeSnippet .replace(/\n/g, ' ') // Replace newlines with spaces .replace(/\s+/g, ' ') // Collapse multiple whitespace to single space .trim(); // Remove leading/trailing whitespace // Truncate if too long if (normalized.length <= maxChars) { return normalized; } // Find a good truncation point (prefer to break at word boundaries) const truncated = normalized.substring(0, maxChars - 3); const lastSpace = truncated.lastIndexOf(' '); // If we found a space near the end, break there; otherwise just cut at maxChars const breakPoint = lastSpace > maxChars - 20 ? lastSpace : maxChars - 3; return normalized.substring(0, breakPoint) + '...'; } /** * Enriches a list of symbol locations with code snippets */ export async function enrichSymbolLocations( locations: Location[] ): Promise<{ codeSnippet: string | null }[]> { const results: { codeSnippet: string | null }[] = []; const fileCache: FileCache = {}; for (const location of locations) { try { const filePath = decodeFileUriToPath(location.uri); // Load file if not cached if (!fileCache[filePath]) { const fileContent = await fs.readFile(filePath, 'utf-8'); fileCache[filePath] = fileContent.split('\n'); } // Expand range to get full line context const expandedRange: Range = { start: { line: location.range.start.line, character: 0 }, end: { line: location.range.start.line, character: 1000 }, }; const snippet = extractCodeSnippet(fileCache[filePath], expandedRange); results.push({ codeSnippet: snippet }); } catch { // If we can't read the file, just return null for the snippet results.push({ codeSnippet: null }); } } return results; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/p1va/symbols-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server