summaryrefslogtreecommitdiff
path: root/commands
diff options
context:
space:
mode:
authorkt programs <ktprograms@gmail.com>2022-03-06 10:58:07 +0800
committerRobin Jarry <robin@jarry.cc>2022-03-07 10:18:50 +0100
commitcc172970a079bb78847f2276db8bfae375cda185 (patch)
tree143f9f049d7faf01e6ab1b07aac5017c516efbb2 /commands
parent55ae3d2cab8489609a1b11c169c28306730a71ea (diff)
downloadaerc-cc172970a079bb78847f2276db8bfae375cda185.zip
commands: implement fuzzy completion for commands and options
Change the option to enable fuzzy completion to be fuzzy-complete, since it's no longer only used for folders Signed-off-by: Kt Programs <ktprograms@gmail.com> Acked-by: Koni Marti <koni.marti@gmail.com>
Diffstat (limited to 'commands')
-rw-r--r--commands/account/recover.go35
-rw-r--r--commands/account/sort.go15
-rw-r--r--commands/commands.go57
-rw-r--r--commands/compose/header.go2
-rw-r--r--commands/ct.go13
-rw-r--r--commands/msg/archive.go2
-rw-r--r--commands/util.go20
7 files changed, 76 insertions, 68 deletions
diff --git a/commands/account/recover.go b/commands/account/recover.go
index a167d50..855d984 100644
--- a/commands/account/recover.go
+++ b/commands/account/recover.go
@@ -6,8 +6,8 @@ import (
"io/ioutil"
"os"
"path/filepath"
- "strings"
+ "git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/widgets"
"git.sr.ht/~sircmpwn/getopt"
@@ -24,24 +24,43 @@ func (Recover) Aliases() []string {
}
func (Recover) Complete(aerc *widgets.Aerc, args []string) []string {
+ acct := aerc.SelectedAccount()
+ if acct == nil {
+ return make([]string, 0)
+ }
+
// file name of temp file is hard-coded in the NewComposer() function
files, err := filepath.Glob(
filepath.Join(os.TempDir(), "aerc-compose-*.eml"),
)
if err != nil {
- return []string{}
+ return make([]string, 0)
+ }
+ // if nothing is entered yet, return all files
+ if len(args) == 0 {
+ return files
}
- arg := strings.Join(args, " ")
- if arg != "" {
- for i, file := range files {
- files[i] = strings.Join([]string{arg, file}, " ")
+ if args[0] == "-" {
+ return []string{"-f"}
+ } else if args[0] == "-f" {
+ if len(args) == 1 {
+ for i, file := range files {
+ files[i] = args[0] + " " + file
+ }
+ return files
+ } else {
+ // only accepts one file to recover
+ return commands.FilterList(files, args[1], args[0]+" ", acct.UiConfig().FuzzyComplete)
}
+ } else {
+ // only accepts one file to recover
+ return commands.FilterList(files, args[0], "", acct.UiConfig().FuzzyComplete)
}
- return files
}
func (Recover) Execute(aerc *widgets.Aerc, args []string) error {
- if len(Recover{}.Complete(aerc, args)) == 0 {
+ // Complete() expects to be passed only the arguments, not including the command name
+ if len(Recover{}.Complete(aerc, args[1:])) == 0 {
return errors.New("No messages to recover.")
}
diff --git a/commands/account/sort.go b/commands/account/sort.go
index 89a5e38..15ecbc0 100644
--- a/commands/account/sort.go
+++ b/commands/account/sort.go
@@ -4,6 +4,7 @@ import (
"errors"
"strings"
+ "git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/lib/sort"
"git.sr.ht/~rjarry/aerc/widgets"
)
@@ -19,6 +20,11 @@ func (Sort) Aliases() []string {
}
func (Sort) Complete(aerc *widgets.Aerc, args []string) []string {
+ acct := aerc.SelectedAccount()
+ if acct == nil {
+ return make([]string, 0)
+ }
+
supportedCriteria := []string{
"arrival",
"cc",
@@ -35,7 +41,7 @@ func (Sort) Complete(aerc *widgets.Aerc, args []string) []string {
last := args[len(args)-1]
var completions []string
currentPrefix := strings.Join(args, " ") + " "
- // if there is a completed criteria then suggest all again or an option
+ // if there is a completed criteria or option then suggest all again
for _, criteria := range append(supportedCriteria, "-r") {
if criteria == last {
for _, criteria := range supportedCriteria {
@@ -54,11 +60,8 @@ func (Sort) Complete(aerc *widgets.Aerc, args []string) []string {
return []string{currentPrefix + "-r"}
}
// the last item is not complete
- for _, criteria := range supportedCriteria {
- if strings.HasPrefix(criteria, last) {
- completions = append(completions, currentPrefix+criteria)
- }
- }
+ completions = commands.FilterList(supportedCriteria, last, currentPrefix,
+ acct.UiConfig().FuzzyComplete)
return completions
}
diff --git a/commands/commands.go b/commands/commands.go
index 70a77b9..c23df7e 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -2,7 +2,6 @@ package commands
import (
"errors"
- "fmt"
"sort"
"strings"
"unicode"
@@ -74,12 +73,14 @@ func (cmds *Commands) GetCompletions(aerc *widgets.Aerc, cmd string) []string {
return nil
}
+ // nothing entered, list all commands
if len(args) == 0 {
names := cmds.Names()
sort.Strings(names)
return names
}
+ // complete options
if len(args) > 1 || cmd[len(cmd)-1] == ' ' {
if cmd, ok := cmds.dict()[args[0]]; ok {
var completions []string
@@ -101,13 +102,9 @@ func (cmds *Commands) GetCompletions(aerc *widgets.Aerc, cmd string) []string {
return nil
}
+ // complete available commands
names := cmds.Names()
- options := make([]string, 0)
- for _, name := range names {
- if strings.HasPrefix(name, args[0]) {
- options = append(options, name)
- }
- }
+ options := FilterList(names, args[0], "", aerc.SelectedAccount().UiConfig().FuzzyComplete)
if len(options) > 0 {
return options
@@ -116,35 +113,23 @@ func (cmds *Commands) GetCompletions(aerc *widgets.Aerc, cmd string) []string {
}
func GetFolders(aerc *widgets.Aerc, args []string) []string {
- out := make([]string, 0)
acct := aerc.SelectedAccount()
if acct == nil {
- return out
+ return make([]string, 0)
}
if len(args) == 0 {
return acct.Directories().List()
}
- for _, dir := range acct.Directories().List() {
- if foundInString(dir, args[0], acct.UiConfig().FuzzyFolderComplete) {
- out = append(out, dir)
- }
- }
- return out
+ return FilterList(acct.Directories().List(), args[0], "", acct.UiConfig().FuzzyComplete)
}
// CompletionFromList provides a convenience wrapper for commands to use in the
// Complete function. It simply matches the items provided in valid
-func CompletionFromList(valid []string, args []string) []string {
- out := make([]string, 0)
+func CompletionFromList(aerc *widgets.Aerc, valid []string, args []string) []string {
if len(args) == 0 {
return valid
}
- for _, v := range valid {
- if hasCaseSmartPrefix(v, args[0]) {
- out = append(out, v)
- }
- }
- return out
+ return FilterList(valid, args[0], "", aerc.SelectedAccount().UiConfig().FuzzyComplete)
}
func GetLabels(aerc *widgets.Aerc, args []string) []string {
@@ -172,27 +157,14 @@ func GetLabels(aerc *widgets.Aerc, args []string) []string {
}
trimmed := strings.TrimLeft(last, "+-")
- out := make([]string, 0)
- for _, label := range acct.Labels() {
- if hasCaseSmartPrefix(label, trimmed) {
- var prev string
- if len(others) > 0 {
- prev = others + " "
- }
- out = append(out, fmt.Sprintf("%v%v%v", prev, prefix, label))
- }
+ var prev string
+ if len(others) > 0 {
+ prev = others + " "
}
+ out := FilterList(acct.Labels(), trimmed, prev+prefix, acct.UiConfig().FuzzyComplete)
return out
}
-func foundInString(s, substring string, fuzzy bool) bool {
- if fuzzy {
- return caseInsensitiveContains(s, substring)
- } else {
- return hasCaseSmartPrefix(s, substring)
- }
-}
-
// hasCaseSmartPrefix checks whether s starts with prefix, using a case
// sensitive match if and only if prefix contains upper case letters.
func hasCaseSmartPrefix(s, prefix string) bool {
@@ -202,11 +174,6 @@ func hasCaseSmartPrefix(s, prefix string) bool {
return strings.HasPrefix(strings.ToLower(s), strings.ToLower(prefix))
}
-func caseInsensitiveContains(s, substr string) bool {
- s, substr = strings.ToUpper(s), strings.ToUpper(substr)
- return strings.Contains(s, substr)
-}
-
func hasUpper(s string) bool {
for _, r := range s {
if unicode.IsUpper(r) {
diff --git a/commands/compose/header.go b/commands/compose/header.go
index 5780aa8..949698e 100644
--- a/commands/compose/header.go
+++ b/commands/compose/header.go
@@ -32,7 +32,7 @@ func (Header) Aliases() []string {
}
func (Header) Complete(aerc *widgets.Aerc, args []string) []string {
- return commands.CompletionFromList(headers, args)
+ return commands.CompletionFromList(aerc, headers, args)
}
func (Header) Execute(aerc *widgets.Aerc, args []string) error {
diff --git a/commands/ct.go b/commands/ct.go
index 7764cab..f5f2cca 100644
--- a/commands/ct.go
+++ b/commands/ct.go
@@ -20,17 +20,16 @@ func (ChangeTab) Aliases() []string {
}
func (ChangeTab) Complete(aerc *widgets.Aerc, args []string) []string {
+ acct := aerc.SelectedAccount()
+ if acct == nil {
+ return make([]string, 0)
+ }
+
if len(args) == 0 {
return aerc.TabNames()
}
joinedArgs := strings.Join(args, " ")
- out := make([]string, 0)
- for _, tab := range aerc.TabNames() {
- if strings.HasPrefix(tab, joinedArgs) {
- out = append(out, tab)
- }
- }
- return out
+ return FilterList(aerc.TabNames(), joinedArgs, "", acct.UiConfig().FuzzyComplete)
}
func (ChangeTab) Execute(aerc *widgets.Aerc, args []string) error {
diff --git a/commands/msg/archive.go b/commands/msg/archive.go
index e73e42c..8f832e5 100644
--- a/commands/msg/archive.go
+++ b/commands/msg/archive.go
@@ -31,7 +31,7 @@ func (Archive) Aliases() []string {
func (Archive) Complete(aerc *widgets.Aerc, args []string) []string {
valid := []string{"flat", "year", "month"}
- return commands.CompletionFromList(valid, args)
+ return commands.CompletionFromList(aerc, valid, args)
}
func (Archive) Execute(aerc *widgets.Aerc, args []string) error {
diff --git a/commands/util.go b/commands/util.go
index f3f9bc8..92b851a 100644
--- a/commands/util.go
+++ b/commands/util.go
@@ -10,6 +10,8 @@ import (
"strings"
"time"
+ "github.com/lithammer/fuzzysearch/fuzzy"
+
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/widgets"
@@ -194,3 +196,21 @@ func MsgInfoFromUids(store *lib.MessageStore, uids []uint32) ([]*models.MessageI
}
return infos, nil
}
+
+// FilterList takes a list of valid completions and filters it, either
+// by case smart prefix, or by fuzzy matching, prepending "prefix" to each completion
+func FilterList(valid []string, search, prefix string, isFuzzy bool) []string {
+ out := make([]string, 0)
+ if isFuzzy {
+ for _, v := range fuzzy.RankFindFold(search, valid) {
+ out = append(out, prefix+v.Target)
+ }
+ } else {
+ for _, v := range valid {
+ if hasCaseSmartPrefix(v, search) {
+ out = append(out, prefix+v)
+ }
+ }
+ }
+ return out
+}