From 4847179afd2fe18efa285878cde0d49c594b18e3 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Tue, 2 Aug 2022 18:18:03 -0500 Subject: [PATCH] Created a compiler plugin, local go-to-definition --- Makefile | 2 +- src/fennel-ls/parser.fnl | 16 +++--- src/fennel-ls/plugin.fnl | 84 +++++++++++++++++++++++++++++++ src/fennel-ls/state.fnl | 2 +- src/fennel-ls/the-actual-code.fnl | 42 ++++------------ test/goto-definition-test.fnl | 17 +++---- test/test-project/example.fnl | 3 +- test/test-project/foo.fnl | 3 +- 8 files changed, 115 insertions(+), 54 deletions(-) create mode 100644 src/fennel-ls/plugin.fnl diff --git a/Makefile b/Makefile index 90ec671..f6cc2f9 100644 --- a/Makefile +++ b/Makefile @@ -10,4 +10,4 @@ fennel-ls: $(SOURCES) clean: rm -f fennel-ls test: - FENNEL_PATH="./src/?.fnl;./src/?/init.fnl" ./fennel --correlate test/init.fnl + FENNEL_PATH="./src/?.fnl;./src/?/init.fnl" ./fennel --correlate test/init.fnl --verbose diff --git a/src/fennel-ls/parser.fnl b/src/fennel-ls/parser.fnl index 5e58fcf..6d312f9 100644 --- a/src/fennel-ls/parser.fnl +++ b/src/fennel-ls/parser.fnl @@ -18,20 +18,16 @@ (and (= (type ast) :table) (< byte (get-ast-info ast :bytestart)))) -(fn range [ast] +(fn 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 i) - (end-line end-col) (util.byte->pos j)] + (let [(start-line start-col) (util.byte->pos text i) + (end-line end-col) (util.byte->pos text j)] {:start {:line start-line :character start-col} :end {:line end-line :character end-col}})))) -(fn from-fennel [file] - (icollect [k v (fennel.parser file.text file.uri)] - v)) - -{: from-fennel - : contains? - : past?} +{: contains? + : past? + : range} diff --git a/src/fennel-ls/plugin.fnl b/src/fennel-ls/plugin.fnl new file mode 100644 index 0000000..65b5739 --- /dev/null +++ b/src/fennel-ls/plugin.fnl @@ -0,0 +1,84 @@ +(local fennel (require :fennel)) +(local insert table.insert) + +;; words surrounded by - are symbols, +;; because fennel doesn't allow 'require in a runtime file +(local -require- (fennel.sym :require)) +(local -local- (fennel.sym :local)) +(local -fn- (fennel.sym :fn)) + +;; types of things in the file.references list +{:from "a literal range" :to "a literal range"} +{:from "a literal range" :to-other-module ["modname" "key1" "key2" "key3" "key4" "etc"]} + +(fn table? [t] + (= :table (type t))) + +(fn multisym? [t] + (and (fennel.sym? t) + (let [t (tostring t)] + (or (t:find "%.") + (t:find ":"))))) + + +(λ analyze [file] + (assert file.text (fennel.view file)) + (assert file.uri) + (set file.references []) + + (local scope-notes + (doto {} + (setmetatable + {:__index + (fn [self key] + (let [val {}] + (tset self key val) + val))}))) + + (fn find-reference [name scope] + (when scope + (or (. scope-notes scope (tostring name)) + (find-reference name scope.parent)))) + + (fn call [ast scope] + "called for every function call. Most calls aren't interesting, but (require) and (local) are" + (match ast + [-local- name value] + nil ;; not actually interesting, I pranked you + (where [-require- mod] (= :string (type mod))) + (insert file.references {:from ast :to-other-module [mod]}))) + ;; nothing + + (fn reference [ast scope] + "called whenever a variable is referenced" + (assert (fennel.sym? ast)) + (let [name (or (string.match (tostring ast) "[^%.:]+"))] + (table.insert file.references {:from ast :to (find-reference name scope)}))) + + (fn fn* [ast scope] + (match ast + (where [-fn- name args] + (and (fennel.sym? name) + (not (multisym? name)) + (table? args))) + (tset (. scope-notes scope.parent) (tostring name) ast))) + ;; (each [_ argument (ipairs args)])) + + (fn define [definition binding scope] + "called whenever a local variable or destructure statement is introduced" + (when (fennel.sym? binding) ;; for now, I am going to bury my head in the sand and ignore destructure logic + (tset (. scope-notes scope) (tostring binding) binding))) + + (local plugin + {:name "fennel-ls" + :versions ["1.2.0"] + :symbol-to-expression reference + : call + :fn fn* + :destructure define}) + + (fennel.compileString file.text + {:filename file.uri + :plugins [plugin]})) + +{: analyze} diff --git a/src/fennel-ls/state.fnl b/src/fennel-ls/state.fnl index 86fa71f..fc86efc 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)) -(λ analyze [] "TODO") +(local {: analyze} (require :fennel-ls.plugin)) (λ init-state [self params] (set self.files {}) diff --git a/src/fennel-ls/the-actual-code.fnl b/src/fennel-ls/the-actual-code.fnl index 7d3f26d..9ba61e9 100644 --- a/src/fennel-ls/the-actual-code.fnl +++ b/src/fennel-ls/the-actual-code.fnl @@ -68,39 +68,19 @@ Every time the client sends a message, it gets handled by a function in the corr (local local* (fennel.sym :local)) (λ requests.textDocument/definition [self send {: position :textDocument {: uri}}] (local file (state.get-by-uri self uri)) - - (set file.ast (or file.ast - (parser.from-fennel (. self.files uri)))) - (local ast file.ast) - (local byte (util.pos->byte file.text position.line position.character)) + (accumulate [result nil + _ reference (ipairs file.references) &until (or result (parser.past? reference.from byte))] + (if (parser.contains? reference.from byte) + (match reference + {: from : to} + {:range (parser.range file.text to) + :uri file.uri} - (var result nil) - (λ check [ast] - (log (fennel.view ast)) - (each [_ item (ipairs ast) :until (or result (parser.past? item byte))] - (log (fennel.view ast)) - (if (parser.contains? item byte) - (match item - (where [require* module &as l] - (and (fennel.list? l) - (string? module))) - (set result module) - (where [local* _ [require* module &as l1] &as l2] - (and (fennel.list? l1) - (fennel.list? l2) - (string? module))) - (set result module) - (where obj (fennel.list? obj)) - (check obj))))) - - (check ast) - - (if result - {:uri (mod.lookup self result) - :range {:start {:line 0 :character 0} - :end {:line 0 :character 0}}})) - + {: from : to-other-module} + {:range {:start {:line 0 :character 0} + :end {:line 0 :character 0}} + :uri (mod.lookup self (. to-other-module 1))})))) (λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}] (local file (state.get-by-uri self uri)) diff --git a/test/goto-definition-test.fnl b/test/goto-definition-test.fnl index 5364559..7c40b5d 100644 --- a/test/goto-definition-test.fnl +++ b/test/goto-definition-test.fnl @@ -35,16 +35,15 @@ (dispatch.handle* state (request-definition-at 1 5 "example.fnl")) [{:jsonrpc "2.0" :id 2 :result {: uri :range {:start {:line 0 :character 0} - :end {:line 0 :character 0}}}}]))) + :end {:line 0 :character 0}}}}])) - ;; TODO - ;; (it "can go to a fn" - ;; (local uri (.. ROOT-URI "/" "example.fnl")) - ;; (assert-matches - ;; (dispatch.handle* state (request-definition-at 8 2 "example.fnl")) - ;; [{:jsonrpc "2.0" :id 2 - ;; :result {: uri :range {:start {:line 4 :character 0} - ;; :end {:line 6 :character 17}}}}]))) + (it "can go to a fn" + (local uri (.. ROOT-URI "/" "example.fnl")) + (assert-matches + (dispatch.handle* state (request-definition-at 9 3 "example.fnl")) + [{:jsonrpc "2.0" :id 2 + :result {: uri :range {:start {:line 4 :character 0} + :end {:line 7 :character 19}}}}]))) ;; (it "can open a require with a custom fennelpath") ;; (it "can go to a fn") diff --git a/test/test-project/example.fnl b/test/test-project/example.fnl index 4754d0f..41effc4 100644 --- a/test/test-project/example.fnl +++ b/test/test-project/example.fnl @@ -4,7 +4,8 @@ (fn bar [a b] (print a b) - (foo.my-export)) + (local c 10) + (foo.my-export c)) (bar 1 2) diff --git a/test/test-project/foo.fnl b/test/test-project/foo.fnl index 81d2e5c..e866187 100644 --- a/test/test-project/foo.fnl +++ b/test/test-project/foo.fnl @@ -1,5 +1,6 @@ (local constant 5) -(fn my-export []) +(fn my-export [a] + a) {: my-export : constant}