diff --git a/changelog.md b/changelog.md index 7dd55cd..b885f9b 100644 --- a/changelog.md +++ b/changelog.md @@ -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. diff --git a/src/fennel-ls/analyzer.fnl b/src/fennel-ls/analyzer.fnl index 0739e31..b048359 100644 --- a/src/fennel-ls/analyzer.fnl +++ b/src/fennel-ls/analyzer.fnl @@ -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 diff --git a/src/fennel-ls/compiler.fnl b/src/fennel-ls/compiler.fnl index f571564..1780a69 100644 --- a/src/fennel-ls/compiler.fnl +++ b/src/fennel-ls/compiler.fnl @@ -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} diff --git a/src/fennel-ls/formatter.fnl b/src/fennel-ls/formatter.fnl index 864ef55..0eb2b96 100644 --- a/src/fennel-ls/formatter.fnl +++ b/src/fennel-ls/formatter.fnl @@ -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} diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index 3ea6d10..b8bc15f 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -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)] diff --git a/src/fennel-ls/lint.fnl b/src/fennel-ls/lint.fnl index e29e8f0..168d202 100644 --- a/src/fennel-ls/lint.fnl +++ b/src/fennel-ls/lint.fnl @@ -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-]")))) diff --git a/src/fennel-ls/message.fnl b/src/fennel-ls/message.fnl index 9b3205a..a142a6c 100644 --- a/src/fennel-ls/message.fnl +++ b/src/fennel-ls/message.fnl @@ -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 diff --git a/test/signature-help.fnl b/test/signature-help.fnl new file mode 100644 index 0000000..f3e459d --- /dev/null +++ b/test/signature-help.fnl @@ -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} diff --git a/test/utils/client.fnl b/test/utils/client.fnl index 37a07d2..d1dc210 100644 --- a/test/utils/client.fnl +++ b/test/utils/client.fnl @@ -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}})