star/bed/cmdline/completor.go
2025-04-26 19:33:14 +08:00

267 lines
6.4 KiB
Go

package cmdline
import (
"os"
"path/filepath"
"slices"
"strings"
"unicode"
"unicode/utf8"
"b612.me/apps/b612/bed/event"
)
type completor struct {
fs fs
env env
command bool
target string
arg string
results []string
index int
}
func newCompletor(fs fs, env env) *completor {
return &completor{fs: fs, env: env}
}
func (c *completor) complete(cmdline string, forward bool) string {
cmd, r, _, name, prefix, arg, _ := parse(cmdline)
if name == "" || c.command ||
!hasSuffixFunc(prefix, unicode.IsSpace) && cmd.fullname != name {
cmdline = c.completeCommand(cmdline, name, prefix, r, forward)
if c.results != nil {
return cmdline
}
prefix = cmdline
}
switch cmd.eventType {
case event.Edit, event.New, event.Vnew, event.Write, event.WriteQuit:
return c.completeFilepath(cmdline, prefix, arg, forward, false)
case event.Chdir:
return c.completeFilepath(cmdline, prefix, arg, forward, true)
case event.Wincmd:
return c.completeWincmd(cmdline, prefix, arg, forward)
default:
return cmdline
}
}
func (c *completor) completeNext(prefix string, forward bool) string {
if len(c.results) == 0 {
return c.target
}
if forward {
c.index = (c.index+2)%(len(c.results)+1) - 1
} else {
c.index = (c.index+len(c.results)+1)%(len(c.results)+1) - 1
}
if c.index < 0 {
return c.target
}
if len(c.results) == 1 {
defer c.clear()
}
return prefix + c.arg + c.results[c.index]
}
func (c *completor) completeCommand(
cmdline, name, prefix string, r *event.Range, forward bool,
) string {
prefix = prefix[:len(prefix)-len(name)]
if c.results == nil {
c.command, c.target, c.index = true, cmdline, -1
c.arg, c.results = "", listCommandNames(name, r)
}
return c.completeNext(prefix, forward)
}
func listCommandNames(name string, r *event.Range) []string {
var targets []string
for _, cmd := range commands {
if strings.HasPrefix(cmd.fullname, name) && cmd.rangeType.allows(r) {
targets = append(targets, cmd.fullname)
}
}
slices.Sort(targets)
return targets
}
func (c *completor) completeFilepath(
cmdline, prefix, arg string, forward, dirOnly bool,
) string {
if !hasSuffixFunc(prefix, unicode.IsSpace) {
prefix += " "
}
if c.results == nil {
c.command, c.target, c.index = false, cmdline, -1
c.arg, c.results = c.listFileNames(arg, dirOnly)
}
return c.completeNext(prefix, forward)
}
const separator = string(filepath.Separator)
func (c *completor) listFileNames(arg string, dirOnly bool) (string, []string) {
var targets []string
path, simplify := c.expandPath(arg)
if strings.HasPrefix(arg, "$") && !strings.Contains(arg, separator) {
base := strings.ToLower(arg[1:])
for _, env := range c.env.List() {
name, value, ok := strings.Cut(env, "=")
if !ok {
continue
}
if !strings.HasPrefix(strings.ToLower(name), base) {
continue
}
if !filepath.IsAbs(value) {
continue
}
fi, err := c.fs.Stat(value)
if err != nil {
continue
}
if fi.IsDir() {
name += separator
} else if dirOnly {
continue
}
targets = append(targets, "$"+name)
}
slices.Sort(targets)
return "", targets
}
if arg != "" && !strings.HasSuffix(arg, separator) &&
(!strings.HasSuffix(arg, ".") || strings.HasSuffix(arg, "..")) {
if stat, err := c.fs.Stat(path); err == nil && stat.IsDir() {
return "", []string{arg + separator}
}
}
if strings.HasSuffix(arg, separator) || strings.HasSuffix(arg, separator+".") {
path += separator
}
dir, base := filepath.Dir(path), strings.ToLower(filepath.Base(path))
if arg == "" {
base = ""
} else if strings.HasSuffix(path, separator) {
if strings.HasSuffix(arg, separator+".") {
base = "."
} else {
base = ""
}
}
f, err := c.fs.Open(dir)
if err != nil {
return arg, nil
}
defer f.Close()
fileInfos, err := f.Readdir(1024)
if err != nil {
return arg, nil
}
for _, fileInfo := range fileInfos {
name := fileInfo.Name()
if !strings.HasPrefix(strings.ToLower(name), base) {
continue
}
isDir := fileInfo.IsDir()
if !isDir && fileInfo.Mode()&os.ModeSymlink != 0 {
fileInfo, err := c.fs.Stat(filepath.Join(dir, name))
if err != nil {
continue
}
isDir = fileInfo.IsDir()
}
if isDir {
name += separator
} else if dirOnly {
continue
}
targets = append(targets, name)
}
slices.SortFunc(targets, func(p, q string) int {
ps, pd := p[len(p)-1] == filepath.Separator, p[0] == '.'
qs, qd := q[len(q)-1] == filepath.Separator, q[0] == '.'
switch {
case ps && !qs:
return 1
case !ps && qs:
return -1
case pd && !qd:
return 1
case !pd && qd:
return -1
default:
return strings.Compare(p, q)
}
})
if simplify != nil {
arg = simplify(dir) + separator
} else if !strings.HasPrefix(arg, "."+separator) && dir == "." {
arg = ""
} else if arg = dir; !strings.HasSuffix(arg, separator) {
arg += separator
}
return arg, targets
}
func (c *completor) expandPath(path string) (string, func(string) string) {
switch {
case strings.HasPrefix(path, "~"):
if name, rest, _ := strings.Cut(path[1:], separator); name != "" {
user, err := c.fs.GetUser(name)
if err != nil {
return path, nil
}
return filepath.Join(user.HomeDir, rest), func(path string) string {
return filepath.Join("~"+user.Username, strings.TrimPrefix(path, user.HomeDir))
}
}
homedir, err := c.fs.UserHomeDir()
if err != nil {
return path, nil
}
return filepath.Join(homedir, path[1:]), func(path string) string {
return filepath.Join("~", strings.TrimPrefix(path, homedir))
}
case strings.HasPrefix(path, "$"):
name, rest, _ := strings.Cut(path[1:], separator)
value := strings.TrimRight(c.env.Get(name), separator)
if value == "" {
return path, nil
}
return filepath.Join(value, rest), func(path string) string {
return filepath.Join("$"+name, strings.TrimPrefix(path, value))
}
default:
return path, nil
}
}
func (c *completor) completeWincmd(
cmdline, prefix, arg string, forward bool,
) string {
if !hasSuffixFunc(prefix, unicode.IsSpace) {
prefix += " "
}
if c.results == nil {
if arg != "" {
return cmdline
}
c.command, c.target, c.arg, c.index = false, cmdline, "", -1
c.results = strings.Split("nohjkltbpHJKL", "")
}
return c.completeNext(prefix, forward)
}
func (c *completor) clear() {
c.command, c.target, c.arg = false, "", ""
c.results, c.index = nil, 0
}
func hasSuffixFunc(s string, f func(rune) bool) bool {
r, size := utf8.DecodeLastRuneInString(s)
return size > 0 && f(r)
}