adjust parents, and take advantage of hooks for better completions

This commit is contained in:
XeroOl 2022-09-01 01:03:59 -05:00
parent b9d084fada
commit 89eb07c064
No known key found for this signature in database
GPG Key ID: 9DD4B4B4DAED0322
6 changed files with 98 additions and 44 deletions

View File

@ -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)

View File

@ -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]

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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