Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to sort base on certain locale. #1818

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 (e.g. `zh-CN`) for specifying the locale used when using sort type
`natural` and `name`.

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)

Send mouse events as input.
Expand Down Expand Up @@ -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`.
joelim-work marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
22 changes: 22 additions & 0 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,17 @@ func (e *setExpr) eval(app *app, args []string) {
}
}
gOpts.info = toks
case "locale":
localeStr := e.val
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":
Expand Down Expand Up @@ -500,6 +511,17 @@ 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 {
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)
}
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ 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/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.18.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
)
5 changes: 4 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
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=
Expand Down Expand Up @@ -46,8 +48,9 @@ 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=
Expand Down
42 changes: 42 additions & 0 deletions misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import (
"strings"
"unicode"

"github.com/Xuanwo/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 }
Expand Down Expand Up @@ -338,6 +346,40 @@ 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) {
if localeStr == localeStrSys {
// read environment locale
return locale.Detect()
}

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.
//
// *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")
}

localeTag, err := getLocaleTag(localeStr)
if err != nil {
return nil, err
}

return collate.New(localeTag, opts...), nil
}

// We don't need no generic code
// We don't need no type control
// No dark templates in compiler
Expand Down
51 changes: 51 additions & 0 deletions misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"strings"
"testing"
"time"

"golang.org/x/text/collate"
)

func TestIsRoot(t *testing.T) {
Expand Down Expand Up @@ -335,3 +337,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, collate.Numeric)
if err != nil {
t.Fatalf("failed to create collator for %q: %s", localeStr, err)
}

for _, test := range tests {
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)
}
}
}
61 changes: 45 additions & 16 deletions nav.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"time"

"github.com/djherbis/times"
"golang.org/x/text/collate"
)

type linkState byte
Expand Down Expand Up @@ -171,6 +172,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
}
Expand Down Expand Up @@ -211,6 +213,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
Expand All @@ -221,23 +224,47 @@ func (dir *dir) sort() {
// of equivalent elements will be reversed
switch dir.sortby {
case naturalSort:
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)
}
})
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, 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:
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
}
})
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, 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 {
if !dir.reverse {
Expand Down Expand Up @@ -463,6 +490,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,
Expand Down Expand Up @@ -527,6 +555,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:
Expand Down
13 changes: 13 additions & 0 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var gOpts struct {
ignoredia bool
incfilter bool
incsearch bool
locale string
mouse bool
number bool
preview bool
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
}