diff --git a/changelog.md b/changelog.md index 94182f1..7fed2f1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ # Changelog ### Features +* Add `documentSymbol` support, for jumping to symbols definitions in the current document * Add `--fix` command-line argument to automatically apply lint fixes * Add `:legacy-multival` and `legacy-multival-case` lints, disabled by default * Code action "Expand macro" lets you see what a macro expands to diff --git a/src/fennel-ls/analyzer.fnl b/src/fennel-ls/analyzer.fnl index d9ca89c..1685585 100644 --- a/src/fennel-ls/analyzer.fnl +++ b/src/fennel-ls/analyzer.fnl @@ -38,7 +38,7 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find {:binding z :definition y}, referring to the `(local z y)` binding. " -(local {: sym? : list? : sequence? : varg?} (require :fennel)) +(local {: sym? : multi-sym? : list? : sequence? : varg?} (require :fennel)) (local {: get-ast-info &as utils} (require :fennel-ls.utils)) (local files (require :fennel-ls.files)) (local docs (require :fennel-ls.docs)) @@ -301,6 +301,29 @@ initialization-opts: {:stack ?list[ast] (fcollect [i 1 (length parents)] (. parents (- (length parents) i -1))))) +(λ find-document-symbols [server file] + "Find all the symbols defined in the file + +returns a sequential table of tables containing each symbol and its definition." + (compiler.compile server file) + (let [symbols []] + (each [symbol definition (pairs file.definitions)] + (when (and (or (sym? symbol) (multi-sym? symbol)) + definition.binding + (. file.lexical symbol) ; exclude gensyms + (not (= (tostring symbol) "_"))) + (table.insert symbols {: symbol : definition})) + ; definitions doesn't have multi-syms so we get them out of fields + (when definition.fields + (each [_field-name field-definition (pairs definition.fields)] + (when (and field-definition.binding + (or (sym? field-definition.binding) + (multi-sym? field-definition.binding))) + (table.insert symbols + {:symbol field-definition.binding + :definition field-definition}))))) + symbols)) + (λ find-nearest-call [server file byte] "Find the nearest call @@ -334,6 +357,7 @@ returns the called symbol and the number of the argument closest to byte" (search server file symbol {:stop-early? true} {:byte ?byte}))) {: find-symbol + : find-document-symbols : find-nearest-call : find-nearest-definition : find-definition diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index d2e0efb..fe56152 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -43,7 +43,7 @@ Every time the client sends a message, it gets handled by a function in the corr ;; :implementationProvider nil :referencesProvider {:workDoneProgress false} :documentHighlightProvider {:workDoneProgress false} - ;; :documentSymbolProvider nil + :documentSymbolProvider {:workDoneProgress false} :codeActionProvider {:workDoneProgress false} ;; :codeLensProvider nil ;; :documentLinkProvider nil @@ -159,6 +159,11 @@ Every time the client sends a message, it gets handled by a function in the corr :range (message.ast->range server file symbol)}) (catch _ nil)))) +(λ requests.textDocument/documentSymbol [server _send {:textDocument {: uri}}] + (let [file (files.get-by-uri server uri) + symbols (analyzer.find-document-symbols server file)] + (message.document-symbol-format server file symbols))) + (set {:textDocument/completion requests.textDocument/completion :completionItem/resolve requests.completionItem/resolve} (require :fennel-ls.completion)) diff --git a/src/fennel-ls/message.fnl b/src/fennel-ls/message.fnl index 03cabd0..3116bff 100644 --- a/src/fennel-ls/message.fnl +++ b/src/fennel-ls/message.fnl @@ -43,6 +43,12 @@ LSP json objects." :WARN :warning _ (string.lower k)))) +(local symbol-kind + {:File 1 :Module 2 :Namespace 3 :Package 4 :Class 5 :Method 6 :Property 7 + :Field 8 :Constructor 9 :Enum 10 :Interface 11 :Function 12 :Variable 13 + :Constant 14 :String 15 :Number 16 :Boolean 17 :Array 18 :Object 19 :Key 20 + :Null 21 :EnumMember 22 :Struct 23 :Event 24 :Operator 25 :TypeParameter 26}) + (λ create-error [code message ?id ?data] {:jsonrpc "2.0" :id ?id @@ -124,6 +130,35 @@ LSP json objects." {:type (. severity msg-type) : message})) +(λ definition->symbol-kind [definition] + (let [def definition.definition] + (if (fennel.list? def) + (let [head (. def 1)] + (if (or (fennel.sym? head :fn) + (fennel.sym? head :lambda) + (fennel.sym? head :λ)) + symbol-kind.Function + symbol-kind.Variable)) + symbol-kind.Variable))) + +(λ document-symbol-format [server file symbols] + (let [symbols (icollect [_ {: symbol : definition} (ipairs symbols)] + (let [name (tostring symbol) + kind (definition->symbol-kind definition) + range (ast->range server file definition.binding)] + (when range + {: name + : kind + : range + :selectionRange range})))] + ; the spec doesn't define an order, and not all clients sort the results + (table.sort symbols + (fn [a b] + (or (< a.range.start.line b.range.start.line) + (and (= a.range.start.line b.range.start.line) + (< a.range.start.character b.range.start.character))))) + symbols)) + {: create-notification : create-request : create-response @@ -137,4 +172,6 @@ LSP json objects." : severity : severity->string : show-message - : unknown-range} + : unknown-range + : document-symbol-format + : symbol-kind} diff --git a/test/document-highlight.fnl b/test/document-highlight.fnl index a30bf43..67a758a 100644 --- a/test/document-highlight.fnl +++ b/test/document-highlight.fnl @@ -1,18 +1,8 @@ (local faith (require :faith)) -(local {: create-client} (require :test.utils)) +(local {: create-client : range-comparator} (require :test.utils)) (local {: null} (require :dkjson)) (local {: view} (require :fennel)) -(fn range-comparator [a b] - (or (< a.range.start.line b.range.start.line) - (and (= a.range.start.line b.range.start.line) - (or (< a.range.start.character b.range.start.character) - (and (= a.range.start.character b.range.start.character) - (or (< a.range.end.line b.range.end.line) - (and (= a.range.end.line b.range.end.line) - (or (< a.range.end.character b.range.end.character) - (= a.range.end.character b.range.end.character))))))))) - (fn check [file-contents] (let [{: client : uri : cursor : highlights} (create-client file-contents) [response] (client:document-highlight uri cursor)] diff --git a/test/document-symbol.fnl b/test/document-symbol.fnl new file mode 100644 index 0000000..81e43d7 --- /dev/null +++ b/test/document-symbol.fnl @@ -0,0 +1,146 @@ +(local faith (require :faith)) +(local {: create-client : range-comparator} (require :test.utils)) +(local {: null} (require :dkjson)) + +(fn check [file-contents expected-symbols] + (let [{: client : uri} (create-client file-contents) + [response] (client:document-symbol uri)] + (if (not= null response.result) + (do + (table.sort response.result range-comparator) + (faith.= expected-symbols response.result)) + (faith.= expected-symbols [])))) + +(fn test-simple-locals [] + (check "(local x 10)" + [{:name "x" + :kind 13 + :range {:start {:line 0 :character 7} + :end {:line 0 :character 8}} + :selectionRange {:start {:line 0 :character 7} + :end {:line 0 :character 8}}}]) + + (check "(local y 20) + (local z 30)" + [{:name "y" + :kind 13 + :range {:start {:line 0 :character 7} + :end {:line 0 :character 8}} + :selectionRange {:start {:line 0 :character 7} + :end {:line 0 :character 8}}} + {:name "z" + :kind 13 + :range {:start {:line 1 :character 17} + :end {:line 1 :character 18}} + :selectionRange {:start {:line 1 :character 17} + :end {:line 1 :character 18}}}]) + nil) + +(fn test-functions [] + (check "(fn my-func [])" + [{:name "my-func" + :kind 12 + :range {:start {:line 0 :character 4} + :end {:line 0 :character 11}} + :selectionRange {:start {:line 0 :character 4} + :end {:line 0 :character 11}}}]) + + (check "(λ another-fn [x] x)" + [{:name "another-fn" + :kind 12 + :range {:start {:line 0 :character 4} + :end {:line 0 :character 14}} + :selectionRange {:start {:line 0 :character 4} + :end {:line 0 :character 14}}} + {:name "x" + :kind 13 + :range {:start {:line 0 :character 16} + :end {:line 0 :character 17}} + :selectionRange {:start {:line 0 :character 16} + :end {:line 0 :character 17}}}]) + nil) + +(fn test-mixed [] + (check "(local x 10) + (fn my-func [y] (+ x y))" + [{:name "x" + :kind 13 + :range {:start {:line 0 :character 7} + :end {:line 0 :character 8}} + :selectionRange {:start {:line 0 :character 7} + :end {:line 0 :character 8}}} + {:name "my-func" + :kind 12 + :range {:start {:line 1 :character 14} + :end {:line 1 :character 21}} + :selectionRange {:start {:line 1 :character 14} + :end {:line 1 :character 21}}} + {:name "y" + :kind 13 + :range {:start {:line 1 :character 23} + :end {:line 1 :character 24}} + :selectionRange {:start {:line 1 :character 23} + :end {:line 1 :character 24}}}]) + nil) + +(fn test-multi-sym-functions [] + (check "(local M {}) + (fn M.my-func [x] x)" + [{:name "M" + :kind 13 + :range {:start {:line 0 :character 7} + :end {:line 0 :character 8}} + :selectionRange {:start {:line 0 :character 7} + :end {:line 0 :character 8}}} + {:name "M.my-func" + :kind 12 + :range {:start {:line 1 :character 14} + :end {:line 1 :character 23}} + :selectionRange {:start {:line 1 :character 14} + :end {:line 1 :character 23}}} + {:name "x" + :kind 13 + :range {:start {:line 1 :character 25} + :end {:line 1 :character 26}} + :selectionRange {:start {:line 1 :character 25} + :end {:line 1 :character 26}}}]) + + (check "(local module {}) + (fn module.func1 []) + (fn module.func2 [a b])" + [{:name "module" + :kind 13 + :range {:start {:line 0 :character 7} + :end {:line 0 :character 13}} + :selectionRange {:start {:line 0 :character 7} + :end {:line 0 :character 13}}} + {:name "module.func1" + :kind 12 + :range {:start {:line 1 :character 14} + :end {:line 1 :character 26}} + :selectionRange {:start {:line 1 :character 14} + :end {:line 1 :character 26}}} + {:name "module.func2" + :kind 12 + :range {:start {:line 2 :character 14} + :end {:line 2 :character 26}} + :selectionRange {:start {:line 2 :character 14} + :end {:line 2 :character 26}}} + {:name "a" + :kind 13 + :range {:start {:line 2 :character 28} + :end {:line 2 :character 29}} + :selectionRange {:start {:line 2 :character 28} + :end {:line 2 :character 29}}} + {:name "b" + :kind 13 + :range {:start {:line 2 :character 30} + :end {:line 2 :character 31}} + :selectionRange {:start {:line 2 :character 30} + :end {:line 2 :character 31}}}]) + nil) + +{: test-simple-locals + : test-functions + : test-mixed + : test-multi-sym-functions} diff --git a/test/init.fnl b/test/init.fnl index d8b7540..f5e332a 100644 --- a/test/init.fnl +++ b/test/init.fnl @@ -22,6 +22,7 @@ :test.completion :test.references :test.document-highlight + :test.document-symbol :test.signature-help :test.lint :test.code-action diff --git a/test/references.fnl b/test/references.fnl index c29c8d9..1c401d9 100644 --- a/test/references.fnl +++ b/test/references.fnl @@ -1,20 +1,8 @@ (local faith (require :faith)) -(local {: create-client} (require :test.utils)) +(local {: create-client : location-comparator} (require :test.utils)) (local {: null} (require :dkjson)) (local {: view} (require :fennel)) -(fn location-comparator [a b] - (or (< a.uri b.uri) - (and (= a.uri b.uri) - (or (< a.range.start.line b.range.start.line) - (and (= a.range.start.line b.range.start.line) - (or (< a.range.start.character b.range.start.character) - (and (= a.range.start.character b.range.start.character) - (or (< a.range.end.line b.range.end.line) - (and (= a.range.end.line b.range.end.line) - (or (< a.range.end.character b.range.end.character) - (= a.range.end.character b.range.end.character))))))))))) - (fn check [file-contents] (let [{: client : uri : cursor : locations} (create-client file-contents) [response] (client:references uri cursor)] diff --git a/test/utils/client.fnl b/test/utils/client.fnl index 0227d1d..90805e7 100644 --- a/test/utils/client.fnl +++ b/test/utils/client.fnl @@ -60,6 +60,11 @@ {: position :textDocument {:uri file}}))) +(fn document-symbol [self file] + (dispatch.handle* self.server + (message.create-request (next-id! self) :textDocument/documentSymbol + {:textDocument {:uri file}}))) + (fn signature-help [self file position] (dispatch.handle* self.server @@ -105,6 +110,7 @@ : hover : references : document-highlight + : document-symbol : signature-help : rename : code-action diff --git a/test/utils/init.fnl b/test/utils/init.fnl index cf27e62..df9678a 100644 --- a/test/utils/init.fnl +++ b/test/utils/init.fnl @@ -104,7 +104,24 @@ (fn position-past-end-of-text [text ?encoding] (utils.byte->position text (+ (length text) 1) (or ?encoding default-encoding))) +(fn range-comparator [a b] + (or (< a.range.start.line b.range.start.line) + (and (= a.range.start.line b.range.start.line) + (or (< a.range.start.character b.range.start.character) + (and (= a.range.start.character b.range.start.character) + (or (< a.range.end.line b.range.end.line) + (and (= a.range.end.line b.range.end.line) + (or (< a.range.end.character b.range.end.character) + (= a.range.end.character b.range.end.character))))))))) + +(fn location-comparator [a b] + (or (< a.uri b.uri) + (and (= a.uri b.uri) + (range-comparator a b)))) + {: create-client : position-past-end-of-text : parse-markup + : range-comparator + : location-comparator : NIL}