github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/shell/autocomplete/paths.go (about) 1 package autocomplete 2 3 import ( 4 "context" 5 "os" 6 "path/filepath" 7 "regexp" 8 "strings" 9 "time" 10 11 "github.com/lmorg/murex/lang" 12 "github.com/lmorg/murex/lang/types" 13 "github.com/lmorg/murex/shell/variables" 14 "github.com/lmorg/murex/utils/cd/cache" 15 "github.com/lmorg/murex/utils/consts" 16 ) 17 18 func MatchDirectories(prefix string, act *AutoCompleteT) { 19 act.append(matchDirs(prefix, act)...) 20 } 21 22 func matchDirs(s string, act *AutoCompleteT) []string { 23 return matchFilesystem(s, false, "", act) 24 } 25 26 func matchFilesAndDirs(s string, act *AutoCompleteT) []string { 27 return matchFilesystem(s, true, "", act) 28 } 29 30 func matchFilesAndDirsWithRegexp(s string, fileRegexp string, act *AutoCompleteT) []string { 31 return matchFilesystem(s, true, fileRegexp, act) 32 } 33 34 func matchFilesystem(s string, filesToo bool, fileRegexp string, act *AutoCompleteT) []string { 35 // compile regex 36 var ( 37 rx *regexp.Regexp 38 err error 39 ) 40 41 act.DoNotSort = true 42 43 if len(fileRegexp) > 0 { 44 rx, err = regexp.Compile(fileRegexp) 45 if err != nil { 46 act.ErrCallback(err) 47 } 48 } 49 50 // Is recursive search enabled? 51 enabled, _ := lang.ShellProcess.Config.Get("shell", "recursive-enabled", types.Boolean) 52 //if err != nil { 53 // enabled = false 54 //} 55 56 // If not, fallback to the faster surface level scan 57 if !enabled.(bool) { 58 if filesToo { 59 return matchFilesAndDirsOnce(s, rx) 60 } 61 return matchDirsOnce(s) 62 } 63 64 // If so, get timeout and depth, then start the scans in parallel 65 var ( 66 once []string 67 recursive []string 68 ) 69 70 softTimeout, _ := lang.ShellProcess.Config.Get("shell", "autocomplete-soft-timeout", types.Integer) 71 hardTimeout, _ := lang.ShellProcess.Config.Get("shell", "autocomplete-hard-timeout", types.Integer) 72 73 softCtx, _ := context.WithTimeout(context.Background(), time.Duration(int64(softTimeout.(int)))*time.Millisecond) 74 hardCtx, _ := context.WithTimeout(context.Background(), time.Duration(int64(hardTimeout.(int)))*time.Millisecond) 75 76 done := make(chan bool) 77 78 act.largeMin() // assume recursive overruns 79 80 go func() { 81 recursive = matchRecursive(hardCtx, s, filesToo, rx, act) 82 83 formatSuggestionsArray(act.ParsedTokens, recursive) 84 act.DelayedTabContext.AppendSuggestions(recursive) 85 }() 86 87 go func() { 88 if filesToo { 89 once = matchFilesAndDirsOnce(s, rx) 90 } else { 91 once = matchDirsOnce(s) 92 } 93 done <- true 94 select { 95 case <-softCtx.Done(): 96 // don't wait too long for regular files. It might be a slow storage device 97 formatSuggestionsArray(act.ParsedTokens, once) 98 act.DelayedTabContext.AppendSuggestions(once) 99 default: 100 } 101 }() 102 103 select { 104 case <-done: 105 return once 106 case <-softCtx.Done(): 107 return []string{} 108 } 109 } 110 111 func partialPath(s string) (path, partial string) { 112 expanded := variables.ExpandString(s) 113 split := strings.Split(expanded, consts.PathSlash) 114 path = strings.Join(split[:len(split)-1], consts.PathSlash) 115 partial = split[len(split)-1] 116 117 if len(s) > 0 && s[0] == consts.PathSlash[0] { 118 path = consts.PathSlash + path 119 } 120 121 if path == "" { 122 path = "." 123 } 124 return 125 } 126 127 func matchLocal(s string, includeColon bool) (items []string) { 128 path, file := partialPath(s) 129 exes := make(map[string]bool) 130 listExes(path, exes) 131 items = matchExes(file, exes) 132 return 133 } 134 135 func matchFilesAndDirsOnce(s string, rx *regexp.Regexp) (items []string) { 136 //s = variables.ExpandString(s) 137 path, partial := partialPath(s) 138 139 var item []string 140 141 files, _ := os.ReadDir(path) 142 for _, f := range files { 143 if f.Name()[0] == '.' && (len(partial) == 0 || partial[0] != '.') { 144 // hide hidden files and directories unless you press dot / period. 145 // (this behavior will also hide files and directories in Windows if 146 // those file system objects are prefixed with a dot / period). 147 continue 148 } 149 if rx != nil && !rx.MatchString(f.Name()) { 150 continue 151 } 152 if f.IsDir() { 153 item = append(item, f.Name()+consts.PathSlash) 154 } else { 155 item = append(item, f.Name()) 156 } 157 } 158 159 item = append(item, ".."+consts.PathSlash) 160 161 for i := range item { 162 if strings.HasPrefix(item[i], partial) { 163 items = append(items, item[i][len(partial):]) 164 } 165 } 166 return 167 } 168 169 func matchRecursive(ctx context.Context, s string, filesToo bool, rx *regexp.Regexp, act *AutoCompleteT) (hierarchy []string) { 170 s = variables.ExpandString(s) 171 172 maxDepth, _ := lang.ShellProcess.Config.Get("shell", "recursive-max-depth", types.Integer) 173 174 split := strings.Split(s, consts.PathSlash) 175 path := strings.Join(split[:len(split)-1], consts.PathSlash) 176 partial := split[len(split)-1] 177 178 if len(s) > 0 && s[0] == consts.PathSlash[0] { 179 path = consts.PathSlash + path 180 } 181 182 //var mutex sync.Mutex 183 184 walker := func(walkedPath string, info os.FileInfo, err error) error { 185 select { 186 case <-ctx.Done(): 187 return ctx.Err() 188 case <-act.DelayedTabContext.Context.Done(): 189 return act.DelayedTabContext.Context.Err() 190 default: 191 } 192 193 if err != nil { 194 return nil 195 } 196 197 if !info.IsDir() && !filesToo { 198 return nil 199 } 200 201 if info.Name()[0] == '.' && (len(partial) == 0 || partial[0] != '.') { 202 return nil 203 } 204 205 dirs := strings.Split(walkedPath, consts.PathSlash) 206 207 if len(dirs) == len(split) { 208 return nil 209 } 210 211 if len(dirs)-len(split) > maxDepth.(int) { 212 return filepath.SkipDir 213 } 214 215 if len(dirs) != 0 && len(dirs[len(dirs)-1]) == 0 { 216 return nil 217 } 218 219 switch { 220 case strings.HasSuffix(s, consts.PathSlash): 221 if (len(dirs)) > 1 && strings.HasPrefix(dirs[len(dirs)-2], ".") { 222 223 return filepath.SkipDir 224 } 225 226 case len(split) == 1: 227 if (len(dirs)) > 1 && strings.HasPrefix(dirs[len(dirs)-2], ".") && 228 (!strings.HasPrefix(s, ".") || strings.HasPrefix(s, "..")) { 229 230 return filepath.SkipDir 231 } 232 233 default: 234 if (len(dirs)) > 1 && strings.HasPrefix(dirs[len(dirs)-2], ".") && !strings.HasPrefix(dirs[len(dirs)-2], "..") && 235 (!strings.HasPrefix(partial, ".") || strings.HasPrefix(partial, "..")) { 236 237 return filepath.SkipDir 238 } 239 } 240 241 if strings.HasPrefix(walkedPath, s) { 242 switch { 243 case info.IsDir(): 244 //mutex.Lock() 245 hierarchy = append(hierarchy, walkedPath[len(s):]+consts.PathSlash) 246 //mutex.Unlock() 247 case rx != nil && !rx.MatchString(info.Name()): 248 return nil 249 default: 250 //mutex.Lock() 251 hierarchy = append(hierarchy, walkedPath[len(s):]) 252 //mutex.Unlock() 253 } 254 } 255 256 return nil 257 } 258 259 var pwd string 260 if path == "" { 261 pwd = "./" 262 } else { 263 pwd = path 264 } 265 266 success := cache.WalkCompletions(pwd, walker) 267 if !success { 268 go cache.GatherFileCompletions(pwd) 269 filepath.Walk(pwd, walker) 270 return 271 } 272 273 go func() { 274 filepath.Walk(pwd, walker) 275 276 formatSuggestionsArray(act.ParsedTokens, hierarchy) 277 act.DelayedTabContext.AppendSuggestions(hierarchy) 278 }() 279 280 /*err = filepath.Walk(pwd, walker) 281 if err != nil { 282 lang.ShellProcess.Stderr.Writeln([]byte(err.Error())) 283 }*/ 284 285 return 286 }