src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/highlight.go (about)

     1  package edit
     2  
     3  import (
     4  	"os"
     5  	"os/exec"
     6  	"strings"
     7  
     8  	"src.elv.sh/pkg/cli"
     9  	"src.elv.sh/pkg/diag"
    10  	"src.elv.sh/pkg/edit/highlight"
    11  	"src.elv.sh/pkg/eval"
    12  	"src.elv.sh/pkg/fsutil"
    13  	"src.elv.sh/pkg/parse"
    14  	"src.elv.sh/pkg/ui"
    15  )
    16  
    17  func initHighlighter(appSpec *cli.AppSpec, ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) {
    18  	hl := highlight.NewHighlighter(highlight.Config{
    19  		Check: func(t parse.Tree) (string, []diag.RangeError) {
    20  			autofixes, err := ev.CheckTree(t, nil)
    21  			autofix := strings.Join(autofixes, "; ")
    22  			ed.autofix.Store(autofix)
    23  
    24  			compErrors := eval.UnpackCompilationErrors(err)
    25  			rangeErrors := make([]diag.RangeError, len(compErrors))
    26  			for i, compErr := range compErrors {
    27  				rangeErrors[i] = compErr
    28  			}
    29  
    30  			return autofix, rangeErrors
    31  		},
    32  		HasCommand: func(cmd string) bool { return hasCommand(ev, cmd) },
    33  		AutofixTip: func(autofix string) ui.Text {
    34  			return bindingTips(ed.ns, "insert:binding",
    35  				bindingTip("autofix: "+autofix, "apply-autofix"),
    36  				bindingTip("autofix first", "smart-enter", "completion:smart-start"))
    37  		},
    38  	})
    39  	appSpec.Highlighter = hl
    40  	ed.applyAutofix = func() {
    41  		code := ed.autofix.Load().(string)
    42  		if code == "" {
    43  			return
    44  		}
    45  		// TODO: Check errors.
    46  		//
    47  		// For now, the autofix snippets are simple enough that we know they'll
    48  		// always succeed.
    49  		ev.Eval(parse.Source{Name: "[autofix]", Code: code}, eval.EvalCfg{})
    50  		hl.InvalidateCache()
    51  	}
    52  	nb.AddGoFn("apply-autofix", ed.applyAutofix)
    53  }
    54  
    55  func hasCommand(ev *eval.Evaler, cmd string) bool {
    56  	if eval.IsBuiltinSpecial[cmd] {
    57  		return true
    58  	}
    59  	if fsutil.DontSearch(cmd) {
    60  		return isDirOrExecutable(cmd) || hasExternalCommand(cmd)
    61  	}
    62  
    63  	sigil, qname := eval.SplitSigil(cmd)
    64  	if sigil != "" {
    65  		// The @ sign is only valid when referring to external commands.
    66  		return hasExternalCommand(cmd)
    67  	}
    68  
    69  	first, rest := eval.SplitQName(qname)
    70  	switch {
    71  	case rest == "":
    72  		// Unqualified name; try builtin and global.
    73  		if hasFn(ev.Builtin(), first) || hasFn(ev.Global(), first) {
    74  			return true
    75  		}
    76  	case first == "e:":
    77  		return hasExternalCommand(rest)
    78  	default:
    79  		// Qualified name. Find the top-level module first.
    80  		if hasQualifiedFn(ev, first, rest) {
    81  			return true
    82  		}
    83  	}
    84  
    85  	// If all failed, it can still be an external command.
    86  	return hasExternalCommand(cmd)
    87  }
    88  
    89  func hasQualifiedFn(ev *eval.Evaler, firstNs string, rest string) bool {
    90  	if rest == "" {
    91  		return false
    92  	}
    93  	modVal, ok := ev.Global().Index(firstNs)
    94  	if !ok {
    95  		modVal, ok = ev.Builtin().Index(firstNs)
    96  		if !ok {
    97  			return false
    98  		}
    99  	}
   100  	mod, ok := modVal.(*eval.Ns)
   101  	if !ok {
   102  		return false
   103  	}
   104  	segs := eval.SplitQNameSegs(rest)
   105  	for _, seg := range segs[:len(segs)-1] {
   106  		modVal, ok = mod.Index(seg)
   107  		if !ok {
   108  			return false
   109  		}
   110  		mod, ok = modVal.(*eval.Ns)
   111  		if !ok {
   112  			return false
   113  		}
   114  	}
   115  	return hasFn(mod, segs[len(segs)-1])
   116  }
   117  
   118  func hasFn(ns *eval.Ns, name string) bool {
   119  	fnVar, ok := ns.Index(name + eval.FnSuffix)
   120  	if !ok {
   121  		return false
   122  	}
   123  	_, ok = fnVar.(eval.Callable)
   124  	return ok
   125  }
   126  
   127  func isDirOrExecutable(fname string) bool {
   128  	stat, err := os.Stat(fname)
   129  	return err == nil && (stat.IsDir() || fsutil.IsExecutable(stat))
   130  }
   131  
   132  func hasExternalCommand(cmd string) bool {
   133  	_, err := exec.LookPath(cmd)
   134  	return err == nil
   135  }