prompt.go

  1package prompt
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"strings"
  7	"sync"
  8
  9	"github.com/charmbracelet/crush/internal/resolver"
 10)
 11
 12type PromptID string
 13
 14const (
 15	PromptCoder      PromptID = "coder"
 16	PromptTitle      PromptID = "title"
 17	PromptTask       PromptID = "task"
 18	PromptSummarizer PromptID = "summarizer"
 19	PromptDefault    PromptID = "default"
 20)
 21
 22func getContextFromPaths(workingDir string, contextPaths []string) string {
 23	return processContextPaths(workingDir, contextPaths)
 24}
 25
 26// expandPath expands ~ and environment variables in file paths
 27func expandPath(path string) string {
 28	// Handle tilde expansion
 29	if strings.HasPrefix(path, "~/") {
 30		homeDir, err := os.UserHomeDir()
 31		if err == nil {
 32			path = filepath.Join(homeDir, path[2:])
 33		}
 34	} else if path == "~" {
 35		homeDir, err := os.UserHomeDir()
 36		if err == nil {
 37			path = homeDir
 38		}
 39	}
 40
 41	// Handle environment variable expansion using the same pattern as config
 42	if strings.HasPrefix(path, "$") {
 43		resolver := resolver.New()
 44		if expanded, err := resolver.ResolveValue(path); err == nil {
 45			path = expanded
 46		}
 47	}
 48
 49	return path
 50}
 51
 52func processContextPaths(workDir string, paths []string) string {
 53	var (
 54		wg       sync.WaitGroup
 55		resultCh = make(chan string)
 56	)
 57
 58	// Track processed files to avoid duplicates
 59	processedFiles := make(map[string]bool)
 60	var processedMutex sync.Mutex
 61
 62	for _, path := range paths {
 63		wg.Add(1)
 64		go func(p string) {
 65			defer wg.Done()
 66
 67			// Expand ~ and environment variables before processing
 68			p = expandPath(p)
 69
 70			// Use absolute path if provided, otherwise join with workDir
 71			fullPath := p
 72			if !filepath.IsAbs(p) {
 73				fullPath = filepath.Join(workDir, p)
 74			}
 75
 76			// Check if the path is a directory using os.Stat
 77			info, err := os.Stat(fullPath)
 78			if err != nil {
 79				return // Skip if path doesn't exist or can't be accessed
 80			}
 81
 82			if info.IsDir() {
 83				filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
 84					if err != nil {
 85						return err
 86					}
 87					if !d.IsDir() {
 88						// Check if we've already processed this file (case-insensitive)
 89						lowerPath := strings.ToLower(path)
 90
 91						processedMutex.Lock()
 92						alreadyProcessed := processedFiles[lowerPath]
 93						if !alreadyProcessed {
 94							processedFiles[lowerPath] = true
 95						}
 96						processedMutex.Unlock()
 97
 98						if !alreadyProcessed {
 99							if result := processFile(path); result != "" {
100								resultCh <- result
101							}
102						}
103					}
104					return nil
105				})
106			} else {
107				// It's a file, process it directly
108				// Check if we've already processed this file (case-insensitive)
109				lowerPath := strings.ToLower(fullPath)
110
111				processedMutex.Lock()
112				alreadyProcessed := processedFiles[lowerPath]
113				if !alreadyProcessed {
114					processedFiles[lowerPath] = true
115				}
116				processedMutex.Unlock()
117
118				if !alreadyProcessed {
119					result := processFile(fullPath)
120					if result != "" {
121						resultCh <- result
122					}
123				}
124			}
125		}(path)
126	}
127
128	go func() {
129		wg.Wait()
130		close(resultCh)
131	}()
132
133	results := make([]string, 0)
134	for result := range resultCh {
135		results = append(results, result)
136	}
137
138	return strings.Join(results, "\n")
139}
140
141func processFile(filePath string) string {
142	content, err := os.ReadFile(filePath)
143	if err != nil {
144		return ""
145	}
146	return "# From:" + filePath + "\n" + string(content)
147}