Implement basic signatureHelp feature.
This implements the simple form of the signatureHelp feature, which only displays the signature of the function being typed, without indication of the active argument. Active argument detection, while accounting for destructuring to support each and for special forms turned out to be more involved than expected and is left for a follow up patch.
This commit is contained in:
parent
3eb016a369
commit
627a02e2c0
@ -4,6 +4,7 @@
|
||||
|
||||
### Features
|
||||
|
||||
* Signature help support.
|
||||
* Provide human readable code actions titles.
|
||||
* Add --help and --version command line flags.
|
||||
* Support providing improved completion kinds to clients.
|
||||
|
||||
@ -39,12 +39,11 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
|
||||
"
|
||||
|
||||
(local {: sym? : list? : sequence? : varg?} (require :fennel))
|
||||
(local utils (require :fennel-ls.utils))
|
||||
(local {: special?} (require :fennel-ls.compiler))
|
||||
(local {: get-ast-info &as utils} (require :fennel-ls.utils))
|
||||
(local files (require :fennel-ls.files))
|
||||
(local docs (require :fennel-ls.docs))
|
||||
|
||||
(local get-ast-info utils.get-ast-info)
|
||||
|
||||
(var search-multival nil) ;; all of the search functions are mutually recursive
|
||||
|
||||
(λ stack-add-keys! [stack ?keys]
|
||||
@ -222,9 +221,8 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
|
||||
(λ _past? [?ast byte]
|
||||
;; check if a byte is past an ast object
|
||||
(and (= (type ?ast) :table)
|
||||
(get-ast-info ?ast :bytestart)
|
||||
(< byte (get-ast-info ?ast :bytestart))
|
||||
false))
|
||||
(get-ast-info ?ast :byteend)
|
||||
(< (get-ast-info ?ast :byteend) byte)))
|
||||
|
||||
(λ contains? [?ast byte]
|
||||
;; check if an ast contains a byte
|
||||
@ -276,12 +274,23 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
|
||||
(fcollect [i 1 (length parents)]
|
||||
(. parents (- (length parents) i -1)))))
|
||||
|
||||
(λ find-nearest-call [server file position]
|
||||
"Find the nearest call
|
||||
|
||||
returns the called symbol and the argument number position points to"
|
||||
(let [byte (utils.position->byte file.text position server.position-encoding)
|
||||
(_ [[call] [parent]]) (find-symbol file.ast byte)]
|
||||
(if (special? parent)
|
||||
(values parent -1)
|
||||
(values call -1))))
|
||||
|
||||
(λ find-nearest-definition [server file symbol ?byte]
|
||||
(if (. file.definitions symbol)
|
||||
(. file.definitions symbol)
|
||||
(search-main server file symbol {:stop-early? true} {:byte ?byte})))
|
||||
|
||||
{: find-symbol
|
||||
: find-nearest-call
|
||||
: find-nearest-definition
|
||||
: search-main
|
||||
: search-name-and-scope
|
||||
|
||||
@ -6,6 +6,7 @@ compiler's plugin hook callbacks. It stores lexical info about which
|
||||
identifiers are declared / referenced in which places."
|
||||
|
||||
(local {: sym? : list? : sequence? : table? : sym : view &as fennel} (require :fennel))
|
||||
(local {:scopes {:global {: specials}}} (require :fennel.compiler))
|
||||
(local docs (require :fennel-ls.docs))
|
||||
(local message (require :fennel-ls.message))
|
||||
(local searcher (require :fennel-ls.searcher))
|
||||
@ -13,6 +14,20 @@ identifiers are declared / referenced in which places."
|
||||
|
||||
(local nil* (sym :nil))
|
||||
|
||||
(fn special? [item]
|
||||
(and (sym? item)
|
||||
(. specials (tostring item))
|
||||
item))
|
||||
|
||||
(local ops {"+" 1 "-" 1 "*" 1 "/" 1 "//" 1 "%" 1 "^" 1 ">" 1 "<" 1 ">=" 1
|
||||
"<=" 1 "=" 1 "not=" 1 ".." 1 "." 1 "and" 1 "or" 1 "band" 1
|
||||
"bor" 1 "bxor" 1 "bnot" 1 "lshift" 1 "rshift" 1})
|
||||
|
||||
(fn op? [item]
|
||||
(and (sym? item)
|
||||
(. ops (tostring item))
|
||||
item))
|
||||
|
||||
(fn scope? [candidate]
|
||||
;; just checking a couple of the fields
|
||||
(and
|
||||
@ -426,4 +441,6 @@ identifiers are declared / referenced in which places."
|
||||
(set file.macro-refs macro-refs)
|
||||
(set file.macro-calls macro-calls))))
|
||||
|
||||
{: compile}
|
||||
{: special?
|
||||
: op?
|
||||
: compile}
|
||||
|
||||
@ -9,6 +9,22 @@ user code. Fennel-ls doesn't support user-code formatting as of now."
|
||||
(λ code-block [str]
|
||||
(.. "```fnl\n" str "\n```"))
|
||||
|
||||
(λ render-arg [arg]
|
||||
(case (type arg)
|
||||
:table (view arg {:one-line? true
|
||||
:prefer-colon? true})
|
||||
_ (tostring arg)))
|
||||
|
||||
(λ fn-signature-format [name args]
|
||||
(let [args (case (type (?. args 1))
|
||||
:table (icollect [_ v (ipairs args)]
|
||||
(render-arg v))
|
||||
_ args)]
|
||||
(.. "("
|
||||
(tostring name) " "
|
||||
(table.concat args " ")
|
||||
")")))
|
||||
|
||||
(fn fn-format [special name args docstring]
|
||||
(.. (code-block (.. "("
|
||||
(tostring special)
|
||||
@ -78,12 +94,32 @@ fntype is one of fn or λ or lambda"
|
||||
(= (type arglist) :table))
|
||||
{: fntype : arglist}))
|
||||
|
||||
(λ signature-help-format [symbol]
|
||||
"Return a signatureHelp lsp object
|
||||
|
||||
symbol can be an actual ast symbol or a binding object from a docset"
|
||||
(case (analyze-fn symbol.definition)
|
||||
{:name ?name :arglist ?arglist :docstring ?docstring}
|
||||
{:label (fn-signature-format ?name ?arglist)
|
||||
:documentation ?docstring
|
||||
:parameters (if ?arglist
|
||||
(icollect [_ arg (ipairs ?arglist)]
|
||||
{:label (render-arg arg)}))}
|
||||
_ (case symbol
|
||||
{: binding :metadata {:fnl/arglist arglist :fnl/docstring docstring}}
|
||||
{:label (fn-signature-format binding arglist)
|
||||
:documentation docstring}
|
||||
_ {:label (.. "ERROR: don't know how to format " (tostring symbol))
|
||||
:documentation (code-block
|
||||
(view symbol {:depth 3}))})))
|
||||
|
||||
(λ hover-format [result]
|
||||
"Format code that will appear when the user hovers over a symbol"
|
||||
{:kind "markdown"
|
||||
:value
|
||||
(case (analyze-fn result.definition)
|
||||
{:fntype ?fntype :name ?name :arglist ?arglist :docstring ?docstring} (fn-format ?fntype ?name ?arglist ?docstring)
|
||||
{:fntype ?fntype :name ?name :arglist ?arglist :docstring ?docstring}
|
||||
(fn-format ?fntype ?name ?arglist ?docstring)
|
||||
_ (if (-?>> result.keys length (< 0))
|
||||
(code-block
|
||||
(.. "ERROR, I don't know how to show this "
|
||||
@ -112,5 +148,6 @@ fntype is one of fn or λ or lambda"
|
||||
:kind (. kinds (?. result :metadata :fls/itemKind))})
|
||||
(tset :documentation (hover-format result))))
|
||||
|
||||
{: hover-format
|
||||
{: signature-help-format
|
||||
: hover-format
|
||||
: completion-item-format}
|
||||
|
||||
@ -40,7 +40,9 @@ Every time the client sends a message, it gets handled by a function in the corr
|
||||
:triggerCharacters ["(" "[" "{" "." ":" "\""]
|
||||
:completionItem {:labelDetailsSupport false}}
|
||||
:hoverProvider {:workDoneProgress false}
|
||||
;; :signatureHelpProvider nil
|
||||
:signatureHelpProvider {:workDoneProgress false
|
||||
:triggerCharacters [" "]
|
||||
:retriggerCharacters [" "]}
|
||||
;; :declarationProvider nil
|
||||
:definitionProvider {:workDoneProgress false}
|
||||
;; :typeDefinitionProvider nil
|
||||
@ -135,6 +137,19 @@ Every time the client sends a message, it gets handled by a function in the corr
|
||||
result)
|
||||
(catch _ nil))))
|
||||
|
||||
(λ requests.textDocument/signatureHelp [server
|
||||
_send
|
||||
{:textDocument {: uri} : position}]
|
||||
(let [file (files.get-by-uri server uri)]
|
||||
(case-try (analyzer.find-nearest-call server file position)
|
||||
(symbol active-parameter)
|
||||
(analyzer.find-nearest-definition server file symbol)
|
||||
{:indeterminate nil &as result}
|
||||
(message.symbol->signature-help server file symbol
|
||||
(formatter.signature-help-format result)
|
||||
active-parameter)
|
||||
(catch _ nil))))
|
||||
|
||||
(λ requests.textDocument/hover [server _send {: position :textDocument {: uri}}]
|
||||
(let [file (files.get-by-uri server uri)
|
||||
byte (utils.position->byte file.text position server.position-encoding)]
|
||||
|
||||
@ -4,7 +4,7 @@ the `file.diagnostics` field, filling it with diagnostics."
|
||||
|
||||
(local {: sym? : list? : table? : view
|
||||
: sym : list &as fennel} (require :fennel))
|
||||
(local {:scopes {:global {: specials}}} (require :fennel.compiler))
|
||||
(local {: special? : op?} (require :fennel-ls.compiler))
|
||||
(local analyzer (require :fennel-ls.analyzer))
|
||||
(local message (require :fennel-ls.message))
|
||||
(local utils (require :fennel-ls.utils))
|
||||
@ -16,20 +16,6 @@ the `file.diagnostics` field, filling it with diagnostics."
|
||||
(fn diagnostic [self quickfix]
|
||||
(setmetatable {: self : quickfix} diagnostic-mt))
|
||||
|
||||
(local ops {"+" 1 "-" 1 "*" 1 "/" 1 "//" 1 "%" 1 "^" 1 ">" 1 "<" 1 ">=" 1
|
||||
"<=" 1 "=" 1 "not=" 1 ".." 1 "." 1 "and" 1 "or" 1 "band" 1
|
||||
"bor" 1 "bxor" 1 "bnot" 1 "lshift" 1 "rshift" 1})
|
||||
|
||||
(fn special? [item]
|
||||
(and (sym? item)
|
||||
(. specials (tostring item))
|
||||
item))
|
||||
|
||||
(fn op? [item]
|
||||
(and (sym? item)
|
||||
(. ops (tostring item))
|
||||
item))
|
||||
|
||||
(fn could-be-rewritten-as-sym? [str]
|
||||
(and (= :string (type str)) (not (str:find "^%d"))
|
||||
(not (str:find "[^!$%*+/0-9<=>?A-Z\\^_a-z|\128-\255-]"))))
|
||||
|
||||
@ -91,6 +91,11 @@ LSP json objects."
|
||||
:kind ?kind
|
||||
:edit {:changes {uri (diagnostic.quickfix)}}}))
|
||||
|
||||
(λ symbol->signature-help [_server _file _call signature active-parameter]
|
||||
{:signatures [signature]
|
||||
:activeSignature 0
|
||||
:activeParameter active-parameter})
|
||||
|
||||
(λ multisym->range [server file ast n]
|
||||
(let [spl (utils.multi-sym-split ast)
|
||||
n (if (< n 0) (+ n 1 (length spl)) n)]
|
||||
@ -129,6 +134,7 @@ LSP json objects."
|
||||
: create-error
|
||||
: ast->range
|
||||
: diagnostic->code-action
|
||||
: symbol->signature-help
|
||||
: multisym->range
|
||||
: range-and-uri
|
||||
: diagnostics
|
||||
|
||||
76
test/signature-help.fnl
Normal file
76
test/signature-help.fnl
Normal file
@ -0,0 +1,76 @@
|
||||
(local faith (require :faith))
|
||||
(local {: view} (require :fennel))
|
||||
(local {: create-client} (require :test.utils))
|
||||
|
||||
(λ check-signature [expected response]
|
||||
(case response
|
||||
{:signatures [{:label signature}]}
|
||||
(faith.= expected.signature signature)
|
||||
;; fail
|
||||
_ (faith.is nil (.. "Invalid response: " (view response)))))
|
||||
|
||||
(fn check [file-contents expected]
|
||||
(let [{: client : uri : cursor} (create-client file-contents)
|
||||
[{: result}] (client:signature-help uri cursor)]
|
||||
(check-signature expected result)))
|
||||
|
||||
(fn test-fn-definition []
|
||||
(check "(fn func [arg1 arg2] (print :hello))
|
||||
(func|)"
|
||||
{:signature "(func arg1 arg2)"})
|
||||
|
||||
(check "(fn func [arg1 arg2] (print :hello))
|
||||
(func |)"
|
||||
{:signature "(func arg1 arg2)"})
|
||||
|
||||
(check "(fn func [arg1 arg2] (print :hello))
|
||||
(func a1|)"
|
||||
{:signature "(func arg1 arg2)"})
|
||||
|
||||
(check "(fn func [arg1 arg2] (print :hello))
|
||||
(func a|1 a2)"
|
||||
{:signature "(func arg1 arg2)"})
|
||||
|
||||
(check "(fn func [arg1 arg2] (print :hello))
|
||||
(func a1 a2|)"
|
||||
{:signature "(func arg1 arg2)"})
|
||||
|
||||
(check "(fn func [arg1 arg2] (print :hello))
|
||||
(func a1 a|2)"
|
||||
{:signature "(func arg1 arg2)"}))
|
||||
|
||||
(fn test-lua-builtin []
|
||||
(check "(error msg lvl|)"
|
||||
{:signature "(error message ?level)"}))
|
||||
|
||||
(fn test-multisym []
|
||||
(check "(table.concat tbl s|)"
|
||||
{:signature "(table.concat list ?sep ?i ?j)"}))
|
||||
|
||||
(fn test-destructuring-arg []
|
||||
(check "(fn dstr [{:field name} arg2] {})
|
||||
(dstr |)"
|
||||
{:signature "(dstr {:field name} arg2)"})
|
||||
|
||||
(check "(fn dstr [{:field name} arg2] {})
|
||||
(dstr arg1 ar|)"
|
||||
{:signature "(dstr {:field name} arg2)"}))
|
||||
|
||||
(fn test-special []
|
||||
(check "(each |)"
|
||||
{:signature "(each [key value (iterator)] ...)"})
|
||||
|
||||
(check "(each [|])"
|
||||
{:signature "(each [key value (iterator)] ...)"})
|
||||
|
||||
(check "(each [k val|])"
|
||||
{:signature "(each [key value (iterator)] ...)"})
|
||||
|
||||
(check "(each [33|])"
|
||||
{:signature "(each [key value (iterator)] ...)"}))
|
||||
|
||||
{: test-fn-definition
|
||||
: test-lua-builtin
|
||||
: test-multisym
|
||||
: test-destructuring-arg
|
||||
: test-special}
|
||||
@ -55,6 +55,14 @@
|
||||
{: position
|
||||
:textDocument {:uri file}})))
|
||||
|
||||
(fn signature-help [self file position]
|
||||
(dispatch.handle*
|
||||
self.server
|
||||
(message.create-request (next-id! self)
|
||||
:textDocument/signatureHelp
|
||||
{: position
|
||||
:textDocument {:uri file}})))
|
||||
|
||||
(fn rename [self file position newName]
|
||||
(dispatch.handle* self.server
|
||||
(message.create-request (next-id! self) :textDocument/rename
|
||||
@ -83,6 +91,7 @@
|
||||
: hover
|
||||
: references
|
||||
: document-highlight
|
||||
: signature-help
|
||||
: rename
|
||||
: code-action
|
||||
: did-save}})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user