From c127c54937c7d3ede98635fa196358791e117074 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Fri, 1 Aug 2025 19:28:23 -0500 Subject: [PATCH] sort text for completions some cursed heuristics for completions, because nvim-cmp kept getting them wrong. nvim-cmp actually prioritizes kind over sort text, so it will still get it wrong, but I'm hoping that having a correct sort-text helps other editors. --- src/fennel-ls/formatter.fnl | 34 ++++++++++++++++++++++++--------- src/fennel-ls/handlers.fnl | 2 +- test/completion.fnl | 4 ++-- test/misc.fnl | 38 +++++++++++++++++++++++++++++++++++-- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/fennel-ls/formatter.fnl b/src/fennel-ls/formatter.fnl index 926e730..b5d13d0 100644 --- a/src/fennel-ls/formatter.fnl +++ b/src/fennel-ls/formatter.fnl @@ -188,17 +188,33 @@ fntype is one of fn or λ or lambda" :Snippet 15 :Color 16 :File 17 :Reference 18 :Folder 19 :EnumMember 20 :Constant 21 :Struct 22 :Event 23 :Operator 24 :TypeParameter 25}) +(fn int-to-str [int] + (string.char (math.min 126 (+ 32 int)))) + +(fn sort-text [name kind] + (let [parts (-> (name:gsub "[.:]+$" #($:gsub "." "x")) + (: :gmatch "[^.:]+")) + part-scores (icollect [part parts] + (int-to-str (length part))) + count (length part-scores) + kind (if (= kind 1) 100 kind)] + (table.insert part-scores (math.max 1 count) (int-to-str kind)) + (.. (int-to-str count) (table.concat part-scores)))) + (λ completion-item-format [server name definition range ?kind] "Makes a completion item" - {:label name - :documentation (when (not server.can-do-good-completions?) (hover-format server name definition)) - :textEdit (when (not server.can-do-good-completions?) {:newText name : range}) - :kind (or (?. kinds ?kind) - (if (name:find ".:") kinds.Method) - (case (navigate.getmetadata server definition) - metadata (?. kinds metadata.fls/itemKind)) - kinds.Value)}) + (let [kind (or (?. kinds ?kind) + (if (name:find ".:") kinds.Method) + (case (navigate.getmetadata server definition) + metadata (?. kinds metadata.fls/itemKind)) + (if (name:find "%.") kinds.Field kinds.Value))] + {:label name + :documentation (when (not server.can-do-good-completions?) (hover-format server name definition)) + :sortText (sort-text name kind) + :textEdit (when (not server.can-do-good-completions?) {:newText name : range}) + : kind})) {: signature-help-format : hover-format - : completion-item-format} + : completion-item-format + : sort-text} diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index 6cb5be5..e818367 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -31,7 +31,7 @@ Every time the client sends a message, it gets handled by a function in the corr ;; The LSP spec claims that "characters that make up identifiers don't need to be listed here" ;; > https://github.com/microsoft/language-server-protocol/blob/4a4ff53db00d8c5e57630ff364dedf1918cbb612/_specifications/lsp/3.18/language/completion.md?plain=1#L205 ;; but many editors don't consider these to be identifier characters, even though they are - "." "!" "^" "$" "%" "&" "*" "+" "/" "<" "=" ">" "?"] + "." ":" "!" "^" "$" "%" "&" "*" "+" "/" "<" "=" ">" "?"] :completionItem {:labelDetailsSupport false}} :hoverProvider {:workDoneProgress false} :signatureHelpProvider {:workDoneProgress false diff --git a/test/completion.fnl b/test/completion.fnl index 58fa148..b81e6fc 100644 --- a/test/completion.fnl +++ b/test/completion.fnl @@ -157,8 +157,8 @@ (fn test-field [] (check "(let [my-table {:foo 10 :bar 20}]\n my-table.|)))" - [{:label :my-table.foo :kind kinds.Value} - {:label :my-table.bar :kind kinds.Value}] + [{:label :my-table.foo :kind kinds.Field} + {:label :my-table.bar :kind kinds.Field}] []) nil) diff --git a/test/misc.fnl b/test/misc.fnl index e036680..90fbefc 100644 --- a/test/misc.fnl +++ b/test/misc.fnl @@ -3,7 +3,8 @@ (local {: create-client} (require :test.utils)) (local analyzer (require :fennel-ls.analyzer)) -(local utils (require :fennel-ls.utils)) +(local utils (require :fennel-ls.utils)) +(local {: sort-text} (require :fennel-ls.formatter)) (fn test-multi-sym-split [] @@ -64,7 +65,40 @@ (faith.= "a/b/c/d" (utils.path-join "a/b" "c/d")) nil) + +(fn test-sort-text [] + (fn assert< [a ak b bk] + (faith.= true (< (sort-text a ak) (sort-text b bk)) + (.. "expected (< (sort-text " (fennel.view a) " " ak ") " + "(sort-text " (fennel.view b) " " bk "))"))) + (fn assert= [a ak b bk] + (faith.= true (= (sort-text a ak) (sort-text b bk)) + (.. "expected (= (sort-text " (fennel.view a) " " ak ") " + "(sort-text " (fennel.view b) " " bk "))"))) + + ;; these are brittle so remove them when sort text requirements change + (assert< "aaabbb" 2 "a.b" 2) + (assert< "a.bb" 2 "aa.b" 2) + (assert< "x.y.zzz" 2 "x.yy.z" 2) + (assert< "short.a" 2 "short.bb" 2) + (assert< "aaa.a" 2 "bbbb.a" 2) + (assert< "aa" 2 "a" 3) + (assert< "a.aa" 2 "a.a" 3) + (assert< "a.a" 3 "aa.a" 2) + (assert< "a" 20 "a" 1) + (assert< "" 2 "a" 2) + (assert= "a.b:c" 1 "a.b.c" 1) + + ;; Operators should be the same, regardless of if .'s are in them in non separator spots + (assert= "?." 1 ".." 1) + (assert= "." 1 "+" 1) + + ;; absolutely please stop recommending string methods as more important than the strings themselves + (faith.< (sort-text "_VERSION" 2) (sort-text "_VERSION:gsub" 100)) + nil) + {: test-multi-sym-split : test-find-symbol : test-failure - : test-path-join} + : test-path-join + : test-sort-text}