forked from u-root/u-root
/
location.go
356 lines (314 loc) · 8.72 KB
/
location.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
// Package location implements the location mode for the editor.
package location
import (
"bytes"
"fmt"
"math"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/u-root/u-root/cmds/elvish/edit/eddefs"
"github.com/u-root/u-root/cmds/elvish/edit/ui"
"github.com/u-root/u-root/cmds/elvish/eval"
"github.com/u-root/u-root/cmds/elvish/eval/vals"
"github.com/u-root/u-root/cmds/elvish/eval/vars"
"github.com/u-root/u-root/cmds/elvish/parse"
"github.com/u-root/u-root/cmds/elvish/store/storedefs"
"github.com/u-root/u-root/cmds/elvish/util"
"github.com/xiaq/persistent/hashmap"
"github.com/xiaq/persistent/vector"
)
var logger = util.GetLogger("[edit/location] ")
// pinnedScore is a special value of Score in storedefs.Dir to represent that the
// directory is pinned.
var pinnedScore = math.Inf(1)
type mode struct {
editor eddefs.Editor
binding eddefs.BindingMap
hidden vector.Vector
pinned vector.Vector
workspaces hashmap.Map
matcher eval.Callable
}
var matchDirPatternBuiltin = eval.NewBuiltinFn("edit:location:match-dir-pattern", matchDirPattern)
// Init initializes the location mode for an Editor.
func Init(ed eddefs.Editor, ns eval.Ns) {
m := &mode{ed, eddefs.EmptyBindingMap,
vals.EmptyList, vals.EmptyList, vals.EmptyMap, matchDirPatternBuiltin}
ns.AddNs("location",
eval.Ns{
"binding": vars.FromPtr(&m.binding),
"hidden": vars.FromPtr(&m.hidden),
"pinned": vars.FromPtr(&m.pinned),
"matcher": vars.FromPtr(&m.matcher),
"workspaces": vars.FromPtr(&m.workspaces),
}.AddBuiltinFn("edit:location:", "start", m.start).
AddFn("match-dir-pattern", matchDirPatternBuiltin))
/*
ed.Evaler().AddAfterChdir(func(string) {
store := ed.Daemon()
if store == nil {
return
}
pwd, err := os.Getwd()
if err != nil {
logger.Println("Failed to get pwd in after-chdir hook:", err)
}
go addDir(store, pwd, m.workspaces)
})
*/
}
func addDir(store storedefs.Store, pwd string, workspaces hashmap.Map) {
_, err := store.Add(pwd)
if err != nil {
logger.Println("add dir in after-chdir hook:", err)
return
}
ws := matchWorkspace(pwd, workspaces)
if ws == nil {
return
}
_, err = store.Add(ws.workspacify(pwd))
if err != nil {
logger.Println("add workspacified dir in after-chdir hook:", err)
}
}
type wsInfo struct {
name string
root string
}
func matchWorkspace(dir string, workspaces hashmap.Map) *wsInfo {
for it := workspaces.Iterator(); it.HasElem(); it.Next() {
k, v := it.Elem()
name, ok := k.(string)
if !ok {
// TODO: Surface to user
logger.Println("$workspaces key not string", k)
continue
}
if strings.HasPrefix(name, "/") {
// TODO: Surface to user
logger.Println("$workspaces key starts with /", k)
continue
}
pattern, ok := v.(string)
if !ok {
// TODO: Surface to user
logger.Println("$workspaces value not string", v)
continue
}
if !strings.HasPrefix(pattern, "^") {
pattern = "^" + pattern
}
re, err := regexp.Compile(pattern)
if err != nil {
// TODO: Surface to user
logger.Println("$workspaces pattern invalid", pattern)
continue
}
if ws := re.FindString(dir); ws != "" {
return &wsInfo{name, ws}
}
}
return nil
}
func (w *wsInfo) workspacify(dir string) string {
return w.name + dir[len(w.root):]
}
func (w *wsInfo) unworkspacify(dir string) string {
return w.root + dir[len(w.name):]
}
func (m *mode) start() {
ed := m.editor
ed.Notify("store offline, cannot start location mode")
return
/* what a fucking mess this is.
// Pinned directories are also blacklisted to prevent them from showing up
// twice.
black := convertListsToSet(m.hidden, m.pinned)
pwd, err := os.Getwd()
if err == nil {
black[pwd] = struct{}{}
}
stored, err := daemon.Dirs(black)
if err != nil {
ed.Notify("store error: %v", err)
return
}
// TODO: Move workspace filtering to the daemon.
ws := matchWorkspace(pwd, m.workspaces)
wsName := ""
if ws != nil {
wsName = ws.name
}
wsNameSlash := wsName + string(filepath.Separator)
var filtered []storedefs.Dir
for _, dir := range stored {
if filepath.IsAbs(dir.Path) || (wsName != "" && (dir.Path == wsName || strings.HasPrefix(dir.Path, wsNameSlash))) {
filtered = append(filtered, dir)
}
}
// Prepend pinned dirs.
pinnedDirs := convertListToDirs(m.pinned)
dirs := make([]storedefs.Dir, len(pinnedDirs)+len(filtered))
copy(dirs, pinnedDirs)
copy(dirs[len(pinnedDirs):], filtered)
// Drop the error. When there is an error, home is "", which is used to
// signify "no home known" in location.
home, _ := util.GetHome("")
ed.SetModeListing(m.binding,
newProvider(dirs, home, ws, ed.Evaler(), m.matcher))
*/
}
// convertListToDirs converts a list of strings to []storedefs.Dir. It uses the
// special score of pinnedScore to signify that the directory is pinned.
func convertListToDirs(li vector.Vector) []storedefs.Dir {
pinned := make([]storedefs.Dir, 0, li.Len())
// XXX(xiaq): silently drops non-string items.
for it := li.Iterator(); it.HasElem(); it.Next() {
if s, ok := it.Elem().(string); ok {
pinned = append(pinned, storedefs.Dir{s, pinnedScore})
}
}
return pinned
}
func convertListsToSet(lis ...vector.Vector) map[string]struct{} {
set := make(map[string]struct{})
// XXX(xiaq): silently drops non-string items.
for _, li := range lis {
for it := li.Iterator(); it.HasElem(); it.Next() {
if s, ok := it.Elem().(string); ok {
set[s] = struct{}{}
}
}
}
return set
}
type provider struct {
all []storedefs.Dir
filtered []storedefs.Dir
home string // The home directory; leave empty if unknown.
ws *wsInfo
ev *eval.Evaler
matcher eval.Callable
}
func newProvider(dirs []storedefs.Dir, home string, ws *wsInfo, ev *eval.Evaler, matcher eval.Callable) *provider {
return &provider{dirs, nil, home, ws, ev, matcher}
}
func (*provider) ModeTitle(i int) string {
return " LOCATION "
}
func (*provider) CursorOnModeLine() bool {
return true
}
func (p *provider) Len() int {
return len(p.filtered)
}
func (p *provider) Show(i int) (string, ui.Styled) {
var header string
score := p.filtered[i].Score
if score == pinnedScore {
header = "*"
} else {
header = fmt.Sprintf("%.0f", score)
}
return header, ui.Unstyled(showPath(p.filtered[i].Path, p.home))
}
func showPath(path, home string) string {
if home != "" && path == home {
return "~"
} else if home != "" && strings.HasPrefix(path, home+"/") {
return "~/" + parse.Quote(path[len(home)+1:])
} else {
return parse.Quote(path)
}
}
func (p *provider) Filter(filter string) int {
p.filtered = nil
// TODO: this is just a replica of `filterRawCandidates`.
matcherInput := make(chan interface{}, len(p.all))
stopCollector := make(chan struct{})
go func() {
defer close(matcherInput)
for _, item := range p.all {
select {
case matcherInput <- showPath(item.Path, p.home):
logger.Printf("put %s\n", item.Path)
case <-stopCollector:
return
}
}
}()
defer close(stopCollector)
ports := []*eval.Port{
{Chan: matcherInput, File: eval.DevNull}, {File: os.Stdout}, {File: os.Stderr}}
ec := eval.NewTopFrame(p.ev, eval.NewInternalSource("[editor matcher]"), ports)
args := []interface{}{filter}
values, err := ec.CaptureOutput(p.matcher, args, eval.NoOpts)
if err != nil {
logger.Printf("failed to match %s: %v", filter, err)
return -1
} else if got, expect := len(values), len(p.all); got != expect {
logger.Printf("wrong match count: got %d, want %d", got, expect)
return -1
}
for i, value := range values {
if vals.Bool(value) {
p.filtered = append(p.filtered, p.all[i])
}
}
if len(p.filtered) == 0 {
return -1
}
return 0
}
var emptyRegexp = regexp.MustCompile("")
func makeLocationFilterPattern(s string, ignoreCase bool) *regexp.Regexp {
var b bytes.Buffer
if ignoreCase {
b.WriteString("(?i)")
}
b.WriteString(".*")
segs := strings.Split(s, "/")
for i, seg := range segs {
if i > 0 {
b.WriteString(".*/.*")
}
b.WriteString(regexp.QuoteMeta(seg))
}
b.WriteString(".*")
p, err := regexp.Compile(b.String())
if err != nil {
logger.Printf("failed to compile regexp %q: %v", b.String(), err)
return emptyRegexp
}
return p
}
func (p *provider) Accept(i int, ed eddefs.Editor) {
path := p.filtered[i].Path
if !filepath.IsAbs(path) {
path = p.ws.unworkspacify(path)
}
err := ed.Evaler().Chdir(path)
if err != nil {
ed.Notify("%v", err)
}
ed.SetModeInsert()
}
func matchDirPattern(fm *eval.Frame, opts eval.RawOptions, pattern string, inputs eval.Inputs) {
var options struct {
IgnoreCase bool
}
opts.Scan(&options)
p := makeLocationFilterPattern(pattern, options.IgnoreCase)
out := fm.OutputChan()
inputs(func(v interface{}) {
s, ok := v.(string)
if !ok {
logger.Printf("input item must be string, but got %#v", v)
return
}
out <- vals.Bool(p.MatchString(s))
})
}