source dump of claude code
at main 90 lines 3.4 kB view raw
1import { logForDebugging } from '../../utils/debug.js' 2import { truncate } from '../../utils/format.js' 3import { getFsImplementation } from '../../utils/fsOperations.js' 4import { expandPath } from '../../utils/path.js' 5 6const MAX_READ_BYTES = 64 * 1024 7 8/** 9 * Extracts the symbol/word at a specific position in a file. 10 * Used to show context in tool use messages. 11 * 12 * @param filePath - The file path (absolute or relative) 13 * @param line - 0-indexed line number 14 * @param character - 0-indexed character position on the line 15 * 16 * Note: This uses synchronous file I/O because it is called from 17 * renderToolUseMessage (a synchronous React render function). The read is 18 * wrapped in try/catch so ENOENT and other errors fall back gracefully. 19 * @returns The symbol at that position, or null if extraction fails 20 */ 21export function getSymbolAtPosition( 22 filePath: string, 23 line: number, 24 character: number, 25): string | null { 26 try { 27 const fs = getFsImplementation() 28 const absolutePath = expandPath(filePath) 29 30 // Read only the first 64KB instead of the whole file. Most LSP hover/goto 31 // targets are near recent edits; 64KB covers ~1000 lines of typical code. 32 // If the target line is past this window we fall back to null (the UI 33 // already handles that by showing `position: line:char`). 34 // eslint-disable-next-line custom-rules/no-sync-fs -- called from sync React render (renderToolUseMessage) 35 const { buffer, bytesRead } = fs.readSync(absolutePath, { 36 length: MAX_READ_BYTES, 37 }) 38 const content = buffer.toString('utf-8', 0, bytesRead) 39 const lines = content.split('\n') 40 41 if (line < 0 || line >= lines.length) { 42 return null 43 } 44 // If we filled the full buffer the file continues past our window, 45 // so the last split element may be truncated mid-line. 46 if (bytesRead === MAX_READ_BYTES && line === lines.length - 1) { 47 return null 48 } 49 50 const lineContent = lines[line] 51 if (!lineContent || character < 0 || character >= lineContent.length) { 52 return null 53 } 54 55 // Extract the word/symbol at the character position 56 // Pattern matches: 57 // - Standard identifiers: alphanumeric + underscore + dollar 58 // - Rust lifetimes: 'a, 'static 59 // - Rust macros: macro_name! 60 // - Operators and special symbols: +, -, *, etc. 61 // This is more inclusive to handle various programming languages 62 const symbolPattern = /[\w$'!]+|[+\-*/%&|^~<>=]+/g 63 let match: RegExpExecArray | null 64 65 while ((match = symbolPattern.exec(lineContent)) !== null) { 66 const start = match.index 67 const end = start + match[0].length 68 69 // Check if the character position falls within this match 70 if (character >= start && character < end) { 71 const symbol = match[0] 72 // Limit length to 30 characters to avoid overly long symbols 73 return truncate(symbol, 30) 74 } 75 } 76 77 return null 78 } catch (error) { 79 // Log unexpected errors for debugging (permission issues, encoding problems, etc.) 80 // Use logForDebugging since this is a display enhancement, not a critical error 81 if (error instanceof Error) { 82 logForDebugging( 83 `Symbol extraction failed for ${filePath}:${line}:${character}: ${error.message}`, 84 { level: 'warn' }, 85 ) 86 } 87 // Still return null for graceful fallback to position display 88 return null 89 } 90}