diff --git a/src/fennel-ls/compiler.fnl b/src/fennel-ls/compiler.fnl index 3aee418..738838d 100644 --- a/src/fennel-ls/compiler.fnl +++ b/src/fennel-ls/compiler.fnl @@ -103,9 +103,15 @@ later by fennel-ls.language to answer requests from the client." (λ define-function [ast scope] ;; handle the definitions of a function - (define-function-name ast scope) + (define-function-name ast scope)) + + (λ compile-fn [ast scope] + (tset scopes ast scope) ;; update scope (define-function-args ast scope)) + (λ compile-do [ast scope] + (tset scopes ast scope)) ;; update scope + (λ call [ast scope] (tset scopes ast scope) ;; Most calls aren't interesting, but here's the list of the ones that are: @@ -147,12 +153,14 @@ later by fennel-ls.language to answer requests from the client." (let [plugin {:name "fennel-ls" - :versions ["1.2.0"] + :versions ["1.3.0"] :symbol-to-expression reference :call call :destructure define :assert-compile on-compile-error - :parse-error on-parse-error} + :parse-error on-parse-error + :customhook-early-do compile-do + :customhook-early-fn compile-fn} scope (fennel.scope) opts {:filename file.uri :plugins [plugin] @@ -171,6 +179,7 @@ later by fennel-ls.language to answer requests from the client." ;; write things back to the file object (set file.ast ast) + (set file.scope scope) (set file.scopes scopes) (set file.definitions definitions) (set file.diagnostics diagnostics) diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index 01510b2..2ca0636 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -72,7 +72,7 @@ Every time the client sends a message, it gets handled by a function in the corr (match-try (language.find-symbol file.ast byte) (symbol parents) (match-try - (let [parent (. parents (length parents))] + (let [parent (. parents 1)] (if (. file.require-calls parent) (language.search self file parent []))) nil @@ -91,19 +91,32 @@ Every time the client sends a message, it gets handled by a function in the corr result {:contents {:kind "markdown" :value (formatter.hover-format result)}}))) +(λ collect-scope [scope typ callback ?target] + (let [result (or ?target [])] + (var scope scope) + (while scope + (icollect [i v (pairs (. scope typ)) &into result] + (callback i v)) + (set scope scope.parent)) + result)) + +(λ find-things-in-scope [file parents typ callback ?target] + (let [scope (or (accumulate [result nil + i parent (ipairs parents) + &until result] + (. file.scopes parent)) + file.scope)] + (collect-scope scope typ callback ?target))) + (λ requests.textDocument/completion [self send {: position :textDocument {: uri}}] (let [file (state.get-by-uri self uri) byte (pos->byte file.text position.line position.character) (?symbol parents) (language.find-symbol file.ast byte)] - ;; TODO build a recursive searcher, - ;; store file scopes parents length parents as an intermediate - ;; potentially reverse parents order so it's only (. parents 1) - (let [begin (if (?. file.scopes (. parents (length parents))) - (icollect [k (pairs (. file.scopes (. parents (length parents)) :manglings))] - {:label k}) - [])] - (icollect [_ k (ipairs file.allowed-globals) &into begin] {:label k})))) - + (let [result []] + (find-things-in-scope file parents :manglings #{:label $} result) + (find-things-in-scope file parents :macros #{:label $} result) + (find-things-in-scope file parents :specials #{:label $} result) + (icollect [_ k (ipairs file.allowed-globals) &into result] {:label k})))) (λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}] (local file (state.get-by-uri self uri)) @@ -120,7 +133,7 @@ Every time the client sends a message, it gets handled by a function in the corr (set file.open? false)) (λ requests.shutdown [self send] - "The server still needs to respond to this request, so the program can't close yet. Wait until notifications.exit" + "The server still needs to respond to this request, so the program can't close yet. Just wait until notifications.exit" nil) (λ notifications.exit [self] diff --git a/src/fennel-ls/language.fnl b/src/fennel-ls/language.fnl index 61ece67..2784f80 100644 --- a/src/fennel-ls/language.fnl +++ b/src/fennel-ls/language.fnl @@ -142,7 +142,8 @@ the data provided by compiler.fnl." (accumulate [result nil i top-level-form (ipairs ast) &until result] (if (contains? top-level-form byte) (recurse top-level-form byte))) - parents)) + (fcollect [i 1 (length parents)] + (. parents (- (length parents) i -1))))) {: find-symbol diff --git a/src/fennel-ls/utils.fnl b/src/fennel-ls/utils.fnl index c088ab9..25bc92a 100644 --- a/src/fennel-ls/utils.fnl +++ b/src/fennel-ls/utils.fnl @@ -79,7 +79,8 @@ These functions are all pure functions, which makes me happy." (local symbol (tostring symbol)) (if (or (= symbol ".") (= symbol "..") - (= symbol "...")) + (= symbol "...") + (= symbol "?.")) [symbol] (let [offset (or ?offset (length symbol)) next-separator (or (symbol:find ".[.:]" offset) diff --git a/test/completion-test.fnl b/test/completion-test.fnl index 24cbbee..dfae9dc 100644 --- a/test/completion-test.fnl +++ b/test/completion-test.fnl @@ -12,36 +12,66 @@ (local filename (.. ROOT-URI "imaginary-file.fnl")) +(fn check-completion [body line col expected ?unexpected] + (local state (doto [] setup-server)) + (open-file state filename body) + (let [response (dispatch.handle* state (completion-at filename line col)) + seen (collect [_ suggestion (ipairs (. response 1 :result))] + suggestion.label suggestion.label)] + (each [_ exp (ipairs expected)] + (is.truthy (. seen exp) (.. exp " was not suggested, but should be"))) + (if ?unexpected + (each [_ exp (ipairs ?unexpected)] + (is.nil (. seen exp) (.. exp " was suggested, but shouldn't be")))))) + (describe "completions" (it "suggests globals" - (local state (doto [] setup-server)) - ;; empty file - (open-file state filename "(") - (let [response (dispatch.handle* state (completion-at filename 0 1))] - ;; TODO fix this test. Write a helper that will search a table and ensure at least one value matches. - (is-matching response - (where - [{:result - [{:label a} - {:label b} - {:label c}]}] - (. _G a) - (. _G b) - (. _G c)) - "oops"))) + (check-completion "(" 0 1 [:_G :debug :table :io :getmetatable :setmetatable :_VERSION :ipairs :pairs :next])) (it "suggests locals in scope" - (local state (doto [] setup-server)) - (open-file state filename "(local x 10)\n(print )") - (let [response (dispatch.handle* state (completion-at filename 1 7))] - (var seen-suggestion false) - (each [_ suggestion (ipairs (. response 1 :result))] - (if (= suggestion.label :x) - (set seen-suggestion true))) - (assert seen-suggestion "x was not suggested")))) + (check-completion "(local x 10)\n(print )" 1 7 [:x])) - ;; (it "treats things in a call position differently") + (it "suggests locals in scope at the top level" + (check-completion "(local x 10)\n\n" 1 0 [:x])) + + (it "suggests more locals in scope" + (check-completion "(let [x 10] (let [y 100] \n nil\n ))" 2 4 [:x :y])) + + (it "suggests specials and macros at beginning of list" + (check-completion "()" 0 1 [:do :let :fn :doto :-> :-?>> :?.]) + (check-completion "(d)" 0 3 [:do :doto])) + + (it "suggests macros in scope" + (check-completion "(macro funny [] `nil)\n()" 1 1 [:funny]))) + + ;; ;; Compiler hardening + ;; (it "works without requiring the close parentheses")) + ;; (it "works without a body in the `let`")) ;; (it "does not suggest locals out of scope") + ;; (it "suggests items from the previous definitions in the same `let`") + + ;; ;; Functions + ;; (it "suggests function arguments at the top scope of the function") + ;; (it "suggests function arguments deep within the function") + + ;; ;; Scope Ordering Rules + ;; (it "does not suggest locals past the suggestion location when a symbol is partially typed") + ;; (it "does not suggest locals past the suggestion location without a symbol") + ;; (it "does not suggest locals past the suggestion point at the top level") + ;; (it "does not suggest items from later definitions in the same `let`") + ;; (it "does not suggest macros defined from later definitions") + + ;; ;; Call ordering rules + + ;; (it "doesn't suggest specials in the middle of a list (open paren required)" + ;; (check-completion "(do )" + ;; 0 4 [] [:do :let :fn :-> :-?>> :?.]))) + + + ;; (it "doesn't suggest specials at the very top level") + ;; (it "doesn't suggest macros in the middle of a list (open paren required)") + ;; (it "doesn't suggest macros at the very top level") + ;; (it "suggests fields of tables") ;; (it "suggests known fn fields of tables when using a method call multisym") ;; (it "suggests known fn keys when using the `:` special") diff --git a/test/misc-test.fnl b/test/misc-test.fnl index 0fc9f4e..b633ffe 100644 --- a/test/misc-test.fnl +++ b/test/misc-test.fnl @@ -29,6 +29,8 @@ (is.same ["a" "b" "c" "d" "e" "f"] (utils.multi-sym-split "a.b.c.d.e.f")) (is.same ["obj" "bar"] (utils.multi-sym-split (fennel.sym "obj.bar"))))) +(describe "utf8") ;; TODO + (describe "find-symbol" (it "finds a symbol and parents" (local state (doto [] setup-server)) @@ -38,7 +40,7 @@ (is.equal symbol (fennel.sym :sym-one)) (is-matching ;; awful way to check AST equality, but I don't mind - parents [[[:match] [1 2 4] [1 2 [:sym-one]] [:sym-one]] [1 2 [:sym-one]]] + parents [[1 2 [:sym-one]] [[:match] [1 2 4] [1 2 [:sym-one]] [:sym-one]]] "bad parents")) (it "finds nothing, but still gives parents" @@ -48,7 +50,5 @@ (local (symbol parents) (language.find-symbol file.ast 18)) (is.equal symbol nil) (is-matching - parents [[[:match] [1 2 4] [1 2 [:sym-one]] [:sym-one]] [1 2 [:sym-one]]] + parents [[1 2 [:sym-one]] [[:match] [1 2 4] [1 2 [:sym-one]] [:sym-one]]] "bad parents"))) - -;; TODO parents for failed forms