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.
This commit is contained in:
XeroOl 2025-08-01 19:28:23 -05:00
parent 1fb27f876b
commit c127c54937
4 changed files with 64 additions and 14 deletions

View File

@ -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}

View File

@ -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

View File

@ -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)

View File

@ -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}