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  }