diff --git a/copy.go b/copy.go index 8127a08a..ea848111 100644 --- a/copy.go +++ b/copy.go @@ -37,7 +37,7 @@ func copySize(srcs []string) (int64, error) { } func copyFile(src, dst string, preserve []string, info os.FileInfo, nums chan int64) error { - var dst_mode os.FileMode = 0666 + var dst_mode os.FileMode = 0o666 preserve_timestamps := false for _, s := range preserve { switch s { @@ -107,9 +107,9 @@ func copyAll(srcs []string, dstDir string, preserve []string) (nums chan int64, file := filepath.Base(src) dst := filepath.Join(dstDir, file) - _, err := os.Lstat(dst) + lstat, err := os.Lstat(dst) if !os.IsNotExist(err) { - ext := filepath.Ext(file) + ext := getFileExtension(lstat) basename := file[:len(file)-len(ext)] var newPath string for i := 1; !os.IsNotExist(err); i++ { diff --git a/eval.go b/eval.go index bdb7ff88..2e93823e 100644 --- a/eval.go +++ b/eval.go @@ -1611,8 +1611,8 @@ func (e *callExpr) eval(app *app, args []string) { } normal(app) app.ui.cmdPrefix = "rename: " - extension := filepath.Ext(curr.Name()) - if len(extension) == 0 || extension == curr.Name() || curr.IsDir() { + extension := getFileExtension(curr) + if len(extension) == 0 { // no extension or .hidden or is directory app.ui.cmdAccLeft = append(app.ui.cmdAccLeft, []rune(curr.Name())...) } else { diff --git a/misc.go b/misc.go index b38dab2d..63b162e4 100644 --- a/misc.go +++ b/misc.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "io" + "io/fs" "path/filepath" "regexp" "strconv" @@ -291,12 +292,30 @@ func naturalLess(s1, s2 string) bool { } } -var reModKey = regexp.MustCompile(`<(c|s|a)-(.+)>`) -var reRulerSub = regexp.MustCompile(`%[apmcsfithd]|%\{[^}]+\}`) +// This function returns the extension of a file with a leading dot +// it returns an empty string if extension could not be determined +// i.e. directories, filenames without extensions +func getFileExtension(file fs.FileInfo) string { + if file.IsDir() { + return "" + } + if strings.Count(file.Name(), ".") == 1 && file.Name()[0] == '.' { + // hidden file without extension + return "" + } + return filepath.Ext(file.Name()) +} -var reWord = regexp.MustCompile(`(\pL|\pN)+`) -var reWordBeg = regexp.MustCompile(`([^\pL\pN]|^)(\pL|\pN)`) -var reWordEnd = regexp.MustCompile(`(\pL|\pN)([^\pL\pN]|$)`) +var ( + reModKey = regexp.MustCompile(`<(c|s|a)-(.+)>`) + reRulerSub = regexp.MustCompile(`%[apmcsfithd]|%\{[^}]+\}`) +) + +var ( + reWord = regexp.MustCompile(`(\pL|\pN)+`) + reWordBeg = regexp.MustCompile(`([^\pL\pN]|^)(\pL|\pN)`) + reWordEnd = regexp.MustCompile(`(\pL|\pN)([^\pL\pN]|$)`) +) func min(a, b int) int { if a < b { diff --git a/misc_test.go b/misc_test.go index 758e3429..52b26281 100644 --- a/misc_test.go +++ b/misc_test.go @@ -5,6 +5,7 @@ import ( "reflect" "strings" "testing" + "time" ) func TestIsRoot(t *testing.T) { @@ -293,3 +294,40 @@ func TestNaturalLess(t *testing.T) { } } } + +type fakeFileInfo struct { + name string + isDir bool +} + +func (fileinfo fakeFileInfo) Name() string { return fileinfo.name } +func (fileinfo fakeFileInfo) Size() int64 { return 0 } +func (fileinfo fakeFileInfo) Mode() os.FileMode { return os.FileMode(0o000) } +func (fileinfo fakeFileInfo) ModTime() time.Time { return time.Unix(0, 0) } +func (fileinfo fakeFileInfo) IsDir() bool { return fileinfo.isDir } +func (fileinfo fakeFileInfo) Sys() any { return nil } + +func TestGetFileExtension(t *testing.T) { + tests := []struct { + name string + fileName string + isDir bool + expectedExtension string + }{ + {"normal file", "file.txt", false, ".txt"}, + {"file without extension", "file", false, ""}, + {"hidden file", ".gitignore", false, ""}, + {"hidden file with extension", ".file.txt", false, ".txt"}, + {"directory", "dir", true, ""}, + {"hidden directory", ".git", true, ""}, + {"directory with dot", "profile.d", true, ""}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := getFileExtension(fakeFileInfo{test.fileName, test.isDir}); got != test.expectedExtension { + t.Errorf("at input %q expected %q but got %q", test.fileName, test.expectedExtension, got) + } + }) + } +} diff --git a/nav.go b/nav.go index f620222e..34df1e6f 100644 --- a/nav.go +++ b/nav.go @@ -55,7 +55,7 @@ type fakeStat struct { func (fs *fakeStat) Name() string { return fs.name } func (fs *fakeStat) Size() int64 { return 0 } -func (fs *fakeStat) Mode() os.FileMode { return os.FileMode(0000) } +func (fs *fakeStat) Mode() os.FileMode { return os.FileMode(0o000) } func (fs *fakeStat) ModTime() time.Time { return time.Unix(0, 0) } func (fs *fakeStat) IsDir() bool { return false } func (fs *fakeStat) Sys() any { return nil } @@ -88,7 +88,7 @@ func readdir(path string) ([]*file, error) { dirSize: -1, accessTime: time.Unix(0, 0), changeTime: time.Unix(0, 0), - ext: filepath.Ext(fpath), + ext: getFileExtension(lstat), err: err, }) continue @@ -122,10 +122,6 @@ func readdir(path string) ([]*file, error) { ct = lstat.ModTime() } - // returns an empty string if extension could not be determined - // i.e. directories, filenames without extensions - ext := filepath.Ext(fpath) - dirCount := -1 if lstat.IsDir() && gOpts.dircounts { d, err := os.Open(fpath) @@ -152,7 +148,7 @@ func readdir(path string) ([]*file, error) { dirSize: -1, accessTime: at, changeTime: ct, - ext: ext, + ext: getFileExtension(lstat), err: nil, }) } @@ -492,7 +488,6 @@ func (nav *nav) loadDirInternal(path string) *dir { nav.dirPreviewChan <- d } nav.dirChan <- d - }() return d } @@ -773,7 +768,6 @@ func matchPattern(pattern, name, path string) bool { } func (nav *nav) previewDir(dir *dir, win *win) { - defer func() { dir.loading = false nav.dirChan <- dir @@ -829,11 +823,9 @@ func (nav *nav) previewDir(dir *dir, win *win) { log.Printf("loading dir: %s", buf.Err()) } } - } func (nav *nav) preview(path string, win *win) { - reg := ®{loadTime: time.Now(), path: path} defer func() { nav.regChan <- reg }() @@ -1408,7 +1400,7 @@ func (nav *nav) moveAsync(app *app, srcs []string, dstDir string) { app.ui.exprChan <- echo continue } else if !os.IsNotExist(err) { - ext := filepath.Ext(file) + ext := getFileExtension(dstStat) basename := file[:len(file)-len(ext)] var newPath string for i := 1; !os.IsNotExist(err); i++ { @@ -1991,7 +1983,6 @@ func (m indexedSelections) Swap(i, j int) { func (m indexedSelections) Less(i, j int) bool { return m.indices[i] < m.indices[j] } func (nav *nav) currSelections() []string { - currDirOnly := gOpts.selmode == "dir" currDirPath := "" if currDirOnly {