From 3f1c2ea3616a0e9d936457933214a60771cc7a05 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Wed, 23 Apr 2025 22:38:46 -0500 Subject: [PATCH] clean up some tests, restore (fn M.method []) completion handling --- changelog.md | 1 + src/fennel-ls/analyzer.fnl | 28 ++++++++++++----- src/fennel-ls/completions.fnl | 59 ++++++++++++++++++++--------------- test/completion.fnl | 46 ++++++++------------------- 4 files changed, 68 insertions(+), 66 deletions(-) diff --git a/changelog.md b/changelog.md index b885f9b..1c2700c 100644 --- a/changelog.md +++ b/changelog.md @@ -20,6 +20,7 @@ * Ignore unused locals if they end in underscore. * Settings file: `flsproject.fnl`. Settings are now editor agnostic. * Support `:intersection` as a Lua version; only includes globals present in every Lua. +* Support better completions. The eglot client is no longer a special case. ### Changes diff --git a/src/fennel-ls/analyzer.fnl b/src/fennel-ls/analyzer.fnl index c116eda..87be246 100644 --- a/src/fennel-ls/analyzer.fnl +++ b/src/fennel-ls/analyzer.fnl @@ -83,14 +83,26 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find :keys ?keys :multival ?multival :fields ?fields}} assignment] - (when (and (= 0 (length stack)) - opts.save-last-binding) - (tset opts.save-last-binding 1 assignment.target)) (if (and (= 0 (length stack)) opts.stop-early?) assignment.target ;; BASE CASE!! - ;; search a virtual field from :fields + + ;; :fields (and (not= 0 (length stack)) (?. ?fields (. stack (length stack)))) (search-assignment server file {:target (. ?fields (table.remove stack))} stack opts) + + ;; This case is hard to explain. + ;; Under these conditions, search-multival is planning on returning {:definition ?definition : file} + ;; but assignment.target is more rich with information. + ;; Specifically, it has the :fields key, which is useful in finding non-local fields. + ;; For example, The `my-method` field of `M` in `(local M {})\n(fn M.my-method [])` + ;; is shared using this :fields mechanism. + (and (not (list? ?definition)) + (not (varg? ?definition)) + (= 1 (or 1 ?multival)) + (not (sym? ?definition)) + (= 0 (length stack))) + assignment.target + (search-multival server file ?definition (stack-add-keys! stack ?keys) (or ?multival 1) opts)))) (λ search-reference [server file ref stack opts] @@ -212,10 +224,10 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find opts (or ?opts {})] (case (docs.get-builtin server (. split 1)) metadata (search-document server metadata stack opts) - _ (case (docs.get-global server (. split 1)) - metadata (search-document server metadata stack opts) - _ (case (find-local-definition file name scope) - def (search-val server file def.definition (stack-add-keys! stack def.keys) opts)))))) + _ (case (find-local-definition file name scope) + def (search-val server file def.definition (stack-add-keys! stack def.keys) opts) + _ (case (docs.get-global server (. split 1)) + metadata (search-document server metadata stack opts)))))) (λ past? [?ast byte] ;; check if a byte is past an ast object diff --git a/src/fennel-ls/completions.fnl b/src/fennel-ls/completions.fnl index 29eeec4..70ad4aa 100644 --- a/src/fennel-ls/completions.fnl +++ b/src/fennel-ls/completions.fnl @@ -29,7 +29,7 @@ file.scope) range (if ?symbol (message.ast->range server file ?symbol) {:start position :end position}) results [] - seen []] + seen {}] (fn add-completion [definition name] (table.insert results @@ -53,45 +53,54 @@ (and (fennel.list? def.definition) ;; TODO check that arg is called `self` (or (and (fennel.sym? (. def.definition 1) "fn") - (fennel.sym? (. def.definition 2) "self")) + (fennel.sym? (?. def.definition 2 1) "self")) (and (fennel.sym? (. def.definition 1) "λ") - (fennel.sym? (. def.definition 2) "self"))))) + (fennel.sym? (?. def.definition 2 1) "self"))))) (add-completion-recursively def (.. name ":" field)) (add-completion-recursively def (.. name "." field))) _ (do (io.stderr:write "BAD!!!! undocumented field: " (tostring field) "\n") {:label field}))))) (when definition.fields - (each [field value (pairs definition.fields)] - (when (= (type field) :string) - (if (or (= :self (tostring (?. value :metadata :fnl/arglist 1))) - (and (fennel.list? value.definition) - ;; TODO check that arg is called `self` - (or (and (fennel.sym? (. value.definition 1) "fn") - (fennel.sym? (. value.definition 2) "self")) - (and (fennel.sym? (. value.definition 1) "λ") - (fennel.sym? (. value.definition 2) "self"))))) - (add-completion-recursively value (.. name ":" field)) - (add-completion-recursively value (.. name "." field)))))) + (each [field value (pairs definition.fields)] + (when (= (type field) :string) + (if (or (= :self (tostring (?. value :metadata :fnl/arglist 1))) + (and (fennel.list? value.definition) + ;; TODO check that arg is called `self` + (or (and (fennel.sym? (. value.definition 1) "fn") + (fennel.sym? (?. value.definition 2 1) "self")) + (and (fennel.sym? (. value.definition 1) "λ") + (fennel.sym? (?. value.definition 2 1) "self"))))) + (add-completion-recursively value (.. name ":" field)) + (add-completion-recursively value (.. name "." field)))))) (set (. seen definition) false))) ;; end yield + (local seen-manglings {}) + (each [_ global* (ipairs file.allowed-globals)] - (case (analyzer.search-name-and-scope server file global* scope) - def (if (and (= :_G (tostring global*)) - (not (: (tostring ?symbol) :match "_G[:.]"))) - (add-completion def global*) - (add-completion-recursively def global*)) - _ (do - (io.stderr:write "BAD!!!! undocumented global: " (tostring global*) "\n") - {:label global*}))) + (when (not (. seen-manglings global*)) + (set (. seen-manglings global*) true) + (case (analyzer.search-name-and-scope server file global* scope) + def (if (and (= :_G (tostring global*)) + (not (: (tostring ?symbol) :match "_G[:.]"))) + (add-completion def global*) + (add-completion-recursively def global*)) + _ (do + (io.stderr:write "BAD!!!! undocumented global: " (tostring global*) "\n") + {:label global*})))) + + (var scope scope) (while scope (each [mangling (pairs scope.manglings)] - (case (analyzer.search-name-and-scope server file mangling scope) - def (add-completion-recursively def mangling) - _ (add-completion-recursively {} mangling))) + (when (not (. seen-manglings mangling)) + (set (. seen-manglings mangling) true) + (case (analyzer.search-name-and-scope server file mangling scope) + def (add-completion-recursively def mangling) + _ (add-completion-recursively {} mangling)))) + (when in-call-position? (each [macro* (pairs scope.macros)] (table.insert results {:label macro* :filterText macro* :textEdit {:newText macro* : range}})) diff --git a/test/completion.fnl b/test/completion.fnl index 144e1ab..dd56ca0 100644 --- a/test/completion.fnl +++ b/test/completion.fnl @@ -154,29 +154,29 @@ nil) (fn test-field [] - (check "(local x {:field (fn [])})\n(x:" [:x:field] []) + (check "(local x {:field (fn [self])})\n(x:" [:x:field] []) + (check "(local x {:field (fn [self])})\n(x:fi|" [:x:field] []) ;; regression test for not crashing - (check "(local x {:field (fn [])})\n(x::f" [] []) + (check "(local x {:field (fn [self])})\n(x::f" [] []) (check "(let [my-table {:foo 10 :bar 20}]\n my-table.|)))" - [:foo :bar] - [:local :doto :+]) ;; no specials or macros because it's not in a head position + [:my-table.foo :my-table.bar] + []) (check {:main.fnl "(let [foo (require :fooo)] foo.|)))" :fooo.fnl "(fn my-export [x] (print x)) {: my-export :constant 10}"} - [:my-export :constant] - [:_G :local :doto :+]) + [:foo.my-export :foo.constant] + []) (check {:main.fnl "(let [foo (require :fooo)] foo.|)))" :fooo.fnl "(local M {:constant 10}) (fn M.my-export [x] (print x)) M"} - [:my-export :constant] - [:_G :local :doto :+]) ;; no globals, specials, macros, or others - (check "(local x {:field (fn [])})\n(x:fi|" [:field] [:table]) + [:foo.my-export :foo.constant] + []) nil) (fn test-docs [] @@ -231,12 +231,11 @@ (check "(coroutine.y|" [{:label "yield" :documentation #(and $.value ($.value:find "```fnl\n(coroutine.yield ...)\n```" 1 true))}] - ["coroutine" "_G" "do" - {:documentation #(= nil $)}]) + [{:documentation #(= nil $)}]) (check "(local c coroutine) (c.y" - [{:label "yield"}] - ["coroutine" "_G" "do"]) + ["coroutine.yield" "c.yield"] + [{:documentation #(= nil $)}]) (check "(local t table) (t.i" ["insert"] @@ -248,24 +247,6 @@ nil) -(fn test-eglot-fields [] - "tests for handling Eglot specially" - (check "(coroutine.y|" - [{:label "coroutine.yield" - :filterText "coroutine.yield" - :documentation #(and $.value ($.value:find "```fnl\n(coroutine.yield ...)\n```" 1 true))}] - []) - (check "(local c coroutine) - (c.y" - [{:label "c.yield" - :filterText "c.yield" - :textEdit #(= nil $)}] - []) - (check "(local x {:field (fn [])}) - (x:" - [:x:field] - [])) - ;; ;; Future tests / features ;; ;; Scope Ordering Rules ;; (it "does not suggest locals past the suggestion location when a symbol is partially typed") @@ -292,5 +273,4 @@ : test-fn-arg : test-field : test-docs - : test-module - : test-eglot-fields} + : test-module}