fileutil.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. package fileutil
  2. import (
  3. "fmt"
  4. "io/fs"
  5. "os"
  6. "os/exec"
  7. "path/filepath"
  8. "sort"
  9. "strings"
  10. "time"
  11. "github.com/bmatcuk/doublestar/v4"
  12. "github.com/sst/opencode/internal/status"
  13. )
  14. var (
  15. rgPath string
  16. fzfPath string
  17. )
  18. func Init() {
  19. var err error
  20. rgPath, err = exec.LookPath("rg")
  21. if err != nil {
  22. status.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.")
  23. rgPath = ""
  24. }
  25. fzfPath, err = exec.LookPath("fzf")
  26. if err != nil {
  27. status.Warn("FZF not found in $PATH. Some features might be limited or slower.")
  28. fzfPath = ""
  29. }
  30. }
  31. func GetRgCmd(globPattern string) *exec.Cmd {
  32. if rgPath == "" {
  33. return nil
  34. }
  35. rgArgs := []string{
  36. "--files",
  37. "-L",
  38. "--null",
  39. }
  40. if globPattern != "" {
  41. if !filepath.IsAbs(globPattern) && !strings.HasPrefix(globPattern, "/") {
  42. globPattern = "/" + globPattern
  43. }
  44. rgArgs = append(rgArgs, "--glob", globPattern)
  45. }
  46. cmd := exec.Command(rgPath, rgArgs...)
  47. cmd.Dir = "."
  48. return cmd
  49. }
  50. func GetFzfCmd(query string) *exec.Cmd {
  51. if fzfPath == "" {
  52. return nil
  53. }
  54. fzfArgs := []string{
  55. "--filter",
  56. query,
  57. "--read0",
  58. "--print0",
  59. }
  60. cmd := exec.Command(fzfPath, fzfArgs...)
  61. cmd.Dir = "."
  62. return cmd
  63. }
  64. type FileInfo struct {
  65. Path string
  66. ModTime time.Time
  67. }
  68. func SkipHidden(path string) bool {
  69. // Check for hidden files (starting with a dot)
  70. base := filepath.Base(path)
  71. if base != "." && strings.HasPrefix(base, ".") {
  72. return true
  73. }
  74. commonIgnoredDirs := map[string]bool{
  75. ".opencode": true,
  76. "node_modules": true,
  77. "vendor": true,
  78. "dist": true,
  79. "build": true,
  80. "target": true,
  81. ".git": true,
  82. ".idea": true,
  83. ".vscode": true,
  84. "__pycache__": true,
  85. "bin": true,
  86. "obj": true,
  87. "out": true,
  88. "coverage": true,
  89. "tmp": true,
  90. "temp": true,
  91. "logs": true,
  92. "generated": true,
  93. "bower_components": true,
  94. "jspm_packages": true,
  95. }
  96. parts := strings.Split(path, string(os.PathSeparator))
  97. for _, part := range parts {
  98. if commonIgnoredDirs[part] {
  99. return true
  100. }
  101. }
  102. return false
  103. }
  104. func GlobWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) {
  105. fsys := os.DirFS(searchPath)
  106. relPattern := strings.TrimPrefix(pattern, "/")
  107. var matches []FileInfo
  108. err := doublestar.GlobWalk(fsys, relPattern, func(path string, d fs.DirEntry) error {
  109. if d.IsDir() {
  110. return nil
  111. }
  112. if SkipHidden(path) {
  113. return nil
  114. }
  115. info, err := d.Info()
  116. if err != nil {
  117. return nil
  118. }
  119. absPath := path
  120. if !strings.HasPrefix(absPath, searchPath) && searchPath != "." {
  121. absPath = filepath.Join(searchPath, absPath)
  122. } else if !strings.HasPrefix(absPath, "/") && searchPath == "." {
  123. absPath = filepath.Join(searchPath, absPath) // Ensure relative paths are joined correctly
  124. }
  125. matches = append(matches, FileInfo{Path: absPath, ModTime: info.ModTime()})
  126. if limit > 0 && len(matches) >= limit*2 {
  127. return fs.SkipAll
  128. }
  129. return nil
  130. })
  131. if err != nil {
  132. return nil, false, fmt.Errorf("glob walk error: %w", err)
  133. }
  134. sort.Slice(matches, func(i, j int) bool {
  135. return matches[i].ModTime.After(matches[j].ModTime)
  136. })
  137. truncated := false
  138. if limit > 0 && len(matches) > limit {
  139. matches = matches[:limit]
  140. truncated = true
  141. }
  142. results := make([]string, len(matches))
  143. for i, m := range matches {
  144. results[i] = m.Path
  145. }
  146. return results, truncated, nil
  147. }