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