macro-path and metadata for builtin macros and a couple bug fixes

This commit is contained in:
XeroOl 2023-06-09 01:08:10 -05:00
parent d36a1ed183
commit 3c79f480a8
11 changed files with 121 additions and 38 deletions

View File

@ -6,6 +6,7 @@ later by fennel-ls.language to answer requests from the client."
(local {: sym? : list? : sequence? : sym : view &as fennel} (require :fennel)) (local {: sym? : list? : sequence? : sym : view &as fennel} (require :fennel))
(local message (require :fennel-ls.message)) (local message (require :fennel-ls.message))
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))
(local searcher (require :fennel-ls.searcher))
;; words surrounded by - are symbols, ;; words surrounded by - are symbols,
;; because fennel doesn't allow 'require in a runtime file ;; because fennel doesn't allow 'require in a runtime file
@ -40,7 +41,7 @@ later by fennel-ls.language to answer requests from the client."
(λ is-values? [?ast] (λ is-values? [?ast]
(and (list? ?ast) (= (sym :values) (. ?ast 1)))) (and (list? ?ast) (= (sym :values) (. ?ast 1))))
(λ compile [self file] (λ compile [{:configuration {: macro-path} : root-uri} file]
"Compile the file, and record all the useful information from the compiler into the file object" "Compile the file, and record all the useful information from the compiler into the file object"
;; The useful information being recorded: ;; The useful information being recorded:
(let [definitions-by-scope (doto {} (setmetatable has-tables-mt)) (let [definitions-by-scope (doto {} (setmetatable has-tables-mt))
@ -263,6 +264,12 @@ later by fennel-ls.language to answer requests from the client."
: scope} : scope}
parser (partial pcall (fennel.parser file.text file.uri opts)) parser (partial pcall (fennel.parser file.text file.uri opts))
ast (icollect [ok ok-2 ast parser &until (not (and ok ok-2))] ast)] ast (icollect [ok ok-2 ast parser &until (not (and ok ok-2))] ast)]
;; This is bad; we mutate fennel.macro-path
(let [old-macro-path fennel.macro-path]
(set fennel.macro-path (searcher.add-workspaces-to-path macro-path [root-uri]))
;; compile ;; compile
(each [_i form (ipairs (if macro-file? (ast->macro-ast ast) ast))] (each [_i form (ipairs (if macro-file? (ast->macro-ast ast) ast))]
(case (xpcall #(fennel.compile form opts) fennel.traceback) (case (xpcall #(fennel.compile form opts) fennel.traceback)
@ -273,6 +280,8 @@ later by fennel-ls.language to answer requests from the client."
; {:range (message.pos->range 0 0 0 0) ; {:range (message.pos->range 0 0 0 0)
; :message (.. "unrecoverable compiler error: " err)}) ; :message (.. "unrecoverable compiler error: " err)})
(set fennel.macro-path old-macro-path))
; (each [sym target (pairs references)] ; (each [sym target (pairs references)]
; (if ; (if
; (sym? target) ; (sym? target)

View File

@ -1,3 +1,7 @@
"Diagnostics
Goes through a file and mutates the `file.diagnostics` field, filling it with diagnostics."
(local language (require :fennel-ls.language)) (local language (require :fennel-ls.language))
(local message (require :fennel-ls.message)) (local message (require :fennel-ls.message))
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))

View File

@ -137,7 +137,9 @@ Every time the client sends a message, it gets handled by a function in the corr
(λ make-completion-item [self file name scope] (λ make-completion-item [self file name scope]
;; TODO consider passing stop-early? ;; TODO consider passing stop-early?
(case (language.search-name-and-scope self file name scope) (case (language.search-name-and-scope self file name scope)
def (formatter.completion-item-format name def))) def (formatter.completion-item-format name def)
_ {:label name}))
(λ scope-completion [self file byte ?symbol parents] (λ scope-completion [self file byte ?symbol parents]
(let [scope (or (accumulate [result nil (let [scope (or (accumulate [result nil
@ -150,8 +152,8 @@ Every time the client sends a message, it gets handled by a function in the corr
in-call-position? (and ?parent (= ?symbol (. ?parent 1)))] in-call-position? (and ?parent (= ?symbol (. ?parent 1)))]
(collect-scope scope :manglings #(make-completion-item self file $ scope) result) (collect-scope scope :manglings #(make-completion-item self file $ scope) result)
(when in-call-position? (when in-call-position?
(collect-scope scope :macros #{:label $ :kind kinds.Keyword} result) (collect-scope scope :macros #(doto (make-completion-item self file $ scope) (tset :kind kinds.Keyword)) result)
(collect-scope scope :specials #(make-completion-item self file $ scope) result)) (collect-scope scope :specials #(doto (make-completion-item self file $ scope) (tset :kind kinds.Operator)) result))
(icollect [_ k (ipairs file.allowed-globals) &into result] (icollect [_ k (ipairs file.allowed-globals) &into result]
(make-completion-item self file k scope)))) (make-completion-item self file k scope))))
@ -195,9 +197,14 @@ Every time the client sends a message, it gets handled by a function in the corr
(send (message.diagnostics file)) (send (message.diagnostics file))
(set file.open? true)) (set file.open? true))
(λ notifications.textDocument/didSave [self send {:textDocument {: uri}}]
;; TODO be careful about which modules need to be recomputed, and also eagerly flush existing files
(tset (require :fennel) :macro-loaded []))
(λ notifications.textDocument/didClose [self send {:textDocument {: uri}}] (λ notifications.textDocument/didClose [self send {:textDocument {: uri}}]
(local file (state.get-by-uri self uri)) (local file (state.get-by-uri self uri))
(set file.open? false) (set file.open? false)
(tset (require :fennel) :macro-loaded [])
;; TODO only reload from disk if we didn't get a didSave, instead of always ;; TODO only reload from disk if we didn't get a didSave, instead of always
(state.flush-uri self uri)) (state.flush-uri self uri))

View File

@ -40,7 +40,6 @@ the data provided by compiler.fnl."
:fields ?fields} assignment] :fields ?fields} assignment]
(if (and (= 0 (length stack)) opts.stop-early?) (if (and (= 0 (length stack)) opts.stop-early?)
(values assignment file) ;; BASE CASE!! (values assignment file) ;; BASE CASE!!
;; search a virtual field from :fields ;; search a virtual field from :fields
(and (not= 0 (length stack)) (?. ?fields (. stack (length stack)))) (and (not= 0 (length stack)) (?. ?fields (. stack (length stack))))
(search-assignment self file (. ?fields (table.remove stack)) stack opts) (search-assignment self file (. ?fields (table.remove stack)) stack opts)
@ -67,19 +66,24 @@ the data provided by compiler.fnl."
(let [newitem (. newfile.ast (length newfile.ast))] (let [newitem (. newfile.ast (length newfile.ast))]
(search self newfile newitem stack (doto opts (tset :searched-through-require true)))))) (search self newfile newitem stack (doto opts (tset :searched-through-require true))))))
;; A . form indexes into item 1 with the other items ;; A . form indexes into item 1 with the other items
[-dot- & split] (where [-dot- & split] (. split 1))
(search self file (. split 1) (stack-add-split! stack split) opts) (search self file (. split 1) (stack-add-split! stack split) opts)
;; A do block returns the last form ;; A do block returns the last form
[-do- & body] (where [-do- & body] (. body 1))
(search self file (. body (length body)) stack opts) (search self file (. body (length body)) stack opts)
[-let- _binding & body] (where [-let- _binding & body] (. body 1))
(search self file (. body (length body)) stack opts) (search self file (. body (length body)) stack opts)
;; functions evaluate to "themselves" ;; functions evaluate to "themselves"
[-fn-] [-fn-]
(values {:definition call} file))) ;; BASE CASE !! (values {:definition call} file) ;; BASE CASE !!
;; if we don't know, give up
_
(if (= 0 (length stack))
(values {:definition call} file)))) ;; BASE CASE!!
(set search (set search
(λ search [self file item stack opts] (λ search [self file item stack opts]
@ -91,7 +95,8 @@ the data provided by compiler.fnl."
(error (.. "I don't know what to do with " (view item)))))) (error (.. "I don't know what to do with " (view item))))))
(local {:metadata METADATA (local {:metadata METADATA
:scopes {:global {:specials SPECIALS}}} :scopes {:global {:specials SPECIALS
:macros MACROS}}}
(require :fennel.compiler)) (require :fennel.compiler))
(λ search-main [self file symbol opts ?byte] (λ search-main [self file symbol opts ?byte]
@ -150,7 +155,7 @@ Returns:
"find a definition just from the name of the item, and the scope it is in" "find a definition just from the name of the item, and the scope it is in"
(assert (= (type name) :string)) (assert (= (type name) :string))
(let [stack (stack-add-multisym! [] name)] (let [stack (stack-add-multisym! [] name)]
(case (. METADATA (. SPECIALS name)) (case (. METADATA (or (. MACROS name) (. SPECIALS name)))
metadata {:binding (sym name) : metadata} metadata {:binding (sym name) : metadata}
_ (case (global-info self name) _ (case (global-info self name)
global-item global-item global-item global-item

View File

@ -41,4 +41,5 @@ I suspect this file may be gone after a bit of refactoring."
modname (utils.path->uri modname) modname (utils.path->uri modname)
nil nil)) nil nil))
{: lookup} {: lookup
: add-workspaces-to-path}

View File

@ -1,9 +1,9 @@
"State "State
This module keeps track of the state of the language server. This module keeps track of the state of the language server.
There are helpers to get file objects, and in the future, there There are helpers to get files (get-by functions are all for
will be functions for managing user options. There is no global getting files), and there's stuff for configuration options.
state in this project: all state will be stored in the \"self\" There is no global state in this project: all state is stored
object." in the \"self\" object."
(local searcher (require :fennel-ls.searcher)) (local searcher (require :fennel-ls.searcher))
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))
@ -65,7 +65,7 @@ object."
;; allow some globals ;; allow some globals
;; pick from existing libraries of globals (ie love2d) ;; pick from existing libraries of globals (ie love2d)
;; pick between different versions of lua (ie luajit) ;; pick between different versions of lua (ie luajit)
;; pick a "compat always" mode that accpets anything if it could be valid in any lua ;; pick a "compat always" mode that accepts anything if it could be valid in any lua
;; make a "compat strict" mode that warns about any lua-version-specific patterns ;; make a "compat strict" mode that warns about any lua-version-specific patterns
;; ie using (unpack) without saying (or table.unpack _G.unpack) or something like that ;; ie using (unpack) without saying (or table.unpack _G.unpack) or something like that

View File

@ -8,11 +8,13 @@ These functions are all pure functions, which makes me happy."
(= (str:sub 1 len) pre))) (= (str:sub 1 len) pre)))
(λ uri->path [uri] (λ uri->path [uri]
"Strips the \"file://\" prefix from a uri to turn it into a path. Throws an error if it is not a path uri"
(local prefix "file://") (local prefix "file://")
(assert (startswith uri prefix)) (assert (startswith uri prefix) "encountered a URI that is not a file???")
(string.sub uri (+ (length prefix) 1))) (string.sub uri (+ (length prefix) 1)))
(λ path->uri [path] (λ path->uri [path]
"Prepents the \"file://\" prefix to a path to turn it into a uri"
(.. "file://" path)) (.. "file://" path))
(λ next-line [str ?from] (λ next-line [str ?from]
@ -25,7 +27,7 @@ These functions are all pure functions, which makes me happy."
(λ pos->byte [str line col] (λ pos->byte [str line col]
"convert a 0-indexed line and column into a 1-indexed byte. Doesn't yet handle UTF8 UTF16 magic from the protocol" "convert a 0-indexed line and column into a 1-indexed byte. Doesn't yet handle UTF8 UTF16 magic from the protocol"
(var sofar 1) (var sofar 1)
(for [_ 1 line :until (not sofar)] (for [_ 1 line &until (not sofar)]
(set sofar (next-line str sofar))) (set sofar (next-line str sofar)))
(if sofar (if sofar
(+ sofar col) (+ sofar col)

View File

@ -1,4 +1,4 @@
(import-macros {: is-matching : describe : it : before-each} :test) (import-macros {: is-matching : is-casing : describe : it : before-each} :test)
(local is (require :test.is)) (local is (require :test.is))
(local {: view} (require :fennel)) (local {: view} (require :fennel))
@ -30,6 +30,9 @@
(it "suggests locals in scope" (it "suggests locals in scope"
(check-completion "(local x 10)\n(print )" 1 7 [:x])) (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" (it "suggests locals in scope at the top level"
(check-completion "(local x 10)\n\n" 1 0 [:x])) (check-completion "(local x 10)\n\n" 1 0 [:x]))
@ -59,6 +62,9 @@
(it "still completes with no body in the `let`" (it "still completes with no body in the `let`"
(check-completion "(let [x 10 y 20]\n )" 1 2 [:x :y])) (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`" (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])) (check-completion "(let [a 10\n b 20\n " 1 6 [:a :b]))
@ -110,6 +116,13 @@
(check-completion "(local x {:field (fn [])})\n(x:fi" 1 5 [:field] [:table])) (check-completion "(local x {:field (fn [])})\n(x:fi" 1 5 [:field] [:table]))
(describe "metadata" (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" (it "offers rich information about function completions"
(let [client (doto (create-client) (let [client (doto (create-client)
(: :open-file! filename "(fn xyzzy [x y z] \"docstring\" nil)\n(xyzz")) (: :open-file! filename "(fn xyzzy [x y z] \"docstring\" nil)\n(xyzz"))
@ -117,9 +130,35 @@
;; TODO this seems a little bit weird to assert ;; TODO this seems a little bit weird to assert
(is.same :xyzzy completion.label "the first completion should be xyzzy") (is.same :xyzzy completion.label "the first completion should be xyzzy")
(assert completion.kind "completion kind should be present") (assert completion.kind "completion kind should be present")
(assert completion.documentation "completion documentation should be present"))))) (assert completion.documentation "completion documentation should be present")))
; (it "offers rich information about global completions" (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 everything"
; (let [client (doto (create-client) ; (let [client (doto (create-client)
; (: :open-file! filename "(")) ; (: :open-file! filename "("))
; [{:result completions}] (client:completion filename 0 1) ; [{:result completions}] (client:completion filename 0 1)
@ -135,15 +174,15 @@
; (each [_ completion (ipairs completions)] ; (each [_ completion (ipairs completions)]
; (is.same (type completion.label) :string "unlabeled completion") ; (is.same (type completion.label) :string "unlabeled completion")
; (is.same (type completion.kind) :number (.. completion.label " needs a kind")) ; (is.same (type completion.kind) :number (.. completion.label " needs a kind"))
; (is.same (type completion.documentation) :table (.. completion.label " needs documentation"))))))) ; (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 macro completions")
;; (it "offers rich information about variable completions") ;; (it "offers rich information about variable completions")
;; (it "offers rich information about field completions") ;; (it "offers rich information about field completions")
;; (it "offers rich information about method completions") ;; (it "offers rich information about method completions")
;; (it "offers rich information about module completions") ;; (it "offers rich information about module completions")
;; (it "offers rich information about macromodule completions"))) ;; (it "offers rich information about macro-module completions")))
;; (it "suggests known fn keys when using the `:` special") ;; (it "suggests known fn keys when using the `:` special")
;; (it "suggests known keys when using the `.` special") ;; (it "suggests known keys when using the `.` special")

View File

@ -36,7 +36,22 @@
,(view pattern) ,(view pattern)
,(and ?msg `(.. "\n" ,?msg)))))) ,(and ?msg `(.. "\n" ,?msg))))))
(fn is-casing [item pattern ?msg]
"check if item matches a pattern according to fennel's `match` builtin"
`(case ,item
,pattern nil
?otherwise#
(error
(.. "Pattern did not match:\n"
(let [fennel# (require :fennel)]
(fennel#.view ?otherwise#))
"\ndid not match pattern:\n"
,(view pattern)
,(and ?msg `(.. "\n" ,?msg))))))
{: it {: it
: describe : describe
: is-matching : is-matching
: is-casing
: before-each} : before-each}

View File

@ -5,6 +5,7 @@
(setmetatable {:equal #(do ((. (expect $1) :to :be) $2) true) (setmetatable {:equal #(do ((. (expect $1) :to :be) $2) true)
:same #(do ((. (expect $1) :to :equal) $2 $3) true) :same #(do ((. (expect $1) :to :equal) $2 $3) true)
:nil #(do ((. (expect $1) :to_not :exist)) true) :nil #(do ((. (expect $1) :to_not :exist)) true)
:not {:nil #(do ((. (expect $1) :to :exist)) true)} :not {:nil #(do ((. (expect $1) :to :exist)) true)
:same #(do ((. (expect $1) :to_not :equal) $2))}
:truthy #(do ((. (expect $1) :to :be :truthy)) true)} :truthy #(do ((. (expect $1) :to :be :truthy)) true)}
{:__call #(do ((. (expect $2) :to :be :truthy)) true)}) {:__call #(do ((. (expect $2) :to :be :truthy)) true)})

View File

@ -14,12 +14,12 @@
[{:result {:range _range}}] [{:result {:range _range}}]
"error message"))) "error message")))
;; (it "can set the path" (it "can set the macro path"
;; (let [client (doto (create-client {:fennel-ls {:macro-path "./?/?.fnl"}}) (let [client (create-client {:fennel-ls {:macro-path "./?/?.fnl"}})
;; (: :open-file! (.. ROOT-URI :/test.fnl) "(import-macros {: this-is-in-modname} :modname)"))] responses (client:open-file! (.. ROOT-URI :/test.fnl) "(import-macros {: this-is-in-modname} :modname)")]
;; (is-matching (print ((. (require :fennel) :view) responses))))
;; (client:definition (.. ROOT-URI :/test.fnl) 0 12)
;; [{:result {:range message}}])))) ;; (it "recompiles modules if the macro files are modified)"
;; (it "can infer the macro path from fennel-path" ;; (it "can infer the macro path from fennel-path"
;; (local self (doto [] (setup-server {:fennel-ls {:fennel-path "./?/?.fnl"}})))) ;; (local self (doto [] (setup-server {:fennel-ls {:fennel-path "./?/?.fnl"}}))))