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 }