diff --git a/README.md b/README.md index c05f8f0..a340f3b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Features / To Do List / Things I would enjoy patches for: - [ ] directly on require statements - [X] for definitions in the same file - [ ] for definitions in other files - - [ ] follows multisyms through table constructor + - [X] follows multisyms through table constructor - [ ] follows multisyms through mutations (difficult) - [ ] for methods/metamethods (difficult, in the general case may require type annotations or some insane global type inference logic) - [ ] into lua files (maybe cheeseable with antifennel if it has --correlate?) diff --git a/src/fennel-ls.fnl b/src/fennel-ls.fnl index 9599115..87e6f20 100644 --- a/src/fennel-ls.fnl +++ b/src/fennel-ls.fnl @@ -1,15 +1,12 @@ -(local fennel (require :fennel)) -(local dispatch (require :fennel-ls.dispatch)) +(local core (require :fennel-ls.core)) (local json-rpc (require :fennel-ls.json-rpc)) -(local {: log} (require :fennel-ls.log)) (λ main-loop [in out] (local send (partial json-rpc.write out)) (local state []) (while true (let [msg (json-rpc.read in)] - ;; (log msg) - (dispatch.handle state send msg)))) + (core.handle state send msg)))) (λ main [] (main-loop diff --git a/src/fennel-ls/plugin.fnl b/src/fennel-ls/analyze.fnl similarity index 59% rename from src/fennel-ls/plugin.fnl rename to src/fennel-ls/analyze.fnl index bba54fb..6957674 100644 --- a/src/fennel-ls/plugin.fnl +++ b/src/fennel-ls/analyze.fnl @@ -1,6 +1,11 @@ (local fennel (require :fennel)) +(local fennelutils (require :fennel.utils)) +(local utils (require :fennel-ls.utils)) (local insert table.insert) +(local sym? fennel.sym?) +(local list? fennel.list?) + ;; words surrounded by - are symbols, ;; because fennel doesn't allow 'require in a runtime file (local -require- (fennel.sym :require)) @@ -8,6 +13,33 @@ (local -λ- (fennel.sym :λ)) (local -lambda- (fennel.sym :lambda)) +(λ contains? [?ast byte] + ;; check if an ast contains a byte + (and (= (type ?ast) :table) + (utils.get-ast-info ?ast :bytestart) + (utils.get-ast-info ?ast :byteend) + (<= (utils.get-ast-info ?ast :bytestart) + byte + (+ 1 (utils.get-ast-info ?ast :byteend))))) + +(λ does-not-contain? [?ast byte] + ;; check if a byte is in range of the ast + (and (= (type ?ast) :table) + (utils.get-ast-info ?ast :bytestart) + (utils.get-ast-info ?ast :byteend) + (not + (<= (utils.get-ast-info ?ast :bytestart) + byte + (+ 1 (utils.get-ast-info ?ast :byteend)))))) + + +(λ past? [?ast byte] + ;; check if a byte is past an ast object + (and (= (type ?ast) :table) + (utils.get-ast-info ?ast :bytestart) + (< byte (utils.get-ast-info ?ast :bytestart)) + false)) + (λ multisym? [t] ;; check if t is a symbol with multiple parts, eg. foo.bar.baz (and (fennel.sym? t) @@ -126,5 +158,66 @@ (set file.ast ast)) ;; (set file.analyzed? true)) +(var + (search-assignment + search-symbol + search) + nil) -{: analyze} +(set search-assignment + (λ search-assignment [self file binding ?definition stack] + (if (= 0 (length stack)) + binding + ;; TODO sift down the binding + (search self file ?definition stack)))) + +(set search-symbol + (λ search-symbol [self file symbol stack] + (let [split (utils.multi-sym-split symbol)] + (for [i (length split) 2 -1] + (table.insert stack (. split i)))) + (match (. file.references symbol) + to (search-assignment self file to.binding to.definition stack) + nil nil))) + +(set search + (λ search [self file item stack] + (if (fennelutils.table? item) + (if (. item (. stack (length stack))) + (search self file (. item (table.remove stack)) stack) + nil) + (sym? item) + (search-symbol self file item stack) + ;; TODO + ;; functioncall (continue searching in body) + ;; require call (search in the new module) + :else (error (.. "I don't know what to do with " (fennel.view item)))))) + +(λ find-symbol [ast byte ?recursively-called] + (if (not= :table (type ast)) + nil + (does-not-contain? ast byte) + nil + (sym? ast) + ast + (or (not ?recursively-called) + (fennel.list? ast) + (fennel.sequence? ast)) + ;; TODO binary search + (accumulate + [result nil + _ v (ipairs ast) + &until (or result (past? v byte))] + (find-symbol v byte true)) + :else + (accumulate + [result nil + k v (pairs ast) + &until result] + (or + (find-symbol k byte true) + (find-symbol v byte true))))) + +{: analyze + : find-symbol + : search-symbol} diff --git a/src/fennel-ls/dispatch.fnl b/src/fennel-ls/core.fnl similarity index 86% rename from src/fennel-ls/dispatch.fnl rename to src/fennel-ls/core.fnl index 1a19f1d..ea1e854 100644 --- a/src/fennel-ls/dispatch.fnl +++ b/src/fennel-ls/core.fnl @@ -1,4 +1,4 @@ -"Dispatch +"core This module is responsible for deciding which code to call in response to a given LSP request from the client. @@ -7,12 +7,12 @@ In general, this involves: * determining the type of the message * calling the appropriate handler" -(local handlers (require :fennel-ls.the-actual-code)) +(local handlers (require :fennel-ls.handlers)) (local message (require :fennel-ls.message)) (λ handle-request [self send id method ?params] - "Call the appropriate request handler. -The return value of the request is sent back to the server." + ;; Call the appropriate request handler. + ;; The return value of the request is sent back to the server. (match (. handlers.requests method) callback (match (callback self send ?params) @@ -26,15 +26,15 @@ The return value of the request is sent back to the server." id)))) (λ handle-response [self send id result] - "I don't care about responses yet" + ;; I don't care about responses yet nil) (λ handle-bad-response [self send id err] - "Handle a message indicating an error. Right now, it just crashes the server." + ;; Handle a message indicating an error. Right now, it just crashes the server. (error (.. "oopsie: " err.code))) (λ handle-notification [self send method ?params] - "Call the appropriate notification handler." + ;; Call the appropriate notification handler. (match (. handlers.notifications method) callback (callback self send ?params))) ;; Silent error for unknown notifications diff --git a/src/fennel-ls/the-actual-code.fnl b/src/fennel-ls/handlers.fnl similarity index 56% rename from src/fennel-ls/the-actual-code.fnl rename to src/fennel-ls/handlers.fnl index 18789d9..9b2b5aa 100644 --- a/src/fennel-ls/the-actual-code.fnl +++ b/src/fennel-ls/handlers.fnl @@ -1,19 +1,14 @@ -"The actual code +"Big dispatch You finally made it. Here is the main code that implements the language server protocol Every time the client sends a message, it gets handled by a function in the corresponding table type. (ie, a textDocument/didChange notification will call notifications.textDocument/didChange and a textDocument/defintion request will call requests.textDocument/didChange)" -(local fennel (require :fennel)) -(local sym? fennel.sym?) -(local list? fennel.list?) -(local fennelutils (require :fennel.utils)) +(local utils (require :fennel-ls.utils)) +(local message (require :fennel-ls.message)) -(local parser (require :fennel-ls.parser)) -(local util (require :fennel-ls.util)) -(local mod (require :fennel-ls.mod)) -(local {: log} (require :fennel-ls.log)) (local state (require :fennel-ls.state)) +(local analyze (require :fennel-ls.analyze)) (local requests []) (local notifications []) @@ -64,81 +59,19 @@ Every time the client sends a message, it gets handled by a function in the corr {:capabilities capabilities :serverInfo {:name "fennel-ls" :version "0.0.0"}}) -;; These three functions are mutually recursive -(var - (search-assignment - search-symbol - search) - nil) - -(set search-assignment - (λ search-assignment [self file binding ?definition stack] - (if (= 0 (length stack)) - binding - ;; TODO sift down the binding - (search self file ?definition stack)))) - -(set search-symbol - (λ search-symbol [self file symbol stack] - (let [split (util.multi-sym-split symbol)] - (for [i (length split) 2 -1] - (table.insert stack (. split i)))) - (match (. file.references symbol) - to (search-assignment self file to.binding to.definition stack) - nil nil))) - -(set search - (λ search [self file item stack] - (if (fennelutils.table? item) - (if (. item (. stack (length stack))) - (search self file (. item (table.remove stack)) stack) - nil) - (sym? item) - (search-symbol self file item stack) - ;; TODO - ;; functioncall (continue searching in body) - ;; require call (search in the new module) - :else (error (.. "I don't know what to do with " (fennel.view item)))))) - -(λ find-symbol [ast byte ?recursively-called] - (if (not= :table (type ast)) - nil - (parser.does-not-contain? ast byte) - nil - (sym? ast) - ast - (or (not ?recursively-called) - (fennel.list? ast) - (fennel.sequence? ast)) - ;; TODO binary search - (accumulate - [result nil - _ v (ipairs ast) - &until (or result (parser.past? v byte))] - (find-symbol v byte true)) - :else - (accumulate - [result nil - k v (pairs ast) - &until result] - (or - (find-symbol k byte true) - (find-symbol v byte true))))) - (λ requests.textDocument/definition [self send {: position :textDocument {: uri}}] (local file (state.get-by-uri self uri)) - (local byte (util.pos->byte file.text position.line position.character)) - (match (find-symbol file.ast byte) + (local byte (utils.pos->byte file.text position.line position.character)) + (match (analyze.find-symbol file.ast byte) symbol - (match (search-symbol self file symbol []) + (match (analyze.search-symbol self file symbol []) definition - {:range (parser.range file.text definition) + {:range (message.range file.text definition) :uri uri}))) - (λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}] (local file (state.get-by-uri self uri)) (assert file.open?) - (util.apply-changes (. self.files uri) contentChanges)) + (utils.apply-changes (. self.files uri) contentChanges)) (λ notifications.textDocument/didOpen [self send {:textDocument {: languageId : text : uri}}] (local file (state.set-uri-contents self uri text)) diff --git a/src/fennel-ls/log.fnl b/src/fennel-ls/log.fnl deleted file mode 100644 index 9a1a345..0000000 --- a/src/fennel-ls/log.fnl +++ /dev/null @@ -1,20 +0,0 @@ -"Log -In the Language Server Protocol, io.stdout is used to send messages to the client. -Because of this, I need another way to do print-debugging." - -(local fennel (require :fennel)) -(local disable-logs false) -(if disable-logs - {:log #nil} - (let [logdocument (io.open "/tmp/fennel.log" "w")] - (assert logdocument) - (fn log [...] - (let [args []] - (for [i 1 (select :# ...)] - (table.insert args - (let [item (select i ...)] - (match (values item (type item)) - (str :string) str - ?any (fennel.view ?any))))) - (logdocument:write (table.concat args) "\n"))) - {: log})) diff --git a/src/fennel-ls/message.fnl b/src/fennel-ls/message.fnl index 6a618c3..34b3758 100644 --- a/src/fennel-ls/message.fnl +++ b/src/fennel-ls/message.fnl @@ -4,6 +4,7 @@ that may need to be sent to the client. I have them all here because I have a feeling I am conflating missing fields with null fields, and I want to have one location to look to fix this in the future." +(local utils (require :fennel-ls.utils)) (local error-codes {;; JSON-RPC errors @@ -43,8 +44,18 @@ and I want to have one location to look to fix this in the future." : id :result ?result}) +(λ range [text ?ast] + "create a LSP range representing the span of an AST object" + (if (= (type ?ast) :table) + (match (values (utils.get-ast-info ?ast :bytestart) (utils.get-ast-info ?ast :byteend)) + (i j) + (let [(start-line start-col) (utils.byte->pos text i) + (end-line end-col) (utils.byte->pos text (+ j 1))] + {:start {:line start-line :character start-col} + :end {:line end-line :character end-col}})))) + {: create-notification : create-request : create-response - : create-error} - + : create-error + : range} diff --git a/src/fennel-ls/parser.fnl b/src/fennel-ls/parser.fnl deleted file mode 100644 index cf66d0e..0000000 --- a/src/fennel-ls/parser.fnl +++ /dev/null @@ -1,49 +0,0 @@ -(local fennel (require :fennel)) -(local util (require :fennel-ls.util)) - -(λ get-ast-info [?ast info] - ;; find a given key of info from an AST object - (or (?. (getmetatable ?ast) info) - (. ?ast info))) - -(λ contains? [?ast byte] - "check if a byte is in range of the AST object" - (and (= (type ?ast) :table) - (get-ast-info ?ast :bytestart) - (get-ast-info ?ast :byteend) - (<= (get-ast-info ?ast :bytestart) - byte - (+ 1 (get-ast-info ?ast :byteend))))) - -(λ does-not-contain? [?ast byte] - "check if a byte is in range of the AST object" - (and (= (type ?ast) :table) - (get-ast-info ?ast :bytestart) - (get-ast-info ?ast :byteend) - (not - (<= (get-ast-info ?ast :bytestart) - byte - (+ 1 (get-ast-info ?ast :byteend)))))) - - -(λ past? [?ast byte] - "check if a byte is past the range of the AST object" - (and (= (type ?ast) :table) - (get-ast-info ?ast :bytestart) - (< byte (get-ast-info ?ast :bytestart)) - false)) - -(λ range [text ?ast] - "create a LSP range representing the span of an AST object" - (if (= (type ?ast) :table) - (match (values (get-ast-info ?ast :bytestart) (get-ast-info ?ast :byteend)) - (i j) - (let [(start-line start-col) (util.byte->pos text i) - (end-line end-col) (util.byte->pos text (+ j 1))] - {:start {:line start-line :character start-col} - :end {:line end-line :character end-col}})))) - -{: contains? - : does-not-contain? - : past? - : range} diff --git a/src/fennel-ls/mod.fnl b/src/fennel-ls/searcher.fnl similarity index 88% rename from src/fennel-ls/mod.fnl rename to src/fennel-ls/searcher.fnl index 0223519..f02fed9 100644 --- a/src/fennel-ls/mod.fnl +++ b/src/fennel-ls/searcher.fnl @@ -1,9 +1,9 @@ -"Mod +"Searcher This file has all the logic needed to take the name of a module and find the corresponding URI. I suspect this file is going to be gone after a bit of refactoring." (local fennel (require :fennel)) -(local util (require :fennel-ls.util)) +(local utils (require :fennel-ls.utils)) "works on my machine >:)" (local luapath "?.lua;src/?.lua") @@ -36,13 +36,13 @@ I suspect this file is going to be gone after a bit of refactoring." (if (is_absolute path) (table.insert result path) (each [_ workspace (ipairs (or ?workspaces []))] - (table.insert result (join (util.uri->path workspace) path))))) + (table.insert result (join (utils.uri->path workspace) path))))) (table.concat result ";"))) (λ lookup [{: root-uri} mod] (match (or (fennel.searchModule mod (add-workspaces-to-path luapath [root-uri])) (fennel.searchModule mod (add-workspaces-to-path fennelpath [root-uri]))) - modname (util.path->uri modname) + modname (utils.path->uri modname) nil nil)) {: lookup} diff --git a/src/fennel-ls/state.fnl b/src/fennel-ls/state.fnl index 29a7d1b..06854d5 100644 --- a/src/fennel-ls/state.fnl +++ b/src/fennel-ls/state.fnl @@ -1,7 +1,7 @@ -(local util (require :fennel-ls.util)) -(local mod (require :fennel-ls.mod)) +(local utils (require :fennel-ls.utils)) +(local searcher (require :fennel-ls.searcher)) -(local {: analyze} (require :fennel-ls.plugin)) +(local {: analyze} (require :fennel-ls.analyze)) (λ init-state [self params] (set self.files {}) @@ -9,7 +9,7 @@ (set self.root-uri params.rootUri)) (λ read-file [uri] - (with-open [fd (io.open (util.uri->path uri))] + (with-open [fd (io.open (utils.uri->path uri))] {:uri uri :text (fd:read :*a)})) @@ -26,7 +26,7 @@ ;; if the cached uri isn't found, clear the cache and try again (do (tset self.modules module nil) (get-by-module self module))) - nil (let [uri (mod.lookup self module)] + nil (let [uri (searcher.lookup self module)] (tset self.modules module uri) (get-by-uri self uri)))) diff --git a/src/fennel-ls/util.fnl b/src/fennel-ls/utils.fnl similarity index 94% rename from src/fennel-ls/util.fnl rename to src/fennel-ls/utils.fnl index 142b6e3..e05d510 100644 --- a/src/fennel-ls/util.fnl +++ b/src/fennel-ls/utils.fnl @@ -70,6 +70,11 @@ These functions are all pure functions, which makes me happy." {: text} text))) +(λ get-ast-info [?ast info] + ;; find a given key of info from an AST object + (or (?. (getmetatable ?ast) info) + (. ?ast info))) + (fn multi-sym-split [sym ?offset] (local sym (tostring sym)) (local offset (or ?offset (length sym))) @@ -84,4 +89,5 @@ These functions are all pure functions, which makes me happy." : pos->byte : byte->pos : apply-changes - : multi-sym-split} + : multi-sym-split + : get-ast-info} diff --git a/test/goto-definition-test.fnl b/test/goto-definition-test.fnl index e5ae284..1056104 100644 --- a/test/goto-definition-test.fnl +++ b/test/goto-definition-test.fnl @@ -5,14 +5,14 @@ (local {: ROOT-URI : setup-server} (require :test.util)) -(local dispatch (require :fennel-ls.dispatch)) +(local core (require :fennel-ls.core)) (local message (require :fennel-ls.message)) (describe "jump to definition" (fn check [request-file line char response-file start-line start-col end-line end-col] (local state (doto [] setup-server)) - (let [message (dispatch.handle* state + (let [message (core.handle* state (message.create-request 2 "textDocument/definition" {:position {:character char :line line} :textDocument {:uri (.. ROOT-URI "/" request-file)}})) diff --git a/test/lsp-test.fnl b/test/lsp-test.fnl index 57739f9..6a03cae 100644 --- a/test/lsp-test.fnl +++ b/test/lsp-test.fnl @@ -2,7 +2,7 @@ (local is (require :luassert)) (local {: ROOT-PATH : ROOT-URI} (require :test.util)) -(local dispatch (require :fennel-ls.dispatch)) +(local core (require :fennel-ls.core)) (local server-initialize-message {:id 1 @@ -22,7 +22,7 @@ (describe "language server" (it "responds to initialize" (is-matching - (dispatch.handle* [] server-initialize-message) + (core.handle* [] server-initialize-message) [{:jsonrpc "2.0" :id 1 :result {:capabilities {} :serverInfo {:name "fennel-ls" : version}}}]))) diff --git a/test/misc-test.fnl b/test/misc-test.fnl index 6a0740b..70befd0 100644 --- a/test/misc-test.fnl +++ b/test/misc-test.fnl @@ -2,7 +2,7 @@ (local is (require :luassert)) (local fennel (require :fennel)) -(local {: multi-sym-split} (require :fennel-ls.util)) +(local {: multi-sym-split} (require :fennel-ls.utils)) (describe "multi-sym-split" (it "should be 1 on regular syms" diff --git a/test/string-processing-test.fnl b/test/string-processing-test.fnl index b6ad10a..52f6c17 100644 --- a/test/string-processing-test.fnl +++ b/test/string-processing-test.fnl @@ -2,9 +2,9 @@ (local is (require :luassert)) (local fennel (require :fennel)) -(local util (require :fennel-ls.util)) +(local utils (require :fennel-ls.utils)) -(describe "util" +(describe "utils" ;; fixme: ;; test for errors on out of bounds @@ -27,7 +27,7 @@ (it "updates the start of a line" (is.equal - (util.apply-changes + (utils.apply-changes "replace beginning" [{:range (range 0 0 0 7) :text "the"}]) @@ -35,7 +35,7 @@ (it "updates the end of a line" (is.equal - (util.apply-changes + (utils.apply-changes "first line\nsecond line\nreplace end" [{:range (range 2 7 2 11) :text "ment"}]) @@ -43,7 +43,7 @@ (it "replaces a line" (is.equal - (util.apply-changes + (utils.apply-changes "replace all" [{:range (range 0 0 0 11) :text "new string"}]) @@ -51,7 +51,7 @@ (it "can handle substituting things" (is.equal - (util.apply-changes + (utils.apply-changes "replace beginning" [{:range {:start {:line 0 :character 0} :end {:line 0 :character 7}} @@ -60,7 +60,7 @@ (it "can handle replacing everything" (is.equal - (util.apply-changes + (utils.apply-changes "this is the\nold file" [{:text "And this is the\nnew file"}]) "And this is the\nnew file")))) diff --git a/test/util.fnl b/test/util.fnl index 5f3c803..b6a2dbb 100644 --- a/test/util.fnl +++ b/test/util.fnl @@ -1,4 +1,4 @@ -(local dispatch (require :fennel-ls.dispatch)) +(local core (require :fennel-ls.core)) (local ROOT-PATH (-> (io.popen "pwd") @@ -24,6 +24,6 @@ :uri ROOT-URI}]}}) (fn setup-server [state] - (dispatch.handle* state initialization-message)) + (core.handle* state initialization-message)) {: ROOT-URI : setup-server}