github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/shell/autocomplete/dynamic.go (about) 1 package autocomplete 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/lmorg/murex/builtins/pipes/streams" 10 "github.com/lmorg/murex/debug" 11 "github.com/lmorg/murex/lang" 12 "github.com/lmorg/murex/lang/stdio" 13 "github.com/lmorg/murex/lang/types" 14 "github.com/lmorg/murex/utils" 15 "github.com/lmorg/murex/utils/cache" 16 "github.com/lmorg/murex/utils/lists" 17 "github.com/lmorg/murex/utils/parser" 18 ) 19 20 type dynamicArgs struct { 21 exe string 22 params []string 23 float int 24 } 25 26 func matchDynamic(f *Flags, partial string, args dynamicArgs, act *AutoCompleteT) { 27 // Default to building up from Dynamic field. Fall back to DynamicDefs 28 dynamic := f.Dynamic 29 if f.Dynamic == "" { 30 dynamic = f.DynamicDesc 31 } 32 if dynamic == "" { 33 return 34 } 35 36 if !types.IsBlock([]byte(dynamic)) { 37 lang.ShellProcess.Stderr.Writeln([]byte("Dynamic autocompleter is not a code block")) 38 return 39 } 40 block := []rune(dynamic[1 : len(dynamic)-1]) 41 42 softTimeout, _ := lang.ShellProcess.Config.Get("shell", "autocomplete-soft-timeout", types.Integer) 43 //if err != nil { 44 // softTimeout = 100 45 //} 46 47 hardTimeout, _ := lang.ShellProcess.Config.Get("shell", "autocomplete-hard-timeout", types.Integer) 48 //if err != nil { 49 // hardTimeout = 5000 50 //} 51 52 softCtx, _ := context.WithTimeout(context.Background(), time.Duration(int64(softTimeout.(int)))*time.Millisecond) 53 hardCtx, _ := context.WithTimeout(context.Background(), time.Duration(int64(hardTimeout.(int)))*time.Millisecond) 54 wait := make(chan bool) 55 done := make(chan bool) 56 57 act.largeMin() 58 /*if f.ListView { 59 // check this here so delayed results can still be ListView 60 // (ie after &act has timed out) 61 act.TabDisplayType = readline.TabDisplayList 62 }*/ 63 64 var fork *lang.Fork 65 go func() { 66 // don't share incomplete parameters with dynamic autocompletion blocks 67 params := act.ParsedTokens.Parameters 68 switch len(params) { 69 case 0: // do nothing 70 case 1: 71 params = []string{} 72 default: 73 params = params[:len(params)-1] 74 } 75 76 cacheHash := cache.CreateHash(args.exe, params, block) 77 dc := new(dynamicCacheItemT) 78 ok := cache.Read(cache.AUTOCOMPLETE_DYNAMIC, cacheHash, dc) 79 var stdout stdio.Io 80 81 if ok { 82 stdout = streams.NewStdin() 83 stdout.SetDataType(dc.DataType) 84 _, err := stdout.Write(dc.Stdout) 85 if err != nil { 86 lang.ShellProcess.Stderr.Writeln([]byte("dynamic autocomplete cache error: " + err.Error())) 87 } 88 } else { 89 90 // Run the commandline if ExecCmdline flag set AND commandline considered safe 91 var fStdin int 92 cmdlineStdout := streams.NewStdin() 93 if f.ExecCmdline && !act.ParsedTokens.Unsafe { 94 cmdline := lang.ShellProcess.Fork(lang.F_BACKGROUND | lang.F_NO_STDIN | lang.F_NO_STDERR) 95 cmdline.Stdout = cmdlineStdout 96 cmdline.Name.Set(args.exe) 97 cmdline.FileRef = ExesFlagsFileRef[args.exe] 98 cmdline.Execute(act.ParsedTokens.Source[:act.ParsedTokens.LastFlowToken]) 99 100 } else { 101 fStdin = lang.F_NO_STDIN 102 } 103 104 stdin := streams.NewStdin() 105 var tee *streams.Tee 106 tee, stdout = streams.NewTee(stdin) 107 108 // Execute the dynamic code block 109 fork = lang.ShellProcess.Fork(lang.F_FUNCTION | lang.F_NEW_MODULE | lang.F_BACKGROUND | fStdin | lang.F_NO_STDERR) 110 fork.Name.Set(args.exe) 111 fork.Parameters.DefineParsed(params) 112 fork.FileRef = ExesFlagsFileRef[args.exe] 113 if f.ExecCmdline && !act.ParsedTokens.Unsafe { 114 fork.Stdin = cmdlineStdout 115 } 116 fork.Stdout = tee 117 fork.Process.Variables.Set(fork.Process, "ISMETHOD", act.ParsedTokens.PipeToken != parser.PipeTokenNone, types.Boolean) 118 fork.Process.Variables.Set(fork.Process, "PREFIX", partial, types.String) 119 120 exitNum, err := fork.Execute(block) 121 if err != nil { 122 lang.ShellProcess.Stderr.Writeln([]byte("dynamic autocomplete code could not compile: " + err.Error())) 123 } 124 if exitNum != 0 && debug.Enabled { 125 lang.ShellProcess.Stderr.Writeln([]byte("dynamic autocomplete returned a none zero exit number." + utils.NewLineString)) 126 } 127 128 dc.DataType = tee.GetDataType() 129 dc.Stdout, err = tee.ReadAll() 130 if err != nil { 131 lang.ShellProcess.Stderr.Writeln([]byte("dynamic autocomplete cache error: " + err.Error())) 132 } 133 ttl := cache.Seconds(ExesFlags[args.exe][0].CacheTTL) 134 cache.Write(cache.AUTOCOMPLETE_DYNAMIC, cacheHash, dc, ttl) 135 } 136 137 select { 138 case <-hardCtx.Done(): 139 act.ErrCallback(fmt.Errorf("dynamic autocompletion took too long")) 140 return 141 default: 142 } 143 144 if f.Dynamic != "" { 145 var ( 146 timeout bool 147 items []string 148 ) 149 150 select { 151 case <-softCtx.Done(): 152 timeout = true 153 default: 154 wait <- true 155 } 156 157 var ( 158 incFiles bool 159 incDirs bool 160 incExePath bool 161 incExeAll bool 162 incManPage bool 163 ) 164 165 err := stdout.ReadArray(hardCtx, func(b []byte) { 166 s := string(b) 167 168 if len(s) == 0 { 169 return 170 } 171 172 switch s { 173 case "@IncFiles": 174 incFiles = true 175 case "@IncDirs": 176 incDirs = true 177 case "@IncExePath": 178 incExePath = true 179 case "@IncExeAll": 180 incExeAll = true 181 case "@IncManPage", "@IncManPages": 182 incManPage = true 183 184 default: 185 if f.IgnorePrefix { 186 items = append(items, "\x02"+s) 187 } else if strings.HasPrefix(s, partial) { 188 items = append(items, s[len(partial):]) 189 } 190 } 191 }) 192 193 switch { 194 case incFiles: 195 files := matchFilesAndDirs(partial, act) 196 items = append(items, files...) 197 case incDirs: 198 files := matchDirs(partial, act) 199 items = append(items, files...) 200 case incExePath: 201 pathExes := allExecutables(false) 202 items = append(items, matchExes(partial, pathExes)...) 203 case incExeAll: 204 pathAll := allExecutables(true) 205 items = append(items, matchExes(partial, pathAll)...) 206 case incManPage: 207 flags, _ := scanManPages(args.exe) 208 items = append(items, lists.CropPartial(flags, partial)...) 209 } 210 211 if err != nil { 212 debug.Log(err) 213 } 214 215 if f.AutoBranch && !act.CacheDynamic { 216 autoBranch(&items) 217 } 218 219 if timeout { 220 formatSuggestionsArray(act.ParsedTokens, items) 221 act.DelayedTabContext.AppendSuggestions(items) 222 } else { 223 act.append(items...) 224 } 225 226 } else { 227 var ( 228 timeout bool 229 items = make(map[string]string) 230 ) 231 232 select { 233 case <-softCtx.Done(): 234 timeout = true 235 default: 236 wait <- true 237 } 238 239 stdout.ReadMap(lang.ShellProcess.Config, func(readmap *stdio.Map) { 240 if f.IgnorePrefix { 241 value, _ := types.ConvertGoType(readmap.Value, types.String) 242 value = strings.Replace(value.(string), "\r", "", -1) 243 value = strings.Replace(value.(string), "\n", " ", -1) 244 245 key := "\x02" + readmap.Key 246 247 if timeout { 248 items[key] = value.(string) 249 } else { 250 act.appendDef(key, value.(string)) 251 } 252 253 } else if strings.HasPrefix(readmap.Key, partial) { 254 value, _ := types.ConvertGoType(readmap.Value, types.String) 255 value = strings.Replace(value.(string), "\r", "", -1) 256 value = strings.Replace(value.(string), "\n", " ", -1) 257 258 if timeout { 259 items[readmap.Key[len(partial):]] = value.(string) 260 } else { 261 act.appendDef(readmap.Key[len(partial):], value.(string)) 262 } 263 } 264 }) 265 266 if timeout { 267 formatSuggestionsMap(act.ParsedTokens, &items) 268 act.DelayedTabContext.AppendDescriptions(items) 269 } 270 } 271 272 done <- true 273 }() 274 275 select { 276 case <-done: 277 return 278 case <-wait: 279 <-done 280 return 281 282 case <-softCtx.Done(): 283 if len(act.Items) == 0 && len(act.Definitions) == 0 { 284 act.ErrCallback(fmt.Errorf("long running autocompletion pushed to the background")) 285 } 286 return 287 288 case <-hardCtx.Done(): 289 act.ErrCallback(fmt.Errorf("dynamic autocompletion took too long. killing autocomplete function")) 290 fork.KillForks(1) 291 } 292 293 }