import { logForDebugging } from '../../utils/debug.js' import { truncate } from '../../utils/format.js' import { getFsImplementation } from '../../utils/fsOperations.js' import { expandPath } from '../../utils/path.js' const MAX_READ_BYTES = 64 * 1024 /** * Extracts the symbol/word at a specific position in a file. * Used to show context in tool use messages. * * @param filePath - The file path (absolute or relative) * @param line - 0-indexed line number * @param character - 0-indexed character position on the line * * Note: This uses synchronous file I/O because it is called from * renderToolUseMessage (a synchronous React render function). The read is * wrapped in try/catch so ENOENT and other errors fall back gracefully. * @returns The symbol at that position, or null if extraction fails */ export function getSymbolAtPosition( filePath: string, line: number, character: number, ): string | null { try { const fs = getFsImplementation() const absolutePath = expandPath(filePath) // Read only the first 64KB instead of the whole file. Most LSP hover/goto // targets are near recent edits; 64KB covers ~1000 lines of typical code. // If the target line is past this window we fall back to null (the UI // already handles that by showing `position: line:char`). // eslint-disable-next-line custom-rules/no-sync-fs -- called from sync React render (renderToolUseMessage) const { buffer, bytesRead } = fs.readSync(absolutePath, { length: MAX_READ_BYTES, }) const content = buffer.toString('utf-8', 0, bytesRead) const lines = content.split('\n') if (line < 0 || line >= lines.length) { return null } // If we filled the full buffer the file continues past our window, // so the last split element may be truncated mid-line. if (bytesRead === MAX_READ_BYTES && line === lines.length - 1) { return null } const lineContent = lines[line] if (!lineContent || character < 0 || character >= lineContent.length) { return null } // Extract the word/symbol at the character position // Pattern matches: // - Standard identifiers: alphanumeric + underscore + dollar // - Rust lifetimes: 'a, 'static // - Rust macros: macro_name! // - Operators and special symbols: +, -, *, etc. // This is more inclusive to handle various programming languages const symbolPattern = /[\w$'!]+|[+\-*/%&|^~<>=]+/g let match: RegExpExecArray | null while ((match = symbolPattern.exec(lineContent)) !== null) { const start = match.index const end = start + match[0].length // Check if the character position falls within this match if (character >= start && character < end) { const symbol = match[0] // Limit length to 30 characters to avoid overly long symbols return truncate(symbol, 30) } } return null } catch (error) { // Log unexpected errors for debugging (permission issues, encoding problems, etc.) // Use logForDebugging since this is a display enhancement, not a critical error if (error instanceof Error) { logForDebugging( `Symbol extraction failed for ${filePath}:${line}:${character}: ${error.message}`, { level: 'warn' }, ) } // Still return null for graceful fallback to position display return null } }