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  }