mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-14 11:08:13 +08:00
356 lines
8.7 KiB
Go
356 lines
8.7 KiB
Go
// Package location implements the location mode for the editor.
|
|
package location
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/elves/elvish/edit/eddefs"
|
|
"github.com/elves/elvish/edit/ui"
|
|
"github.com/elves/elvish/eval"
|
|
"github.com/elves/elvish/eval/vals"
|
|
"github.com/elves/elvish/eval/vars"
|
|
"github.com/elves/elvish/parse"
|
|
"github.com/elves/elvish/store/storedefs"
|
|
"github.com/elves/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.AddDir(pwd, 1)
|
|
if err != nil {
|
|
logger.Println("add dir in after-chdir hook:", err)
|
|
return
|
|
}
|
|
ws := matchWorkspace(pwd, workspaces)
|
|
if ws == nil {
|
|
return
|
|
}
|
|
err = store.AddDir(ws.workspacify(pwd), 1)
|
|
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
|
|
|
|
daemon := ed.Daemon()
|
|
if daemon == nil {
|
|
ed.Notify("store offline, cannot start location mode")
|
|
return
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
type matchDirPatternOpts struct {
|
|
IgnoreCase bool
|
|
}
|
|
|
|
func (*matchDirPatternOpts) SetDefaultOptions() {}
|
|
|
|
func matchDirPattern(fm *eval.Frame, opts matchDirPatternOpts, pattern string, inputs eval.Inputs) {
|
|
p := makeLocationFilterPattern(pattern, opts.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))
|
|
})
|
|
}
|