Improved signature help and active argument detection.

This now uses a more correct algorithm to find the function call the
user is typing and also adds active argument detection.
This commit is contained in:
Michele Campeotto 2025-04-02 15:59:22 +02:00 committed by Phil Hagelberg
parent 96aef99728
commit f3d5984091
5 changed files with 191 additions and 39 deletions

View File

@ -39,7 +39,6 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
" "
(local {: sym? : list? : sequence? : varg?} (require :fennel)) (local {: sym? : list? : sequence? : varg?} (require :fennel))
(local {: special?} (require :fennel-ls.compiler))
(local {: get-ast-info &as utils} (require :fennel-ls.utils)) (local {: get-ast-info &as utils} (require :fennel-ls.utils))
(local files (require :fennel-ls.files)) (local files (require :fennel-ls.files))
(local docs (require :fennel-ls.docs)) (local docs (require :fennel-ls.docs))
@ -218,7 +217,7 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
_ (case (find-local-definition file name scope) _ (case (find-local-definition file name scope)
def (search-val server file def.definition (stack-add-keys! stack def.keys) opts)))))) def (search-val server file def.definition (stack-add-keys! stack def.keys) opts))))))
_past? [?ast byte] (λ past? [?ast byte]
;; check if a byte is past an ast object ;; check if a byte is past an ast object
(and (= (type ?ast) :table) (and (= (type ?ast) :table)
(get-ast-info ?ast :byteend) (get-ast-info ?ast :byteend)
@ -277,12 +276,37 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
(λ find-nearest-call [_server file byte] (λ find-nearest-call [_server file byte]
"Find the nearest call "Find the nearest call
returns the called symbol and the argument number position points to" returns the called symbol and the number of the argument closest to byte"
(let [(_ [[call] parent-call]) (find-symbol file.ast byte) (λ find-list [[call & parents]]
parent (?. parent-call 1)] (if (. file.calls call)
(if (and parent (special? parent)) call
(values parent -1) (if (next parents)
(values call -1)))) (find-list parents))))
(λ arg-index [call byte]
;; TODO: special handling for binding forms so we can point to the
;; individual arguments in an each or accumulate call.
;; Also need to split them up in formatter.fnl
(faccumulate [index nil
i (length call) 1 -1 &until index]
(if (contains? (. call i) byte)
; -2 because this is the 3rd element of the list, but
; the 2nd argument to the call, and LSP is 0-indexed
(- i 2)
(past? (. call i) byte)
; this means we are either at the end of the list or
; inserting between two arguments
(- i 1))))
(case-try (find-symbol file.ast byte)
(_symbol parents) (find-list parents)
[callee &as call] (values callee (arg-index call byte))
(catch _ nil)))
(λ find-definition [server file symbol ?byte]
(if (. file.definitions symbol)
(. file.definitions symbol)
(search-main server file symbol {:stop-early? false} {:byte ?byte})))
(λ find-nearest-definition [server file symbol ?byte] (λ find-nearest-definition [server file symbol ?byte]
(if (. file.definitions symbol) (if (. file.definitions symbol)
@ -292,6 +316,7 @@ returns the called symbol and the argument number position points to"
{: find-symbol {: find-symbol
: find-nearest-call : find-nearest-call
: find-nearest-definition : find-nearest-definition
: find-definition
: search-main : search-main
: search-name-and-scope : search-name-and-scope
:search-ast search-val} :search-ast search-val}

View File

@ -19,6 +19,11 @@ user code. Fennel-ls doesn't support user-code formatting as of now."
(.. ": " $2 $3))) (.. ": " $2 $3)))
_ (tostring arg))) _ (tostring arg)))
(fn render-arglist [?arglist]
(if ?arglist
(icollect [_ arg (ipairs ?arglist)]
{:label (render-arg arg)})))
(fn fn-signature-format [special name args] (fn fn-signature-format [special name args]
(let [args (case (type (?. args 1)) (let [args (case (type (?. args 1))
:table (icollect [_ v (ipairs args)] :table (icollect [_ v (ipairs args)]
@ -95,14 +100,14 @@ fntype is one of fn or λ or lambda"
{:fntype ?fntype :name ?name :arglist ?arglist :docstring ?docstring} {:fntype ?fntype :name ?name :arglist ?arglist :docstring ?docstring}
{:label (fn-signature-format ?fntype ?name ?arglist) {:label (fn-signature-format ?fntype ?name ?arglist)
:documentation ?docstring :documentation ?docstring
:parameters (if ?arglist :parameters (render-arglist ?arglist)}
(icollect [_ arg (ipairs ?arglist)]
{:label (render-arg arg)}))}
_ (case symbol _ (case symbol
{: binding :metadata {:fnl/arglist arglist :fnl/docstring docstring}} {: binding :metadata {:fnl/arglist arglist :fnl/docstring docstring}}
{:label (fn-signature-format :fn binding arglist) {:label (fn-signature-format :fn binding arglist)
:documentation docstring} :documentation docstring
_ {:label (.. "ERROR: don't know how to format " (tostring symbol)) :parameters (render-arglist arglist)}
_ {:label (.. "ERROR: don't know how to format "
(view symbol {:one-line? true :depth 3}))
:documentation (code-block :documentation (code-block
(view symbol {:depth 3}))}))) (view symbol {:depth 3}))})))

View File

@ -138,16 +138,18 @@ Every time the client sends a message, it gets handled by a function in the corr
(catch _ nil)))) (catch _ nil))))
(λ requests.textDocument/signatureHelp [server (λ requests.textDocument/signatureHelp [server
_send _send
{:textDocument {: uri} : position}] {:textDocument {: uri} : position}]
(let [file (files.get-by-uri server uri) (let [file (files.get-by-uri server uri)
byte (utils.position->byte file.text position server.position-encoding)] byte (utils.position->byte file.text position server.position-encoding)]
(case-try (analyzer.find-nearest-call server file byte) (case-try (analyzer.find-nearest-call server file byte)
(symbol active-parameter) (symbol active-parameter)
(analyzer.find-nearest-definition server file symbol) (analyzer.find-definition server file symbol)
{:indeterminate nil &as result} {:indeterminate nil &as definition}
(formatter.signature-help-format definition)
signature
(message.symbol->signature-help server file symbol (message.symbol->signature-help server file symbol
(formatter.signature-help-format result) signature
active-parameter) active-parameter)
(catch _ nil)))) (catch _ nil))))

View File

@ -91,9 +91,16 @@ LSP json objects."
:kind ?kind :kind ?kind
:edit {:changes {uri (diagnostic.quickfix)}}})) :edit {:changes {uri (diagnostic.quickfix)}}}))
(λ symbol->signature-help [_server _file _call signature _active-parameter] (λ symbol->signature-help [_server _file _call signature active-parameter]
{:signatures [signature] (let [params-count (length signature.parameters)
:activeSignature 0}) {:label last} (. signature.parameters params-count)]
{:signatures [signature]
:activeSignature 0 ; we only ever have one signature
:activeParameter (if (and (>= active-parameter params-count)
(= last "..."))
(- params-count 1)
(>= active-parameter 0)
active-parameter)}))
(λ multisym->range [server file ast n] (λ multisym->range [server file ast n]
(let [spl (utils.multi-sym-split ast) (let [spl (utils.multi-sym-split ast)

View File

@ -5,7 +5,9 @@
(λ check-signature [expected response] (λ check-signature [expected response]
(case response (case response
{:signatures [{:label signature}]} {:signatures [{:label signature}]}
(faith.= expected.signature signature) (do
(faith.= expected.signature signature)
(faith.= expected.activeParameter response.activeParameter))
;; fail ;; fail
_ (faith.is nil (.. "Invalid response: " (view response))))) _ (faith.is nil (.. "Invalid response: " (view response)))))
@ -15,62 +17,173 @@
(check-signature expected result))) (check-signature expected result)))
(fn test-fn-definition [] (fn test-fn-definition []
(check "(fn |)"
{:signature "(fn name? args docstring? ...)"
:activeParameter 0})
(check "(fn some-nam|)"
{:signature "(fn name? args docstring? ...)"
:activeParameter 0})
(check "(fn some-name [|]"
{:signature "(fn name? args docstring? ...)"
:activeParameter 1})
(check "(fn some-name [arg|])"
{:signature "(fn name? args docstring? ...)"
:activeParameter 1})
(check "(fn some-name [arg1 arg2]
\"docstring|\")"
{:signature "(fn name? args docstring? ...)"
:activeParameter 2})
(check "(fn some-name [arg1 arg2]
\"docstring\"
|"
{:signature "(fn name? args docstring? ...)"
:activeParameter 2}))
(fn test-local-function []
(check "(fn func [arg1 arg2] (print :hello)) (check "(fn func [arg1 arg2] (print :hello))
(func|)" (func|)"
{:signature "(func arg1 arg2)"}) {:signature "(func arg1 arg2)"
:activeParameter nil})
(check "(fn func [arg1 arg2] (print :hello)) (check "(fn func [arg1 arg2] (print :hello))
(func |)" (func |)"
{:signature "(func arg1 arg2)"}) {:signature "(func arg1 arg2)"
:activeParameter 0})
(check "(fn func [arg1 arg2] (print :hello)) (check "(fn func [arg1 arg2] (print :hello))
(func a1|)" (func a1|)"
{:signature "(func arg1 arg2)"}) {:signature "(func arg1 arg2)"
:activeParameter 0})
(check "(fn func [arg1 arg2] (print :hello)) (check "(fn func [arg1 arg2] (print :hello))
(func a|1 a2)" (func a|1 a2)"
{:signature "(func arg1 arg2)"}) {:signature "(func arg1 arg2)"
:activeParameter 0})
(check "(fn func [arg1 arg2] (print :hello)) (check "(fn func [arg1 arg2] (print :hello))
(func a1 a2|)" (func a1 a2|)"
{:signature "(func arg1 arg2)"}) {:signature "(func arg1 arg2)"
:activeParameter 1})
(check "(fn func [arg1 arg2] (print :hello)) (check "(fn func [arg1 arg2] (print :hello))
(func a1 a|2)" (func a1 a|2)"
{:signature "(func arg1 arg2)"})) {:signature "(func arg1 arg2)"
:activeParameter 1}))
(fn test-literals []
(check "(fn func [arg1 arg2] (print :hello))
(func arg 10|2)"
{:signature "(func arg1 arg2)"
:activeParameter 1})
(check "(fn func [arg1 arg2] (print :hello))
(func arg \"10|2\")"
{:signature "(func arg1 arg2)"
:activeParameter 1}))
(fn test-vararg []
(check "(or a b|)"
{:signature "(or a b ...)"
:activeParameter 1})
(check "(or a b c|)"
{:signature "(or a b ...)"
:activeParameter 2})
(check "(or a b c d e|)"
{:signature "(or a b ...)"
:activeParameter 2}))
(fn test-lua-builtin [] (fn test-lua-builtin []
(check "(error msg lvl|)" (check "(error msg lvl|)"
{:signature "(error message ?level)"})) {:signature "(error message ?level)"
:activeParameter 1})
(check "(error msg lvl extr|)"
{:signature "(error message ?level)"
:activeParameter 2}))
(fn test-multisym [] (fn test-multisym []
(check "(table.concat tbl s|)" (check "(table.concat tbl s|)"
{:signature "(table.concat list ?sep ?i ?j)"})) {:signature "(table.concat list ?sep ?i ?j)"
:activeParameter 1}))
(fn test-destructuring-arg [] (fn test-destructuring-arg []
(check "(fn dstr [{:field name} arg2] {}) (check "(fn dstr [{:field name} arg2] {})
(dstr |)" (dstr |)"
{:signature "(dstr {:field name} arg2)"}) {:signature "(dstr {:field name} arg2)"
:activeParameter 0})
(check "(fn dstr [{:field name} arg2] {}) (check "(fn dstr [{:field name} arg2] {})
(dstr arg1 ar|)" (dstr arg1 ar|)"
{:signature "(dstr {:field name} arg2)"})) {:signature "(dstr {:field name} arg2)"
:activeParameter 1})
(fn test-special [] (check "(fn dstr [{:field |} arg2] {})"
{:signature "(fn name? args docstring? ...)"
:activeParameter 1}))
(fn test-binding-form []
(check "(each |)" (check "(each |)"
{:signature "(each [key value (iterator)] ...)"}) {:signature "(each [key value (iterator)] ...)"
:activeParameter 0})
(check "(each [|])" (check "(each [|])"
{:signature "(each [key value (iterator)] ...)"}) {:signature "(each [key value (iterator)] ...)"
:activeParameter 0})
(check "(each [k val|])" (check "(each [k val|])"
{:signature "(each [key value (iterator)] ...)"}) {:signature "(each [key value (iterator)] ...)"
:activeParameter 0})
(check "(each [33|])" (check "(let [a 0]
{:signature "(each [key value (iterator)] ...)"})) (error |))"
{:signature "(error message ?level)"
:activeParameter 0})
(check "(let [a|] (error))"
{:signature "(let [name1 val1 ... nameN valN] ...)"
:activeParameter 0}))
(fn test-indirect-definition []
(check "(let [a error]
(a |))"
{:signature "(error message ?level)"
:activeParameter 0}))
(fn test-destructuring-binding []
(check "(let [(a b|) {}]"
{:signature "(let [name1 val1 ... nameN valN] ...)"
:activeParameter 0})
(check "(let [(a {:b |}) {}]"
{:signature "(let [name1 val1 ... nameN valN] ...)"
:activeParameter 0})
(check "(let [{:field |} {}]"
{:signature "(let [name1 val1 ... nameN valN] ...)"
:activeParameter 0})
(check "(let [{:field {: nested |"
{:signature "(let [name1 val1 ... nameN valN] ...)"
:activeParameter 0})
(check "(let [{:field {: nested &as |}} {}]"
{:signature "(let [name1 val1 ... nameN valN] ...)"
:activeParameter 0}))
{: test-fn-definition {: test-fn-definition
: test-local-function
: test-literals
: test-vararg
: test-lua-builtin : test-lua-builtin
: test-multisym : test-multisym
: test-destructuring-arg : test-destructuring-arg
: test-special} : test-binding-form
: test-indirect-definition
: test-destructuring-binding}