From 42850a76e0668a48a573dc40d3cc83d121b270c8 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Tue, 2 Aug 2022 22:54:29 -0500 Subject: [PATCH] Refactoring --- src/fennel-ls/parser.fnl | 8 ++- src/fennel-ls/plugin.fnl | 79 ++++++++++++++++----------- src/fennel-ls/the-actual-code.fnl | 1 + test/goto-definition-test.fnl | 89 ++++++++++++++++--------------- test/lsp-test.fnl | 2 - test/macros.fnl | 5 +- test/test-project/example.fnl | 9 ++++ 7 files changed, 112 insertions(+), 81 deletions(-) diff --git a/src/fennel-ls/parser.fnl b/src/fennel-ls/parser.fnl index 6d312f9..664e1b4 100644 --- a/src/fennel-ls/parser.fnl +++ b/src/fennel-ls/parser.fnl @@ -9,6 +9,8 @@ (fn 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 (get-ast-info ast :byteend)))) @@ -16,7 +18,9 @@ (fn past? [ast byte] "check if a byte is past the range of the AST object" (and (= (type ast) :table) - (< byte (get-ast-info ast :bytestart)))) + (get-ast-info ast :bytestart) + (< byte (get-ast-info ast :bytestart)) + false)) (fn range [text ast] "create a LSP range representing the span of an AST object" @@ -24,7 +28,7 @@ (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)] + (end-line end-col) (util.byte->pos text (+ j 1))] {:start {:line start-line :character start-col} :end {:line end-line :character end-col}})))) diff --git a/src/fennel-ls/plugin.fnl b/src/fennel-ls/plugin.fnl index 65b5739..9ea4ec9 100644 --- a/src/fennel-ls/plugin.fnl +++ b/src/fennel-ls/plugin.fnl @@ -4,17 +4,18 @@ ;; 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)) +(local -λ- (fennel.sym :λ)) +(local -lambda- (fennel.sym :lambda)) ;; 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? [t] (= :table (type t))) -(fn multisym? [t] +(λ multisym? [t] (and (fennel.sym? t) (let [t (tostring t)] (or (t:find "%.") @@ -30,54 +31,70 @@ (doto {} (setmetatable {:__index - (fn [self key] + (λ [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)))) + (λ 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] + (λ reference [ast scope] "called whenever a variable is referenced" (assert (fennel.sym? ast)) - (let [name (or (string.match (tostring ast) "[^%.:]+"))] + (let [name (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] + (λ 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))) + (λ define-function-name [ast scope] + (match ast + (where [_fn name args] + (and (fennel.sym? name) + (not (multisym? name)) ;; not dealing with multisym for now + (fennel.sequence? args))) + (tset (. scope-notes scope.parent) (tostring name) ast))) + + (λ define-function-args [ast scope] + (local args + (match ast + (where [_fn args] (fennel.sequence? args)) args + (where [_fn _name args] (fennel.sequence? args)) args)) + (each [_ argument (ipairs args)] + (define nil argument scope))) ;; we say arguments are bound to "nil" for now + + (λ define-function [ast scope] + "Introduces the various symbols exported by a function. +This cannot be done through the :fn feature of the compiler plugin system, because it needs to be +called before the body of the function happens" + (define-function-name ast scope) + (define-function-args ast scope)) + + (λ call [ast scope] + "called for every function call. Most calls aren't interesting, but (require) and (local) are" + (match ast + (where [-require- mod] (= :string (type mod))) + (insert file.references {:from ast :to-other-module [mod]}) + [-fn-] + (define-function ast scope) + [-λ-] + (define-function ast scope) + [-lambda-] + (define-function ast scope))) + (local plugin {:name "fennel-ls" :versions ["1.2.0"] :symbol-to-expression reference : call - :fn fn* :destructure define}) - (fennel.compileString file.text + (pcall fennel.compileString file.text {:filename file.uri :plugins [plugin]})) diff --git a/src/fennel-ls/the-actual-code.fnl b/src/fennel-ls/the-actual-code.fnl index 9ba61e9..2c1a6d7 100644 --- a/src/fennel-ls/the-actual-code.fnl +++ b/src/fennel-ls/the-actual-code.fnl @@ -69,6 +69,7 @@ Every time the client sends a message, it gets handled by a function in the corr (λ 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)) + (log file) (accumulate [result nil _ reference (ipairs file.references) &until (or result (parser.past? reference.from byte))] (if (parser.contains? reference.from byte) diff --git a/test/goto-definition-test.fnl b/test/goto-definition-test.fnl index 7c40b5d..ebdf0c3 100644 --- a/test/goto-definition-test.fnl +++ b/test/goto-definition-test.fnl @@ -10,57 +10,58 @@ (describe "jump to definition" - (var state nil) - - (before-each - (set state []) - (setup-server state)) - - (fn request-definition-at [line char file] - (message.create-request 2 "textDocument/definition" - {:position {:character char :line line} - :textDocument {:uri (.. ROOT-URI "/" file)}})) + (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 + (message.create-request 2 "textDocument/definition" + {:position {:character char :line line} + :textDocument {:uri (.. ROOT-URI "/" request-file)}})) + uri (.. ROOT-URI "/" response-file)] + (assert-matches + message + [{:jsonrpc "2.0" :id 2 + :result {: uri + :range {:start {:line start-line :character start-col} + :end {:line end-line :character end-col}}}}] + (.. "expected position: " start-line " " start-col " " end-line " " end-col)))) (it "handles (local _ (require XXX)" - (local uri (.. ROOT-URI "/" "foo.fnl")) - (assert-matches - (dispatch.handle* state (request-definition-at 0 11 "example.fnl")) - [{:jsonrpc "2.0" :id 2 - :result {: uri :range {:start {:line 0 :character 0} - :end {:line 0 :character 0}}}}])) + (check "example.fnl" 0 11 "foo.fnl" 0 0 0 0)) (it "handles (require XXX))" - (local uri (.. ROOT-URI "/" "bar.fnl")) - (assert-matches - (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}}}}])) + (check "example.fnl" 1 5 "bar.fnl" 0 0 0 0)) (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}}}}]))) + ;; TODO maybe it's better to just go to the name of the function, not the whole list + (check "example.fnl" 9 3 "example.fnl" 4 0 7 20)) - ;; (it "can open a require with a custom fennelpath") - ;; (it "can go to a fn") - ;; (it "can go to a local") - ;; (it "can go to a table and its field") - ;; (it "can go to a destructured local") - ;; (it "can go to a table field in another file") - ;; (it "can go to a table field in another file (through a destructuring assignment)") - ;; (it "can go to a field in a lua file") - ;; (it "finds the definition of macros") - ;; (it "can go through more than one extra file") - ;; (it "will give up on recursive requires") - ;; (it "can follow import-macros") + (it "can go to a local" + (check "example.fnl" 7 17 "example.fnl" 6 9 6 10)) - ;; (describe "diagnostic") - ;; (it "reports compiler errors") - ;; (it "reports lint warnings") + (it "can go to a function argument" + (check "example.fnl" 5 9 "example.fnl" 4 9 4 10)) - ;; (describe "completion") + (it "can handle variables shadowed with let" + (check "example.fnl" 14 10 "example.fnl" 13 6 13 9)) + (it "can sort out the unification rule with match (variable unified)" + (check "example.fnl" 19 12 "example.fnl" 17 8 17 9)) + + (it "can sort out the unification rule with match (variable introduced)" + (check "example.fnl" 20 12 "example.fnl" 20 9 20 10))) + + ;; (it "doesn't have ghost definitions from the same byte ranges as the macro files it's using") + ;; (it "can go to a reference that occurs in a macro") + ;; (it "can go to a function inside a table") + ;; (it "can go to an field inside of a table") + ;; (it "can go to a destructured local") + ;; (it "can go to a destructured function argument") + ;; (it "can go to a function in another file when accessed by multisym") + ;; (it "can go to a function in another file imported via destructuring assignment") + ;; (it "can work with a custom fennelpath") + ;; (it "can go through more than one extra file") + ;; (it "will give up instead of freezing on recursive requires") + ;; (it "does slightly better in the presense of macros") + ;; (it "finds the definition of macros") + ;; (it "can follow import-macros") + ;; (it "can go to the definition even in a lua file") diff --git a/test/lsp-test.fnl b/test/lsp-test.fnl index ec2b6de..9da9447 100644 --- a/test/lsp-test.fnl +++ b/test/lsp-test.fnl @@ -4,7 +4,6 @@ (local {: ROOT-PATH : ROOT-URI} (require :test.util)) (local dispatch (require :fennel-ls.dispatch)) - (local server-initialize-message {:id 1 :jsonrpc "2.0" @@ -27,4 +26,3 @@ [{:jsonrpc "2.0" :id 1 :result {:capabilities {} :serverInfo {:name "fennel-ls" : version}}}]))) - diff --git a/test/macros.fnl b/test/macros.fnl index 3e9606f..6185223 100644 --- a/test/macros.fnl +++ b/test/macros.fnl @@ -16,7 +16,7 @@ (fn [] ,...))) -(fn assert-matches [item pattern] +(fn assert-matches [item pattern ?msg] "check if item matches a pattern according to fennel's `match` builtin" `(match ,item ,pattern nil @@ -26,7 +26,8 @@ (let [fennel# (require :fennel)] (fennel#.view ?otherwise#)) "\ndid not match pattern:\n" - ,(view pattern))))) + ,(view pattern) + (and ,?msg (.. "\n" ,?msg)))))) {: it : describe diff --git a/test/test-project/example.fnl b/test/test-project/example.fnl index 41effc4..78eb634 100644 --- a/test/test-project/example.fnl +++ b/test/test-project/example.fnl @@ -11,4 +11,13 @@ (print bazfn) +(let [bar "shadowed"] + (print bar)) + +(λ test [{: foo}] + (let [a 10] + (match [0 10] + [1 a] a + [0 b] b))) + {: bar}