6422bd7492
* Fix replace dialog for symlinks If the oldPath is a symlink to the newPath or vice versa, than os.Stat() would resolve this symlink, and both the oldStat and newStat would be the same. Hence, the replace dialog would not appear and the newPath file would be overwritten by the oldPath file whilst the oldPath would be deleted. It is the same story when the oldPath and newPath are both symlinks to the same file. * Fix completion in the case of broken symlinks If the current directory contains broken symlinks then matchFile() would return at first broken symlink. Let's consider the following example: $ ls -F ~/ broken@ dir/ file The broken@ is a symlink to ~/foo - non existent file. If one would enter the following command in lf: :cd ~/<tab> it would not suggest possible completion options because matchFile() would return as soon as it meet the broken symlink. * Don't resolve symlinks when move files/dirs (#571) This will allow to move broken symlinks. * Fix symlinks copying/moving (#571) Copy symlinks, do not try to resolve it and copy the file pointed by this symlink. Also this allows to copy symlink to directory.
399 lines
6.6 KiB
Go
399 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
gCmdWords = []string{
|
|
"set",
|
|
"map",
|
|
"cmd",
|
|
"up",
|
|
"half-up",
|
|
"page-up",
|
|
"down",
|
|
"half-down",
|
|
"page-down",
|
|
"updir",
|
|
"open",
|
|
"quit",
|
|
"top",
|
|
"bottom",
|
|
"toggle",
|
|
"invert",
|
|
"unselect",
|
|
"copy",
|
|
"cut",
|
|
"paste",
|
|
"clear",
|
|
"redraw",
|
|
"reload",
|
|
"read",
|
|
"rename",
|
|
"shell",
|
|
"shell-pipe",
|
|
"shell-wait",
|
|
"shell-async",
|
|
"find",
|
|
"find-back",
|
|
"find-next",
|
|
"find-prev",
|
|
"search",
|
|
"search-back",
|
|
"search-next",
|
|
"search-prev",
|
|
"mark-save",
|
|
"mark-remove",
|
|
"mark-load",
|
|
"draw",
|
|
"load",
|
|
"sync",
|
|
"echo",
|
|
"echomsg",
|
|
"echoerr",
|
|
"cd",
|
|
"select",
|
|
"glob-select",
|
|
"glob-unselect",
|
|
"source",
|
|
"push",
|
|
"delete",
|
|
}
|
|
|
|
gOptWords = []string{
|
|
"anchorfind",
|
|
"noanchorfind",
|
|
"anchorfind!",
|
|
"dircounts",
|
|
"nodircounts",
|
|
"dircounts!",
|
|
"dirfirst",
|
|
"nodirfirst",
|
|
"dirfirst!",
|
|
"drawbox",
|
|
"nodrawbox",
|
|
"drawbox!",
|
|
"globsearch",
|
|
"noglobsearch",
|
|
"globsearch!",
|
|
"hidden",
|
|
"nohidden",
|
|
"hidden!",
|
|
"icons",
|
|
"noicons",
|
|
"icons!",
|
|
"ignorecase",
|
|
"noignorecase",
|
|
"ignorecase!",
|
|
"ignoredia",
|
|
"noignoredia",
|
|
"ignoredia!",
|
|
"incsearch",
|
|
"noincsearch",
|
|
"incsearch!",
|
|
"mouse",
|
|
"nomouse",
|
|
"mouse!",
|
|
"number",
|
|
"nonumber",
|
|
"number!",
|
|
"preview",
|
|
"nopreview",
|
|
"preview!",
|
|
"relativenumber",
|
|
"norelativenumber",
|
|
"relativenumber!",
|
|
"reverse",
|
|
"noreverse",
|
|
"reverse!",
|
|
"smartcase",
|
|
"nosmartcase",
|
|
"smartcase!",
|
|
"smartdia",
|
|
"nosmartdia",
|
|
"smartdia!",
|
|
"wrapscan",
|
|
"nowrapscan",
|
|
"wrapscan!",
|
|
"wrapscroll",
|
|
"nowrapscroll",
|
|
"wrapscroll!",
|
|
"findlen",
|
|
"period",
|
|
"scrolloff",
|
|
"tabstop",
|
|
"errorfmt",
|
|
"filesep",
|
|
"hiddenfiles",
|
|
"ifs",
|
|
"info",
|
|
"previewer",
|
|
"cleaner",
|
|
"promptfmt",
|
|
"ratios",
|
|
"shell",
|
|
"shellopts",
|
|
"sortby",
|
|
"timefmt",
|
|
"truncatechar",
|
|
}
|
|
)
|
|
|
|
func matchLongest(s1, s2 string) string {
|
|
i := 0
|
|
for ; i < len(s1) && i < len(s2); i++ {
|
|
if s1[i] != s2[i] {
|
|
break
|
|
}
|
|
}
|
|
return s1[:i]
|
|
}
|
|
|
|
func matchWord(s string, words []string) (matches []string, longest string) {
|
|
for _, w := range words {
|
|
if !strings.HasPrefix(w, s) {
|
|
continue
|
|
}
|
|
|
|
matches = append(matches, w)
|
|
if longest != "" {
|
|
longest = matchLongest(longest, w)
|
|
} else if s != "" {
|
|
longest = w + " "
|
|
}
|
|
}
|
|
|
|
if longest == "" {
|
|
longest = s
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func matchExec(s string) (matches []string, longest string) {
|
|
var words []string
|
|
|
|
paths := strings.Split(envPath, string(filepath.ListSeparator))
|
|
|
|
for _, p := range paths {
|
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
|
|
files, err := ioutil.ReadDir(p)
|
|
if err != nil {
|
|
log.Printf("reading path: %s", err)
|
|
}
|
|
|
|
for _, f := range files {
|
|
if !strings.HasPrefix(f.Name(), s) {
|
|
continue
|
|
}
|
|
|
|
f, err = os.Stat(filepath.Join(p, f.Name()))
|
|
if err != nil {
|
|
log.Printf("getting file information: %s", err)
|
|
continue
|
|
}
|
|
|
|
if !f.Mode().IsRegular() || !isExecutable(f) {
|
|
continue
|
|
}
|
|
|
|
log.Print(f.Name())
|
|
words = append(words, f.Name())
|
|
}
|
|
}
|
|
|
|
sort.Strings(words)
|
|
|
|
if len(words) > 0 {
|
|
uniq := words[:1]
|
|
for i := 1; i < len(words); i++ {
|
|
if words[i] != words[i-1] {
|
|
uniq = append(uniq, words[i])
|
|
}
|
|
}
|
|
words = uniq
|
|
}
|
|
|
|
return matchWord(s, words)
|
|
}
|
|
|
|
func matchFile(s string) (matches []string, longest string) {
|
|
dir := replaceTilde(s)
|
|
|
|
if !filepath.IsAbs(dir) {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
log.Printf("getting current directory: %s", err)
|
|
} else {
|
|
dir = wd + string(filepath.Separator) + dir
|
|
}
|
|
}
|
|
|
|
dir = unescape(filepath.Dir(dir))
|
|
|
|
files, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
log.Printf("reading directory: %s", err)
|
|
}
|
|
|
|
for _, f := range files {
|
|
name := filepath.Join(dir, f.Name())
|
|
f, err := os.Stat(name)
|
|
if err != nil {
|
|
fl, err := os.Lstat(name)
|
|
if err == nil && fl.Mode() & os.ModeSymlink != 0 {
|
|
continue
|
|
} else {
|
|
log.Printf("getting file information: %s", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
_, last := filepath.Split(s)
|
|
if !strings.HasPrefix(escape(f.Name()), last) {
|
|
continue
|
|
}
|
|
|
|
name = f.Name()
|
|
if isRoot(s) || filepath.Base(s) != s {
|
|
name = filepath.Join(filepath.Dir(unescape(s)), f.Name())
|
|
}
|
|
name = escape(name)
|
|
|
|
item := f.Name()
|
|
if f.Mode().IsDir() {
|
|
item += escape(string(filepath.Separator))
|
|
}
|
|
matches = append(matches, item)
|
|
|
|
if longest != "" {
|
|
longest = matchLongest(longest, name)
|
|
} else if s != "" {
|
|
if f.Mode().IsRegular() {
|
|
longest = name + " "
|
|
} else {
|
|
longest = name + escape(string(filepath.Separator))
|
|
}
|
|
}
|
|
}
|
|
|
|
if longest == "" {
|
|
longest = s
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func completeCmd(acc []rune) (matches []string, longestAcc []rune) {
|
|
s := string(acc)
|
|
f := tokenize(s)
|
|
|
|
var longest string
|
|
|
|
switch len(f) {
|
|
case 1:
|
|
words := gCmdWords
|
|
for c := range gOpts.cmds {
|
|
words = append(words, c)
|
|
}
|
|
sort.Strings(words)
|
|
j := 0
|
|
for i := 1; i < len(words); i++ {
|
|
if words[j] == words[i] {
|
|
continue
|
|
}
|
|
j++
|
|
words[i], words[j] = words[j], words[i]
|
|
}
|
|
words = words[:j+1]
|
|
matches, longest = matchWord(s, words)
|
|
longestAcc = []rune(longest)
|
|
case 2:
|
|
switch f[0] {
|
|
case "set":
|
|
matches, longest = matchWord(f[1], gOptWords)
|
|
longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
|
|
case "map", "cmd":
|
|
longestAcc = acc
|
|
default:
|
|
matches, longest = matchFile(f[len(f)-1])
|
|
longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
|
|
}
|
|
default:
|
|
switch f[0] {
|
|
case "set", "map", "cmd":
|
|
longestAcc = acc
|
|
default:
|
|
matches, longest = matchFile(f[len(f)-1])
|
|
longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func completeFile(acc []rune) (matches []string, longestAcc []rune) {
|
|
s := string(acc)
|
|
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
log.Printf("getting current directory: %s", err)
|
|
}
|
|
|
|
files, err := ioutil.ReadDir(wd)
|
|
if err != nil {
|
|
log.Printf("reading directory: %s", err)
|
|
}
|
|
|
|
var longest string
|
|
|
|
for _, f := range files {
|
|
if !strings.HasPrefix(f.Name(), s) {
|
|
continue
|
|
}
|
|
|
|
matches = append(matches, f.Name())
|
|
|
|
if longest != "" {
|
|
longest = matchLongest(longest, f.Name())
|
|
} else if s != "" {
|
|
longest = f.Name()
|
|
}
|
|
}
|
|
|
|
if longest == "" {
|
|
longest = s
|
|
}
|
|
|
|
longestAcc = []rune(longest)
|
|
|
|
return
|
|
}
|
|
|
|
func completeShell(acc []rune) (matches []string, longestAcc []rune) {
|
|
s := string(acc)
|
|
f := tokenize(s)
|
|
|
|
var longest string
|
|
|
|
switch len(f) {
|
|
case 1:
|
|
matches, longest = matchExec(s)
|
|
longestAcc = []rune(longest)
|
|
default:
|
|
matches, longest = matchFile(f[len(f)-1])
|
|
longestAcc = append(acc[:len(acc)-len([]rune(f[len(f)-1]))], []rune(longest)...)
|
|
}
|
|
|
|
return
|
|
}
|