source dump of claude code
at main 130 lines 4.5 kB view raw
1import { basename, dirname, isAbsolute, join, sep } from 'path' 2import type { ToolPermissionContext } from '../Tool.js' 3import { isEnvTruthy } from './envUtils.js' 4import { 5 getFileReadIgnorePatterns, 6 normalizePatternsToPath, 7} from './permissions/filesystem.js' 8import { getPlatform } from './platform.js' 9import { getGlobExclusionsForPluginCache } from './plugins/orphanedPluginFilter.js' 10import { ripGrep } from './ripgrep.js' 11 12/** 13 * Extracts the static base directory from a glob pattern. 14 * The base directory is everything before the first glob special character (* ? [ {). 15 * Returns the directory portion and the remaining relative pattern. 16 */ 17export function extractGlobBaseDirectory(pattern: string): { 18 baseDir: string 19 relativePattern: string 20} { 21 // Find the first glob special character: *, ?, [, { 22 const globChars = /[*?[{]/ 23 const match = pattern.match(globChars) 24 25 if (!match || match.index === undefined) { 26 // No glob characters - this is a literal path 27 // Return the directory portion and filename as pattern 28 const dir = dirname(pattern) 29 const file = basename(pattern) 30 return { baseDir: dir, relativePattern: file } 31 } 32 33 // Get everything before the first glob character 34 const staticPrefix = pattern.slice(0, match.index) 35 36 // Find the last path separator in the static prefix 37 const lastSepIndex = Math.max( 38 staticPrefix.lastIndexOf('/'), 39 staticPrefix.lastIndexOf(sep), 40 ) 41 42 if (lastSepIndex === -1) { 43 // No path separator before the glob - pattern is relative to cwd 44 return { baseDir: '', relativePattern: pattern } 45 } 46 47 let baseDir = staticPrefix.slice(0, lastSepIndex) 48 const relativePattern = pattern.slice(lastSepIndex + 1) 49 50 // Handle root directory patterns (e.g., /*.txt on Unix or C:/*.txt on Windows) 51 // When lastSepIndex is 0, baseDir is empty but we need to use '/' as the root 52 if (baseDir === '' && lastSepIndex === 0) { 53 baseDir = '/' 54 } 55 56 // Handle Windows drive root paths (e.g., C:/*.txt) 57 // 'C:' means "current directory on drive C" (relative), not root 58 // We need 'C:/' or 'C:\' for the actual drive root 59 if (getPlatform() === 'windows' && /^[A-Za-z]:$/.test(baseDir)) { 60 baseDir = baseDir + sep 61 } 62 63 return { baseDir, relativePattern } 64} 65 66export async function glob( 67 filePattern: string, 68 cwd: string, 69 { limit, offset }: { limit: number; offset: number }, 70 abortSignal: AbortSignal, 71 toolPermissionContext: ToolPermissionContext, 72): Promise<{ files: string[]; truncated: boolean }> { 73 let searchDir = cwd 74 let searchPattern = filePattern 75 76 // Handle absolute paths by extracting the base directory and converting to relative pattern 77 // ripgrep's --glob flag only works with relative patterns 78 if (isAbsolute(filePattern)) { 79 const { baseDir, relativePattern } = extractGlobBaseDirectory(filePattern) 80 if (baseDir) { 81 searchDir = baseDir 82 searchPattern = relativePattern 83 } 84 } 85 86 const ignorePatterns = normalizePatternsToPath( 87 getFileReadIgnorePatterns(toolPermissionContext), 88 searchDir, 89 ) 90 91 // Use ripgrep for better memory performance 92 // --files: list files instead of searching content 93 // --glob: filter by pattern 94 // --sort=modified: sort by modification time (oldest first) 95 // --no-ignore: don't respect .gitignore (default true, set CLAUDE_CODE_GLOB_NO_IGNORE=false to respect .gitignore) 96 // --hidden: include hidden files (default true, set CLAUDE_CODE_GLOB_HIDDEN=false to exclude) 97 // Note: use || instead of ?? to treat empty string as unset (defaulting to true) 98 const noIgnore = isEnvTruthy(process.env.CLAUDE_CODE_GLOB_NO_IGNORE || 'true') 99 const hidden = isEnvTruthy(process.env.CLAUDE_CODE_GLOB_HIDDEN || 'true') 100 const args = [ 101 '--files', 102 '--glob', 103 searchPattern, 104 '--sort=modified', 105 ...(noIgnore ? ['--no-ignore'] : []), 106 ...(hidden ? ['--hidden'] : []), 107 ] 108 109 // Add ignore patterns 110 for (const pattern of ignorePatterns) { 111 args.push('--glob', `!${pattern}`) 112 } 113 114 // Exclude orphaned plugin version directories 115 for (const exclusion of await getGlobExclusionsForPluginCache(searchDir)) { 116 args.push('--glob', exclusion) 117 } 118 119 const allPaths = await ripGrep(args, searchDir, abortSignal) 120 121 // ripgrep returns relative paths, convert to absolute 122 const absolutePaths = allPaths.map(p => 123 isAbsolute(p) ? p : join(searchDir, p), 124 ) 125 126 const truncated = absolutePaths.length > offset + limit 127 const files = absolutePaths.slice(offset, offset + limit) 128 129 return { files, truncated } 130}