fennel-ls/test/completion-test.fnl
XeroOl 364d02b90d
Better completions for fields of tables
Now it does a two-level-deep search when creating competions for tables,
which means that a completion for module fields have better metadata.
the completion code is a bit of a mess, so I want to look into
refactoring it soon.
2023-09-03 10:23:33 -05:00

232 lines
11 KiB
Fennel

(import-macros {: is-matching : is-casing : describe : it : before-each} :test)
(local is (require :test.is))
(local {: view} (require :fennel))
(local {: ROOT-URI
: create-client} (require :test.client))
(local filename (.. ROOT-URI "/imaginary-file.fnl"))
(fn check-completion [body line col expected ?unexpected]
(let [client (doto (create-client)
(: :open-file! filename body))
[{: result}] (client:completion filename line col)
seen (if result
(collect [_ suggestion (ipairs result)]
suggestion.label suggestion.label))]
(if expected
(each [_ exp (ipairs expected)]
(is (. 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"
(check-completion "(" 0 1 [:_G :debug :table :io :getmetatable :setmetatable :_VERSION :ipairs :pairs :next])
(check-completion "#nil\n(" 1 1 [:_G :debug :table :io :getmetatable :setmetatable :_VERSION :ipairs :pairs :next]))
(it "suggests locals in scope"
(check-completion "(local x 10)\n(print )" 1 7 [:x]))
(it "suggests locals where the definition can't be found"
(check-completion "(local x (doto 10 or and +))\n(print )" 1 7 [:x]))
(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 :-> :-?>> :?.])
;; it's not the language server's job to do filtering,
;; so there's no negative assertions here for other symbols
(check-completion "(d)" 0 2 [:do :doto]))
(it "suggests macros in scope"
(check-completion "(macro funny [] `nil)\n()" 1 1 [:funny]))
(it "does not suggest locals out of scope"
(check-completion "(do (local x 10))\n" 1 0 [] [:x]))
(it "does not suggest function args out of scope"
(check-completion "(fn [x] (print x))\n" 1 0 [] [:x])
(check-completion "(fn [x] (print x))\n(print " 1 7 [] [:x]))
(describe "When the program doesn't compile"
(it "still completes without requiring the close parentheses"
(check-completion "(fn foo [z]\n (let [x 10 y 20]\n " 2 4 [:x :y :z]))
(it "still completes with no body in the `let`"
(check-completion "(let [x 10 y 20]\n )" 1 2 [:x :y]))
(it "still completes with no body in the `let` and no close parentheses"
(check-completion "(local foo 10)\n(local x (let [y f]\n" 1 18 [:foo]))
(it "still completes items from the previous definitions in the same `let`"
(check-completion "(let [a 10\n b 20\n " 1 6 [:a :b]))
(it "completes fields with a partially typed multisym that ends in :"
(check-completion "(local x {:field (fn [])})\n(x:" 1 3 [:field] [:local]))
(it "doesn't crash with a partially typed multisym contains ::"
(check-completion "(local x {:field (fn [])})\n(x::f" 1 3 [])))
;; Functions
(it "suggests function arguments at the top scope of the function"
(check-completion "(fn foo [arg1 arg2 arg3]\n )" 1 2 [:arg1 :arg2 :arg3]))
(it "suggests function arguments at the top scope of the function"
(check-completion "(fn foo [arg1 arg2 arg3]\n (do (do (do ))))" 1 14 [:arg1 :arg2 :arg3]))
;; ;; 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"
(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"
(check-completion
"(let [my-table {:foo 10 :bar 20}]\n my-table.)))"
1 11
[:foo :bar]
[:_G :local :doto :1])) ;; no globals, specials, macros, or others
(it "suggests fields of tables indirectly"
(check-completion
"(let [foo (require :foo)]\n foo.)))"
1 6
[:my-export :constant]
[:_G :local :doto :1])) ;; no globals, specials, macros, or others
;; (it "suggests fields of strings"))
(it "suggests known fn fields of tables when using a method call multisym"
(check-completion "(local x {:field (fn [])})\n(x:fi" 1 5 [:field] [:table]))
(describe "metadata"
;; CompletionItemKind
(local kinds
{:Text 1 :Method 2 :Function 3 :Constructor 4 :Field 5 :Variable 6 :Class 7
:Interface 8 :Module 9 :Property 10 :Unit 11 :Value 12 :Enum 13 :Keyword 14
:Snippet 15 :Color 16 :File 17 :Reference 18 :Folder 19 :EnumMember 20
:Constant 21 :Struct 22 :Event 23 :Operator 24 :TypeParameter 25})
(it "offers rich information about function completions"
(let [client (doto (create-client)
(: :open-file! filename "(fn xyzzy [x y z] \"docstring\" nil)\n(xyzz"))
[{:result [completion]}] (client:completion filename 1 5)]
;; TODO this seems a little bit weird to assert
(is.same :xyzzy completion.label "the first completion should be xyzzy")
(assert completion.kind "completion kind should be present")
(assert completion.documentation "completion documentation should be present")))
(it "offers rich information about builtin/special completions"
(let [client (doto (create-client)
(: :open-file! filename "("))
[{:result completions}] (client:completion filename 0 1)
completion (accumulate [item nil _ completion (ipairs completions) &until item] (if (= completion.label :local) completion))]
(is-casing
completion
(where
{:label :local
:kind (= kinds.Operator)
:documentation documentation}
(not= documentation :nil)))))
(it "offers rich information about builtin-macro completions"
(let [client (doto (create-client)
(: :open-file! filename "("))
[{:result completions}] (client:completion filename 0 1)
completion (accumulate [item nil _ completion (ipairs completions) &until item] (if (= completion.label :-?>) completion))]
(is-casing
completion
(where
{:label :-?>
:kind (= kinds.Keyword)
:documentation documentation}
(not= documentation :nil)))))
(it "offers rich information about all builtin/globals"
(let [client (doto (create-client)
(: :open-file! filename "("))
[{:result completions}] (client:completion filename 0 1)
_ (table.sort completions #(< $1.label $2.label))
missing-docs (icollect [_ completion (ipairs completions)]
(if (not (and (= (type completion.label) :string)
(= (type completion.kind) :number)
(= (type completion.documentation) :table)))
completion.label))
allowed-missing-docs {:lua true
:set-forcibly! true
;; TODO support other lua versions besides 5.4
:gcinfo true
:getfenv true
:setfenv true
:loadstring true
:module true
:newproxy true
:unpack true
:bit32 true
;; luajit
:bit true
:jit true
;; TODO remove these from fennel-ls when they become configurable
:love true
:vim true}]
(each [_ completion (ipairs completions)]
(when (not (. allowed-missing-docs completion.label))
(is.same (type completion.label) :string "unlabeled completion")
(is.same (type completion.kind) :number (.. completion.label " needs a kind"))
(is.same (type completion.documentation) :table (.. completion.label " needs documentation"))
(is.not.same completion.documentation :nil (.. completion.label " needs documentation"))))))
(it "offers rich information about fields"
(let [client (doto (create-client)
(: :open-file! filename "(let [x (fn x [a b c] \"\"\"docstring\"\"\" nil)\n t {: x}]\n (t."))
[{:result completions}] (client:completion filename 2 5)
_ (table.sort completions #(< $1.label $2.label))
missing-docs (icollect [_ completion (ipairs completions)]
(if (not (and (= (type completion.label) :string)
(= (type completion.kind) :number)
(= (type completion.documentation) :table)))
completion.label))
allowed-missing-docs {}]
(each [_ completion (ipairs completions)]
(when (not (. allowed-missing-docs completion.label))
(is.same (type completion.label) :string "unlabeled completion")
(is.same (type completion.kind) :number (.. completion.label " needs a kind"))
(is.same (type completion.documentation) :table (.. completion.label " needs documentation"))
(is.not.same completion.documentation :nil (.. completion.label " needs documentation"))))))))
;; (it "offers rich information about variable completions")
;; (it "offers rich information about field completions")
;; (it "offers rich information about method completions")
;; (it "offers rich information about module completions")
;; (it "offers rich information about macro-module completions")))
;; (it "suggests known fn keys when using the `:` special")
;; (it "suggests known keys when using the `.` special")
;; (it "suggests known module names in `require` and `include` and `import-macros` and `require-macros` and friends")
;; (it "knows the fields of the standard lua library.")
;; (it "suggests special forms for the call position of a list, but not other positions")
;; (it "does not suggest special forms for the \"call\" position when a list isn't actually a call, ie destructuring assignment")
;; (it "suggests keys when typing out destructuring, as in `(local {: typinghere} (require :mod))`")
;; (it "only suggests tables for `ipairs` / begin work on type checking system")