@@ -12,12 +12,63 @@ import (
17 "github.com/charmbracelet/crush/internal/config"
18 "github.com/charmbracelet/crush/internal/config"
18 "github.com/charmbracelet/crush/internal/fileutil"
19 "github.com/charmbracelet/crush/internal/fileutil"
22+// regexCache provides thread-safe caching of compiled regex patterns
23+type regexCache struct {
24+ cache map[string]*regexp.Regexp
28+// newRegexCache creates a new regex cache
29+func newRegexCache() *regexCache {
31+ cache: make(map[string]*regexp.Regexp),
35+// get retrieves a compiled regex from cache or compiles and caches it
36+func (rc *regexCache) get(pattern string) (*regexp.Regexp, error) {
37+ // Try to get from cache first (read lock)
39+ if regex, exists := rc.cache[pattern]; exists {
45+ // Compile the regex (write lock)
49+ // Double-check in case another goroutine compiled it while we waited
50+ if regex, exists := rc.cache[pattern]; exists {
54+ // Compile and cache the regex
55+ regex, err := regexp.Compile(pattern)
60+ rc.cache[pattern] = regex
64+// Global regex cache instances
66+ searchRegexCache = newRegexCache()
67+ globRegexCache = newRegexCache()
68+ // Pre-compiled regex for glob conversion (used frequently)
69+ globBraceRegex = regexp.MustCompile(`\{([^}]+)\}`)
21type GrepParams struct {
72type GrepParams struct {
22 Pattern string `json:"pattern"`
73 Pattern string `json:"pattern"`
23 Path string `json:"path"`
74 Path string `json:"path"`
@@ -266,7 +317,8 @@ func searchWithRipgrep(pattern, path, include string) ([]grepMatch, error) {
266func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
317func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error) {
267 matches := []grepMatch{}
318 matches := []grepMatch{}
320+ // Use cached regex compilation
269− regex, err := regexp.Compile(pattern)
321+ regex, err := searchRegexCache.get(pattern)
270 if err != nil {
322 if err != nil {
271 return nil, fmt.Errorf("invalid regex pattern: %w", err)
323 return nil, fmt.Errorf("invalid regex pattern: %w", err)
@@ -274,7 +326,7 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
274 var includePattern *regexp.Regexp
326 var includePattern *regexp.Regexp
275 if include != "" {
327 if include != "" {
276 regexPattern := globToRegex(include)
328 regexPattern := globToRegex(include)
277− includePattern, err = regexp.Compile(regexPattern)
329+ includePattern, err = globRegexCache.get(regexPattern)
278 if err != nil {
330 if err != nil {
279 return nil, fmt.Errorf("invalid include pattern: %w", err)
331 return nil, fmt.Errorf("invalid include pattern: %w", err)
@@ -349,8 +401,8 @@ func globToRegex(glob string) string {
349 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
401 regexPattern = strings.ReplaceAll(regexPattern, "*", ".*")
350 regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
402 regexPattern = strings.ReplaceAll(regexPattern, "?", ".")
352− re := regexp.MustCompile(`\{([^}]+)\}`)
404+ // Use pre-compiled regex instead of compiling each time
353− regexPattern = re.ReplaceAllStringFunc(regexPattern, func(match string) string {
405+ regexPattern = globBraceRegex.ReplaceAllStringFunc(regexPattern, func(match string) string {
354 inner := match[1 : len(match)-1]
406 inner := match[1 : len(match)-1]
355 return "(" + strings.ReplaceAll(inner, ",", "|") + ")"
407 return "(" + strings.ReplaceAll(inner, ",", "|") + ")"