From c1e8272b3839f4d960906945eefdc3311e21570c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=86=E7=A6=BB?= Date: Fri, 25 Oct 2024 16:07:22 +0800 Subject: [PATCH 1/5] Add support for localized sorting --- doc.md | 11 ++++++- eval.go | 22 ++++++++++++++ go.mod | 3 +- go.sum | 6 ++++ misc.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ misc_test.go | 49 ++++++++++++++++++++++++++++++ nav.go | 47 +++++++++++++++++++++++++--- opts.go | 13 ++++++++ 8 files changed, 231 insertions(+), 6 deletions(-) diff --git a/doc.md b/doc.md index cb0fbdb3..fa95ccba 100644 --- a/doc.md +++ b/doc.md @@ -169,6 +169,7 @@ The following options can be used to customize the behavior of lf: info []string (default '') infotimefmtnew string (default 'Jan _2 15:04') infotimefmtold string (default 'Jan _2 2006') + locale string (default '') mouse bool (default false) number bool (default false) numberfmt string (default "\033[33m") @@ -823,6 +824,14 @@ Format string of the file time shown in the info column when it matches this yea Format string of the file time shown in the info column when it doesn't match this year. +## locale (string) (default `''`) + +An IETF BCP 47 language tag for specifying locale used when when using sort type +`natural` and `name`. + +Empty string means disable locale ordering, and a special value `'*'` is used +to indicate reading locale setting from system environment. + ## mouse (bool) (default false) Send mouse events as input. @@ -1184,7 +1193,7 @@ Command `set` is used to set an option which can be a boolean, integer, or strin set sortby "time" # string value with double quotes (backslash escapes) Command `setlocal` is used to set a local option for a directory which can be a boolean or string. -Currently supported local options are `dirfirst`, `dironly`, `hidden`, `info`, `reverse`, and `sortby`. +Currently supported local options are `dirfirst`, `dironly`, `hidden`, `info`, `reverse`, `sortby` and `locale`. Adding a trailing path separator (i.e. `/` for Unix and `\` for Windows) sets the option for the given directory along with its subdirectories: setlocal /foo/bar hidden # boolean enable diff --git a/eval.go b/eval.go index f68eb22a..340ec4a9 100644 --- a/eval.go +++ b/eval.go @@ -269,6 +269,18 @@ func (e *setExpr) eval(app *app, args []string) { } } gOpts.info = toks + case "locale": + localeStr := e.val + if gOpts.locale != localeStr { + if localeStr != localeStrDisable { + _, err = getLocaleTag(localeStr) + } + if err == nil { + gOpts.locale = localeStr + app.nav.sort() + app.ui.sort() + } + } case "rulerfmt": gOpts.rulerfmt = e.val case "preserve": @@ -500,6 +512,16 @@ func (e *setLocalExpr) eval(app *app, args []string) { gLocalOpts.sortbys[e.path] = method app.nav.sort() app.ui.sort() + case "locale": + localeStr := e.val + if localeStr != localeStrDisable { + _, err = getLocaleTag(localeStr) + } + if err == nil { + gLocalOpts.locales[e.path] = localeStr + app.nav.sort() + app.ui.sort() + } default: err = fmt.Errorf("unknown option: %s", e.opt) } diff --git a/go.mod b/go.mod index c221ec4f..f0800d5a 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,15 @@ require ( github.com/djherbis/times v1.6.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gdamore/tcell/v2 v2.7.4 + github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 github.com/mattn/go-runewidth v0.0.16 golang.org/x/sys v0.26.0 golang.org/x/term v0.25.0 + golang.org/x/text v0.14.0 ) require ( github.com/gdamore/encoding v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect - golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 1123cf5f..040316b0 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -6,14 +7,18 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= +github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 h1:Po+wkNdMmN+Zj1tDsJQy7mJlPlwGNQd9JZoPjObagf8= +github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49/go.mod h1:YiutDnxPRLk5DLUFj6Rw4pRBBURZY07GFr54NdV9mQg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -53,3 +58,4 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/misc.go b/misc.go index d877d642..73c347ec 100644 --- a/misc.go +++ b/misc.go @@ -11,7 +11,15 @@ import ( "strings" "unicode" + "github.com/jeandeaual/go-locale" "github.com/mattn/go-runewidth" + "golang.org/x/text/collate" + "golang.org/x/text/language" +) + +const ( + localeStrDisable = "" // disable locale ordering for this locale value + localeStrSys = "*" // replace this locale value with locale value read from environment ) func isRoot(name string) bool { return filepath.Dir(name) == name } @@ -338,6 +346,84 @@ func max(a, b int) int { return b } +// This function parses given locale string into language tag value. Passing empty +// string as locale means reading locale value from environment. +func getLocaleTag(localeStr string) (language.Tag, error) { + localeTag := language.Und + var err error + + if localeStr == localeStrSys { + // read environment locale + localeStr, err = locale.GetLocale() + if err != nil { + return localeTag, err + } + } + + localeTag, err = language.Parse(localeStr) + if err != nil { + return localeTag, fmt.Errorf("invalid locale %q: %s", localeStr, err) + } + + return localeTag, nil +} + +// This function creates new collator for given locale. Passing empty string as +// as locale means reading locale value from environment. +func makeCollator(localeStr string) (*collate.Collator, error) { + if localeStr == localeStrDisable { + return nil, fmt.Errorf("locale suppose to be disabled with given string") + } + + localeTag, err := getLocaleTag(localeStr) + if err != nil { + return nil, err + } + + collator := collate.New(localeTag) + + return collator, nil +} + +// This function works like `naturalLess`, except it use locale order when +// comparing non-digit parts of names. +func localeNaturalLess(collator *collate.Collator, s1, s2 string) bool { + lo1, lo2, hi1, hi2 := 0, 0, 0, 0 + for { + if hi1 >= len(s1) { + return hi2 != len(s2) + } + + if hi2 >= len(s2) { + return false + } + + isDigit1 := isDigit(s1[hi1]) + isDigit2 := isDigit(s2[hi2]) + + for lo1 = hi1; hi1 < len(s1) && isDigit(s1[hi1]) == isDigit1; hi1++ { + } + + for lo2 = hi2; hi2 < len(s2) && isDigit(s2[hi2]) == isDigit2; hi2++ { + } + + if s1[lo1:hi1] == s2[lo2:hi2] { + continue + } + + if isDigit1 && isDigit2 { + num1, err1 := strconv.Atoi(s1[lo1:hi1]) + num2, err2 := strconv.Atoi(s2[lo2:hi2]) + + if err1 == nil && err2 == nil { + return num1 < num2 + } + } + + return collator.CompareString(s1[lo1:hi1], s2[lo2:hi2]) < 0 + } +} + // We don't need no generic code // We don't need no type control // No dark templates in compiler diff --git a/misc_test.go b/misc_test.go index f4095603..e7c9a042 100644 --- a/misc_test.go +++ b/misc_test.go @@ -335,3 +335,52 @@ func TestGetFileExtension(t *testing.T) { }) } } + +func TestLocaleNaturalLess(t *testing.T) { + tests := []struct { + s1 string + s2 string + exp bool + }{ + // preserving behavior of `naturalLess` + {"foo", "bar", false}, + {"bar", "baz", true}, + {"foo", "123", false}, + {"foo1", "foobar", true}, + {"foo1", "foo10", true}, + {"foo2", "foo10", true}, + {"foo1", "foo10bar", true}, + {"foo2", "foo10bar", true}, + {"foo1bar", "foo10bar", true}, + {"foo2bar", "foo10bar", true}, + {"foo1bar", "foo10", true}, + {"foo2bar", "foo10", true}, + + // locale sort + {"你好", "他好", true}, // \u4F60\u597D, \u4ED6\u597D + {"到这", "到那", false}, // \u5230\u8FD9, \u5230\u90A3 + {"你说", "什么", true}, // \u4f60\u8bf4, \u4ec0\u4e48 + {"你好", "World", false}, // \u4F60\u597D, \u57\u6f\u72\u6c\u64 + {"甲1", "甲乙", true}, + {"甲1", "甲10", true}, + {"甲2", "甲10", true}, + {"甲1", "甲10乙", true}, + {"甲2", "甲10乙", true}, + {"甲1乙", "甲10乙", true}, + {"甲2乙", "甲10乙", true}, + {"甲1乙", "甲10", true}, + {"甲2乙", "甲10", true}, + } + + localeStr := "zh-CN" + collator, err := makeCollator(localeStr) + if err != nil { + t.Fatalf("failed to create collator for %q: %s", localeStr, err) + } + + for _, test := range tests { + if got := localeNaturalLess(collator, test.s1, test.s2); got != test.exp { + t.Errorf("at input '%s' and '%s' expected '%t' but got '%t'", test.s1, test.s2, test.exp, got) + } + } +} diff --git a/nav.go b/nav.go index 503731cb..577be4cb 100644 --- a/nav.go +++ b/nav.go @@ -171,6 +171,7 @@ type dir struct { filter []string // last filter for this directory ignorecase bool // ignorecase value from last sort ignoredia bool // ignoredia value from last sort + locale string // locale value from last sort noPerm bool // whether lf has no permission to open the directory lines []string // lines of text to display if directory previews are enabled } @@ -211,6 +212,7 @@ func (dir *dir) sort() { dir.dironly = getDirOnly(dir.path) dir.hidden = getHidden(dir.path) dir.reverse = getReverse(dir.path) + dir.locale = getLocale(dir.path) dir.hiddenfiles = gOpts.hiddenfiles dir.ignorecase = gOpts.ignorecase dir.ignoredia = gOpts.ignoredia @@ -221,23 +223,58 @@ func (dir *dir) sort() { // of equivalent elements will be reversed switch dir.sortby { case naturalSort: - sort.SliceStable(dir.files, func(i, j int) bool { + fallback := func(i, j int) bool { s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) if !dir.reverse { return naturalLess(s1, s2) } else { return naturalLess(s2, s1) } - }) + } + + if dir.locale != localeStrDisable { + if collator, err := makeCollator(dir.locale); err == nil { + sort.SliceStable(dir.files, func(i, j int) bool { + s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) + if !dir.reverse { + return localeNaturalLess(collator, s1, s2) + } else { + return localeNaturalLess(collator, s2, s1) + } + }) + } else { + sort.SliceStable(dir.files, fallback) + } + } else { + sort.SliceStable(dir.files, fallback) + } case nameSort: - sort.SliceStable(dir.files, func(i, j int) bool { + fallbackSort := func(i, j int) bool { s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) if !dir.reverse { return s1 < s2 } else { return s2 < s1 } - }) + } + + if dir.locale != localeStrDisable { + if collator, err := makeCollator(dir.locale); err == nil { + sort.SliceStable(dir.files, func(i, j int) bool { + s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) + result := collator.CompareString(s1, s2) + if !dir.reverse { + return result < 0 + } else { + return result > 0 + } + }) + } else { + sort.SliceStable(dir.files, fallbackSort) + } + } else { + sort.SliceStable(dir.files, fallbackSort) + } case sizeSort: sort.SliceStable(dir.files, func(i, j int) bool { if !dir.reverse { @@ -463,6 +500,7 @@ func (nav *nav) loadDirInternal(path string) *dir { dironly: getDirOnly(path), hidden: getHidden(path), reverse: getReverse(path), + locale: getLocale(path), hiddenfiles: gOpts.hiddenfiles, ignorecase: gOpts.ignorecase, ignoredia: gOpts.ignoredia, @@ -527,6 +565,7 @@ func (nav *nav) checkDir(dir *dir) { dir.dironly != getDirOnly(dir.path) || dir.hidden != getHidden(dir.path) || dir.reverse != getReverse(dir.path) || + dir.locale != getLocale(dir.path) || !reflect.DeepEqual(dir.hiddenfiles, gOpts.hiddenfiles) || dir.ignorecase != gOpts.ignorecase || dir.ignoredia != gOpts.ignoredia: diff --git a/opts.go b/opts.go index 2a7fdb96..3431a338 100644 --- a/opts.go +++ b/opts.go @@ -53,6 +53,7 @@ var gOpts struct { ignoredia bool incfilter bool incsearch bool + locale string mouse bool number bool preview bool @@ -111,6 +112,7 @@ var gLocalOpts struct { hiddens map[string]bool reverses map[string]bool infos map[string][]string + locales map[string]string } func localOptPaths(path string) []string { @@ -176,6 +178,15 @@ func getSortBy(path string) sortMethod { return gOpts.sortby } +func getLocale(path string) string { + for _, key := range localOptPaths(path) { + if val, ok := gLocalOpts.locales[key]; ok { + return val + } + } + return gOpts.locale +} + func init() { gOpts.anchorfind = true gOpts.autoquit = false @@ -200,6 +211,7 @@ func init() { gOpts.ignoredia = true gOpts.incfilter = false gOpts.incsearch = false + gOpts.locale = localeStrDisable gOpts.mouse = false gOpts.number = false gOpts.preview = true @@ -370,6 +382,7 @@ func init() { gLocalOpts.hiddens = make(map[string]bool) gLocalOpts.reverses = make(map[string]bool) gLocalOpts.infos = make(map[string][]string) + gLocalOpts.locales = make(map[string]string) setDefaults() } From 716236d1c58f6b6243172f2ded4b9745f5c6f11c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=86=E7=A6=BB?= Date: Fri, 25 Oct 2024 21:57:38 +0800 Subject: [PATCH 2/5] Switch go-locale module; Use collate option for natural sorting --- go.mod | 4 ++-- go.sum | 11 ++++------- misc.go | 50 ++++---------------------------------------------- misc_test.go | 6 ++++-- nav.go | 8 +++++--- 5 files changed, 19 insertions(+), 60 deletions(-) diff --git a/go.mod b/go.mod index f0800d5a..7a174906 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module github.com/gokcehan/lf go 1.18 require ( + github.com/Xuanwo/go-locale v1.1.2 github.com/djherbis/times v1.6.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gdamore/tcell/v2 v2.7.4 - github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 github.com/mattn/go-runewidth v0.0.16 golang.org/x/sys v0.26.0 golang.org/x/term v0.25.0 - golang.org/x/text v0.14.0 + golang.org/x/text v0.18.0 ) require ( diff --git a/go.sum b/go.sum index 040316b0..8dfa5906 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/Xuanwo/go-locale v1.1.2 h1:6H+olvrQcyVOZ+GAC2rXu4armacTT4ZrFCA0mB24XVo= +github.com/Xuanwo/go-locale v1.1.2/go.mod h1:1JBER4QV7Ji39GJ4AvVlfvqmTUqopzxQxdg2mXYOw94= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -7,18 +8,14 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= -github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 h1:Po+wkNdMmN+Zj1tDsJQy7mJlPlwGNQd9JZoPjObagf8= -github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49/go.mod h1:YiutDnxPRLk5DLUFj6Rw4pRBBURZY07GFr54NdV9mQg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -51,11 +48,11 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/misc.go b/misc.go index 73c347ec..175f80ee 100644 --- a/misc.go +++ b/misc.go @@ -11,7 +11,7 @@ import ( "strings" "unicode" - "github.com/jeandeaual/go-locale" + "github.com/Xuanwo/go-locale" "github.com/mattn/go-runewidth" "golang.org/x/text/collate" "golang.org/x/text/language" @@ -354,10 +354,7 @@ func getLocaleTag(localeStr string) (language.Tag, error) { if localeStr == localeStrSys { // read environment locale - localeStr, err = locale.GetLocale() - if err != nil { - return localeTag, err - } + return locale.Detect() } localeTag, err = language.Parse(localeStr) @@ -370,7 +367,7 @@ func getLocaleTag(localeStr string) (language.Tag, error) { // This function creates new collator for given locale. Passing empty string as // as locale means reading locale value from environment. -func makeCollator(localeStr string) (*collate.Collator, error) { +func makeCollator(localeStr string, o ...collate.Option) (*collate.Collator, error) { if localeStr == localeStrDisable { return nil, fmt.Errorf("locale suppose to be disabled with given string") } @@ -380,50 +377,11 @@ func makeCollator(localeStr string) (*collate.Collator, error) { return nil, err } - collator := collate.New(localeTag) + collator := collate.New(localeTag, o...) return collator, nil } -// This function works like `naturalLess`, except it use locale order when -// comparing non-digit parts of names. -func localeNaturalLess(collator *collate.Collator, s1, s2 string) bool { - lo1, lo2, hi1, hi2 := 0, 0, 0, 0 - for { - if hi1 >= len(s1) { - return hi2 != len(s2) - } - - if hi2 >= len(s2) { - return false - } - - isDigit1 := isDigit(s1[hi1]) - isDigit2 := isDigit(s2[hi2]) - - for lo1 = hi1; hi1 < len(s1) && isDigit(s1[hi1]) == isDigit1; hi1++ { - } - - for lo2 = hi2; hi2 < len(s2) && isDigit(s2[hi2]) == isDigit2; hi2++ { - } - - if s1[lo1:hi1] == s2[lo2:hi2] { - continue - } - - if isDigit1 && isDigit2 { - num1, err1 := strconv.Atoi(s1[lo1:hi1]) - num2, err2 := strconv.Atoi(s2[lo2:hi2]) - - if err1 == nil && err2 == nil { - return num1 < num2 - } - } - - return collator.CompareString(s1[lo1:hi1], s2[lo2:hi2]) < 0 - } -} - // We don't need no generic code // We don't need no type control // No dark templates in compiler diff --git a/misc_test.go b/misc_test.go index e7c9a042..41d6eb96 100644 --- a/misc_test.go +++ b/misc_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" "time" + + "golang.org/x/text/collate" ) func TestIsRoot(t *testing.T) { @@ -373,13 +375,13 @@ func TestLocaleNaturalLess(t *testing.T) { } localeStr := "zh-CN" - collator, err := makeCollator(localeStr) + collator, err := makeCollator(localeStr, collate.Numeric) if err != nil { t.Fatalf("failed to create collator for %q: %s", localeStr, err) } for _, test := range tests { - if got := localeNaturalLess(collator, test.s1, test.s2); got != test.exp { + if got := collator.CompareString(test.s1, test.s2) < 0; got != test.exp { t.Errorf("at input '%s' and '%s' expected '%t' but got '%t'", test.s1, test.s2, test.exp, got) } } diff --git a/nav.go b/nav.go index 577be4cb..9cfed0a2 100644 --- a/nav.go +++ b/nav.go @@ -16,6 +16,7 @@ import ( "time" "github.com/djherbis/times" + "golang.org/x/text/collate" ) type linkState byte @@ -233,13 +234,14 @@ func (dir *dir) sort() { } if dir.locale != localeStrDisable { - if collator, err := makeCollator(dir.locale); err == nil { + if collator, err := makeCollator(dir.locale, collate.Numeric); err == nil { sort.SliceStable(dir.files, func(i, j int) bool { s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) + result := collator.CompareString(s1, s2) if !dir.reverse { - return localeNaturalLess(collator, s1, s2) + return result < 0 } else { - return localeNaturalLess(collator, s2, s1) + return result > 0 } }) } else { From db5bee92c920708e56ef082b627b58fb9eb1c924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=86=E7=A6=BB?= Date: Sat, 26 Oct 2024 13:10:16 +0800 Subject: [PATCH 3/5] Doc and error message improvement --- doc.md | 6 +++--- misc.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc.md b/doc.md index fa95ccba..9049adda 100644 --- a/doc.md +++ b/doc.md @@ -826,11 +826,11 @@ Format string of the file time shown in the info column when it doesn't match th ## locale (string) (default `''`) -An IETF BCP 47 language tag for specifying locale used when when using sort type +An IETF BCP 47 language tag (e.g. `zh-CN`) for specifying the locale used when using sort type `natural` and `name`. -Empty string means disable locale ordering, and a special value `'*'` is used -to indicate reading locale setting from system environment. +An empty string means disable locale ordering, and the special value `'*'` is used +to indicate reading the locale setting from the system environment. ## mouse (bool) (default false) diff --git a/misc.go b/misc.go index 175f80ee..63676ad7 100644 --- a/misc.go +++ b/misc.go @@ -369,7 +369,7 @@ func getLocaleTag(localeStr string) (language.Tag, error) { // as locale means reading locale value from environment. func makeCollator(localeStr string, o ...collate.Option) (*collate.Collator, error) { if localeStr == localeStrDisable { - return nil, fmt.Errorf("locale suppose to be disabled with given string") + return nil, fmt.Errorf("locale is disabled") } localeTag, err := getLocaleTag(localeStr) From d6fc3ce629b6cfa9bc42db0ac2582083480806bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=86=E7=A6=BB?= Date: Sat, 26 Oct 2024 13:26:00 +0800 Subject: [PATCH 4/5] Simplify predicated checking of localized sorting. --- misc.go | 14 +++++----- nav.go | 84 +++++++++++++++++++++++++-------------------------------- 2 files changed, 42 insertions(+), 56 deletions(-) diff --git a/misc.go b/misc.go index 63676ad7..0908a759 100644 --- a/misc.go +++ b/misc.go @@ -349,15 +349,12 @@ func max(a, b int) int { // This function parses given locale string into language tag value. Passing empty // string as locale means reading locale value from environment. func getLocaleTag(localeStr string) (language.Tag, error) { - localeTag := language.Und - var err error - if localeStr == localeStrSys { // read environment locale return locale.Detect() } - localeTag, err = language.Parse(localeStr) + localeTag, err := language.Parse(localeStr) if err != nil { return localeTag, fmt.Errorf("invalid locale %q: %s", localeStr, err) } @@ -367,7 +364,10 @@ func getLocaleTag(localeStr string) (language.Tag, error) { // This function creates new collator for given locale. Passing empty string as // as locale means reading locale value from environment. -func makeCollator(localeStr string, o ...collate.Option) (*collate.Collator, error) { +// +// *Note*: this function returns error when given `localeStr` has value `localeStrDisable` +// or is an invalid locale tag. +func makeCollator(localeStr string, opts ...collate.Option) (*collate.Collator, error) { if localeStr == localeStrDisable { return nil, fmt.Errorf("locale is disabled") } @@ -377,9 +377,7 @@ func makeCollator(localeStr string, o ...collate.Option) (*collate.Collator, err return nil, err } - collator := collate.New(localeTag, o...) - - return collator, nil + return collate.New(localeTag, opts...), nil } // We don't need no generic code diff --git a/nav.go b/nav.go index 9cfed0a2..940de91b 100644 --- a/nav.go +++ b/nav.go @@ -224,58 +224,46 @@ func (dir *dir) sort() { // of equivalent elements will be reversed switch dir.sortby { case naturalSort: - fallback := func(i, j int) bool { - s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) - if !dir.reverse { - return naturalLess(s1, s2) - } else { - return naturalLess(s2, s1) - } - } - - if dir.locale != localeStrDisable { - if collator, err := makeCollator(dir.locale, collate.Numeric); err == nil { - sort.SliceStable(dir.files, func(i, j int) bool { - s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) - result := collator.CompareString(s1, s2) - if !dir.reverse { - return result < 0 - } else { - return result > 0 - } - }) - } else { - sort.SliceStable(dir.files, fallback) - } + if collator, err := makeCollator(dir.locale, collate.Numeric); err == nil { + sort.SliceStable(dir.files, func(i, j int) bool { + s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) + result := collator.CompareString(s1, s2) + if !dir.reverse { + return result < 0 + } else { + return result > 0 + } + }) } else { - sort.SliceStable(dir.files, fallback) + sort.SliceStable(dir.files, func(i, j int) bool { + s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) + if !dir.reverse { + return naturalLess(s1, s2) + } else { + return naturalLess(s2, s1) + } + }) } case nameSort: - fallbackSort := func(i, j int) bool { - s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) - if !dir.reverse { - return s1 < s2 - } else { - return s2 < s1 - } - } - - if dir.locale != localeStrDisable { - if collator, err := makeCollator(dir.locale); err == nil { - sort.SliceStable(dir.files, func(i, j int) bool { - s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) - result := collator.CompareString(s1, s2) - if !dir.reverse { - return result < 0 - } else { - return result > 0 - } - }) - } else { - sort.SliceStable(dir.files, fallbackSort) - } + if collator, err := makeCollator(dir.locale); err == nil { + sort.SliceStable(dir.files, func(i, j int) bool { + s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) + result := collator.CompareString(s1, s2) + if !dir.reverse { + return result < 0 + } else { + return result > 0 + } + }) } else { - sort.SliceStable(dir.files, fallbackSort) + sort.SliceStable(dir.files, func(i, j int) bool { + s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia) + if !dir.reverse { + return s1 < s2 + } else { + return s2 < s1 + } + }) } case sizeSort: sort.SliceStable(dir.files, func(i, j int) bool { From 79458094de6c482948e0426624f2285d1e7d4c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=86=E7=A6=BB?= Date: Sat, 26 Oct 2024 16:39:27 +0800 Subject: [PATCH 5/5] Improve error handling of `set locale` command --- eval.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/eval.go b/eval.go index 340ec4a9..b755d016 100644 --- a/eval.go +++ b/eval.go @@ -271,16 +271,15 @@ func (e *setExpr) eval(app *app, args []string) { gOpts.info = toks case "locale": localeStr := e.val - if gOpts.locale != localeStr { - if localeStr != localeStrDisable { - _, err = getLocaleTag(localeStr) - } - if err == nil { - gOpts.locale = localeStr - app.nav.sort() - app.ui.sort() + if localeStr != localeStrDisable { + if _, err = getLocaleTag(localeStr); err != nil { + app.ui.echoerrf("locale: %s", err.Error()) + return } } + gOpts.locale = localeStr + app.nav.sort() + app.ui.sort() case "rulerfmt": gOpts.rulerfmt = e.val case "preserve": @@ -515,13 +514,14 @@ func (e *setLocalExpr) eval(app *app, args []string) { case "locale": localeStr := e.val if localeStr != localeStrDisable { - _, err = getLocaleTag(localeStr) - } - if err == nil { - gLocalOpts.locales[e.path] = localeStr - app.nav.sort() - app.ui.sort() + if _, err = getLocaleTag(localeStr); err != nil { + app.ui.echoerrf("locale: %s", err.Error()) + return + } } + gLocalOpts.locales[e.path] = localeStr + app.nav.sort() + app.ui.sort() default: err = fmt.Errorf("unknown option: %s", e.opt) }