This commit is contained in:
XeroOl 2022-08-19 09:43:58 -05:00
parent 22b4235073
commit a09035dabc
No known key found for this signature in database
GPG Key ID: 9DD4B4B4DAED0322
15 changed files with 283 additions and 187 deletions

View File

@ -1,24 +1,25 @@
(local fennel (require :fennel)) (local {: sym? : list? : sequence? : sym : view &as fennel} (require :fennel))
(local message (require :fennel-ls.message))
;; 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
(local -require- (fennel.sym :require)) (local -require- (sym :require))
(local -fn- (fennel.sym :fn)) (local -fn- (sym :fn))
(local -λ- (fennel.sym :λ)) (local -λ- (sym :λ))
(local -lambda- (fennel.sym :lambda)) (local -lambda- (sym :lambda))
(λ multisym? [t] (λ multisym? [t]
;; check if t is a symbol with multiple parts, eg. foo.bar.baz ;; check if t is a symbol with multiple parts, eg. foo.bar.baz
(and (fennel.sym? t) (and (sym? t)
(let [t (tostring t)] (let [t (tostring t)]
(or (t:find "%.") (or (t:find "%.")
(t:find ":"))))) (t:find ":")))))
(λ iter [t] (λ iter [t]
;; iterate through a list, sequence, or table ;; iterate through a list, sequence, or table
(if (or (fennel.list? t) (if (or (list? t)
(fennel.sequence? t)) (sequence? t))
(ipairs t) (ipairs t)
(pairs t))) (pairs t)))
@ -31,10 +32,11 @@
(λ compile [file] (λ compile [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"
(let [definitions-by-scope (doto {} (setmetatable has-tables-mt))
(local references {}) definitions {}
(local definitions-by-scope (doto {} (setmetatable has-tables-mt))) diagnostics {}
(local definitions {}) references {}
require-calls {}]
(λ find-definition [name ?scope] (λ find-definition [name ?scope]
(when ?scope (when ?scope
@ -43,7 +45,7 @@
(λ reference [ast scope] (λ reference [ast scope]
;; Add a reference to the references ;; Add a reference to the references
(assert (fennel.sym? ast)) (assert (sym? ast))
;; find reference ;; find reference
(let [name (string.match (tostring ast) "[^%.:]+") (let [name (string.match (tostring ast) "[^%.:]+")
target (find-definition (tostring name) scope)] target (find-definition (tostring name) scope)]
@ -55,7 +57,7 @@
;; right now I'm not keeping track of *how* the symbol was destructured: just finding all the symbols for now. ;; right now I'm not keeping track of *how* the symbol was destructured: just finding all the symbols for now.
;; also, there's no logic for (values) ;; also, there's no logic for (values)
(λ recurse [binding keys] (λ recurse [binding keys]
(if (fennel.sym? binding) (if (sym? binding)
(let [definition (let [definition
{: binding {: binding
: ?definition : ?definition
@ -75,9 +77,9 @@
;; add a function definition to the definitions ;; add a function definition to the definitions
(match ast (match ast
(where [_fn name args] (where [_fn name args]
(and (fennel.sym? name) (and (sym? name)
(not (multisym? name)) ;; not dealing with multisym for now (not (multisym? name)) ;; not dealing with multisym for now
(fennel.sequence? args))) (sequence? args)))
(tset (. definitions-by-scope scope) ;; !!! TODO somehow insert into child scope (tset (. definitions-by-scope scope) ;; !!! TODO somehow insert into child scope
(tostring name) (tostring name)
{:binding name {:binding name
@ -109,31 +111,57 @@
[-λ-] [-λ-]
(define-function ast scope) (define-function ast scope)
[-lambda-] [-lambda-]
(define-function ast scope))) (define-function ast scope)
[-require- modname]
(tset require-calls ast true)))
(local plugin (λ on-compiler-error [_ msg ast call-me-to-reset-the-compiler]
(let [range (message.ast->range ast file)]
(table.insert diagnostics
{:range range
:message msg
:severity 3
:code 201
:codeDescription "compiler error"}))
(call-me-to-reset-the-compiler)
(error "__NOT_AN_ERROR"))
;; TODO clean up this code. It's awful now that there is error handling
(let
[plugin
{:name "fennel-ls" {:name "fennel-ls"
:versions ["1.2.0"] :versions ["1.2.0"]
:symbol-to-expression reference :symbol-to-expression reference
:call call :call call
:destructure define}) :destructure define
:assert-compile on-compiler-error}]
(local filename file.uri) ;; ATTEMPT TO PARSE AST
(local ast (match (pcall
(icollect [ok ast (fennel.parser file.text filename)] #(icollect [ok ast (fennel.parser file.text file.uri {:plugins [plugin]})]
ast)) ast))
;; ON SUCCESS
(local scope (fennel.scope)) (true ast)
(let [scope (fennel.scope)]
(each [_i form (ipairs ast)] (each [_i form (ipairs ast)]
(fennel.compile form ;; COMPILE
{: filename (match (pcall fennel.compile form {:filename file.uri : scope :plugins [plugin]})
: scope (where (nil err) (not= err "__NOT_AN_ERROR"))
:plugins [plugin]})) (error err)))
(set file.ast ast))
;; ON FAILURE
(false err)
;; RECORD THE FAILURE
(table.insert diagnostics
{:range (message.pos->range 0 0 0 0)
:message err}))
;; write things back to the file object ;; write things back to the file object
(set file.references references)
(set file.definitions definitions)
;; (set file.definitions-by-scope definitions-by-scope) ;; not needed yet ;; (set file.definitions-by-scope definitions-by-scope) ;; not needed yet
(set file.ast ast)) (set file.definitions definitions)
(set file.diagnostics diagnostics)
(set file.references references)
(set file.require-calls require-calls))))
;; (set file.compiled? true)) ;; (set file.compiled? true))
{: compile} {: compile}

View File

@ -4,12 +4,14 @@ You finally made it. Here is the main code that implements the language server p
Every time the client sends a message, it gets handled by a function in the corresponding table type. Every time the client sends a message, it gets handled by a function in the corresponding table type.
(ie, a textDocument/didChange notification will call notifications.textDocument/didChange (ie, a textDocument/didChange notification will call notifications.textDocument/didChange
and a textDocument/defintion request will call requests.textDocument/didChange)" and a textDocument/defintion request will call requests.textDocument/didChange)"
(local {: pos->byte : apply-changes} (require :fennel-ls.utils)) (local {: pos->byte : apply-changes} (require :fennel-ls.utils))
(local message (require :fennel-ls.message)) (local message (require :fennel-ls.message))
(local state (require :fennel-ls.state)) (local state (require :fennel-ls.state))
(local language (require :fennel-ls.language)) (local language (require :fennel-ls.language))
(local formatter (require :fennel-ls.formatter)) (local formatter (require :fennel-ls.formatter))
(local {: view} (require :fennel))
(local requests []) (local requests [])
(local notifications []) (local notifications [])
@ -21,7 +23,7 @@ Every time the client sends a message, it gets handled by a function in the corr
:hoverProvider {:workDoneProgress false} :hoverProvider {:workDoneProgress false}
;; :signatureHelpProvider nil ;; :signatureHelpProvider nil
;; :declarationProvider nil ;; :declarationProvider nil
:definitionProvider {:workDoneProgress false}}) :definitionProvider {:workDoneProgress false}
;; :typeDefinitionProvider nil ;; :typeDefinitionProvider nil
;; :implementationProvider nil ;; :implementationProvider nil
;; :referencesProvider nil ;; :referencesProvider nil
@ -45,7 +47,7 @@ Every time the client sends a message, it gets handled by a function in the corr
;; :typeHierarchyProvider nil ;; :typeHierarchyProvider nil
;; :inlineValueProvider nil ;; :inlineValueProvider nil
;; :inlayHintProvider nil ;; :inlayHintProvider nil
;; :diagnosticProvider {:workDoneProgress false}}) :diagnosticProvider {:workDoneProgress false}})
;; :workspaceSymbolProvider nil ;; :workspaceSymbolProvider nil
;; :workspace {:workspaceFolders nil ;; :workspace {:workspaceFolders nil
;; :documentOperations {:didCreate nil ;; :documentOperations {:didCreate nil
@ -63,38 +65,39 @@ Every time the client sends a message, it gets handled by a function in the corr
(λ requests.textDocument/definition [self send {: position :textDocument {: uri}}] (λ requests.textDocument/definition [self send {: position :textDocument {: uri}}]
(local file (state.get-by-uri self uri)) (local file (state.get-by-uri self uri))
(local byte (pos->byte file.text position.line position.character)) (local byte (pos->byte file.text position.line position.character))
(match (language.find-symbol file.ast byte) (match-try (language.find-symbol file.ast byte)
symbol (symbol parents)
(match (language.search-main self file symbol) (match-try
(result result-file) ;; curse you, magical match rules (let [parent (. parents (length parents))]
(if (. file.require-calls parent)
(language.search self file parent [])))
nil
(language.search-main self file symbol))
(result result-file)
(message.range-and-uri (message.range-and-uri
(or result.binding result.?definition) (or result.binding result.?definition)
result-file)))) result-file)
(catch _ nil)))
(λ requests.textDocument/hover [self send {: position :textDocument {: uri}}] (λ requests.textDocument/hover [self send {: position :textDocument {: uri}}]
(local file (state.get-by-uri self uri)) (local file (state.get-by-uri self uri))
(local byte (pos->byte file.text position.line position.character)) (local byte (pos->byte file.text position.line position.character))
(match (language.find-symbol file.ast byte) (match-try (language.find-symbol file.ast byte)
symbol symbol (language.search-main self file symbol)
(match (language.search-main self file symbol) result {:contents {:kind "markdown"
result :value (formatter.hover-format result)}}))
{:contents
{:kind
"markdown"
:value
(formatter.hover-format result)}})))
(λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}] (λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}]
(local file (state.get-by-uri self uri)) (local file (state.get-by-uri self uri))
(assert file.open?) (state.set-uri-contents self uri (apply-changes file.text contentChanges))
(apply-changes (. self.files uri) contentChanges)) (send (message.diagnostics file)))
(λ notifications.textDocument/didOpen [self send {:textDocument {: languageId : text : uri}}] (λ notifications.textDocument/didOpen [self send {:textDocument {: languageId : text : uri}}]
(local file (state.set-uri-contents self uri text)) (local file (state.set-uri-contents self uri text))
(set file.open? true)) (set file.open? true)
(send (message.diagnostics file)))
(λ notifications.textDocument/didClose [self send {:textDocument {: uri}}] (λ notifications.textDocument/didClose [self send {:textDocument {: uri}}]
;; TODO fix
(local file (state.get-by-uri self uri)) (local file (state.get-by-uri self uri))
(set file.open? false)) (set file.open? false))

View File

@ -1,15 +1,12 @@
(local fennel (require :fennel)) (local {: sym? : list? : sequence? : sym : view} (require :fennel))
(local fennelutils (require :fennel.utils))
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))
(local state (require :fennel-ls.state)) (local state (require :fennel-ls.state))
(local get-ast-info utils.get-ast-info) (local get-ast-info utils.get-ast-info)
(local sym? fennel.sym?) (local -require- (sym :require))
(local list? fennel.list?) (local -dot- (sym :.))
(local -do- (sym :do))
(local -require- (fennel.sym :require))
(local -dot- (fennel.sym :.))
(var search nil) ;; all of the search functions are mutually recursive (var search nil) ;; all of the search functions are mutually recursive
@ -23,11 +20,11 @@
(search self file ?definition stack)))) (search self file ?definition stack))))
(λ search-symbol [self file symbol stack] (λ search-symbol [self file symbol stack]
(match (. file.references symbol)
to (search-assignment self file to
(let [split (utils.multi-sym-split symbol)] (let [split (utils.multi-sym-split symbol)]
(fcollect [i (length split) 2 -1 &into stack] (fcollect [i (length split) 2 -1 &into stack]
(. split i))) ;; TODO test coverage for this line (. split i)))))) ;; TODO test coverage for this line
(match (. file.references symbol)
to (search-assignment self file to stack)))
(λ search-table [self file tbl stack] (λ search-table [self file tbl stack]
(if (. tbl (. stack (length stack))) (if (. tbl (. stack (length stack)))
@ -42,19 +39,24 @@
(let [newfile (state.get-by-module self mod) (let [newfile (state.get-by-module self mod)
newitem (. newfile.ast (length newfile.ast))] newitem (. newfile.ast (length newfile.ast))]
(search self newfile newitem stack)) (search self newfile newitem stack))
; A . form indexes into item 1 with the other items
[-dot- & split] [-dot- & split]
(do (search self file (. split 1)
(fcollect [i (length split) 2 -1 &into stack] (fcollect [i (length split) 2 -1 &into stack]
(. split i)) (. split i)))
(search self file (. split 1) stack))))
;; A do block returns the last form
[-do- & body]
(search self file (. body (length body)) stack)))
(set search (set search
(λ search [self file item stack] (λ search [self file item stack]
(if (fennelutils.table? item) (search-table self file item stack) (if
(sym? item) (search-symbol self file item stack) (sym? item) (search-symbol self file item stack)
(list? item) (search-list self file item stack) (list? item) (search-list self file item stack)
(= :table (type item)) (search-table self file item stack)
(= 0 (length stack)) {:?definition item} ;; BASE CASE !! (= 0 (length stack)) {:?definition item} ;; BASE CASE !!
(error (.. "I don't know what to do with " (fennel.view item)))))) (error (.. "I don't know what to do with " (view item))))))
(λ search-main [self file symbol] (λ search-main [self file symbol]
;; TODO partial byting, go to different defitition sites depending on which section of the symbol the trigger happens on ;; TODO partial byting, go to different defitition sites depending on which section of the symbol the trigger happens on
@ -103,29 +105,37 @@
byte byte
(+ 1 (get-ast-info ?ast :byteend)))))) (+ 1 (get-ast-info ?ast :byteend))))))
(λ find-symbol [ast byte ?recursively-called] (λ find-symbol [ast byte ?stack]
(local stack (or ?stack []))
(if (or (not= :table (type ast)) (if (or (not= :table (type ast))
(does-not-contain? ast byte)) (does-not-contain? ast byte))
nil nil
(and (sym? ast) (contains? ast byte)) (and (sym? ast) (contains? ast byte))
ast (values ast [])
(or (not ?recursively-called) (or (= 0 (length stack))
(fennel.list? ast) (list? ast)
(fennel.sequence? ast)) (sequence? ast))
;; TODO binary search ;; TODO binary search
(accumulate (accumulate
[result nil [(result stack*) nil
_ v (ipairs ast) _ v (ipairs ast)
&until (or result (past? v byte))] &until (or result (past? v byte))]
(find-symbol v byte true)) (do
:else (table.insert stack ast)
(match (find-symbol v byte stack)
ret (values ret stack)
nil (do (table.remove stack) nil))))
(accumulate (accumulate
[result nil [(result stack*) nil
k v (pairs ast) k v (pairs ast)
&until result] &until result]
(or (do
(find-symbol k byte true) (table.insert stack ast)
(find-symbol v byte true))))) (match (or (find-symbol k byte stack)
(find-symbol v byte stack))
ret (values ret stack)
nil (do (table.remove stack) nil))))))
{: find-symbol {: find-symbol
: search-main} : search-main
: search}

View File

@ -7,7 +7,6 @@ missing fields with null fields, and I want to have one location
to look to fix this in the future." to look to fix this in the future."
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))
(local state (require :fennel-ls.state))
(local error-codes (local error-codes
{;; JSON-RPC errors {;; JSON-RPC errors
@ -47,21 +46,38 @@ to look to fix this in the future."
: id : id
:result ?result}) :result ?result})
(λ range-and-uri [?ast file] (λ pos->range [sl sc el ec]
"if possible, returns the location of a symbol" {:start {:line sl :character sc}
(match :end {:line el :character ec}})
(values
(utils.get-ast-info ?ast :bytestart) (λ ast->range [?ast file]
(match (values (utils.get-ast-info ?ast :bytestart)
(utils.get-ast-info ?ast :byteend)) (utils.get-ast-info ?ast :byteend))
(i j) (i j)
(let [(start-line start-col) (utils.byte->pos file.text i) (let [(start-line start-col) (utils.byte->pos file.text i)
(end-line end-col) (utils.byte->pos file.text (+ j 1))] (end-line end-col) (utils.byte->pos file.text (+ j 1))]
{:range {:start {:line start-line :character start-col} (pos->range start-line start-col end-line end-col))))
:end {:line end-line :character end-col}}
:uri file.uri}))) (λ range-and-uri [?ast {: uri &as file}]
"if possible, returns the location of a symbol"
(match (ast->range ?ast file)
range {: range : uri}))
(λ log [msg]
(create-notification :window/logMessage {: msg :type 4}))
(λ diagnostics [file]
(create-notification
"textDocument/publishDiagnostics"
{:uri file.uri
:diagnostics file.diagnostics}))
{: create-notification {: create-notification
: create-request : create-request
: create-response : create-response
: create-error : create-error
: range-and-uri} : pos->range
: ast->range
: log
: range-and-uri
: diagnostics}

View File

@ -1,6 +1,5 @@
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))
(local searcher (require :fennel-ls.searcher)) (local searcher (require :fennel-ls.searcher))
(local {: compile} (require :fennel-ls.compiler)) (local {: compile} (require :fennel-ls.compiler))
(λ init-state [self params] (λ init-state [self params]
@ -23,7 +22,6 @@
(λ get-by-path [self path] (λ get-by-path [self path]
(get-by-uri (utils.path->uri path))) (get-by-uri (utils.path->uri path)))
(λ get-by-module [self module] (λ get-by-module [self module]
;; check the cache ;; check the cache
(match (. self.modules module) (match (. self.modules module)
@ -45,9 +43,10 @@
(match (. self.files uri) (match (. self.files uri)
;; modify existing file ;; modify existing file
file file
(do
(when (not= text file.text) (when (not= text file.text)
(set file.text text) (set file.text text)
(compile file) (compile file))
file) file)
;; create new file ;; create new file
@ -57,7 +56,6 @@
(compile file) (compile file)
file))) file)))
{: get-by-uri {: get-by-uri
: get-by-module : get-by-module
: set-uri-contents : set-uri-contents

42
test/diagnostic-test.fnl Normal file
View File

@ -0,0 +1,42 @@
(import-macros {: is-matching : describe : it : before-each} :test)
(local is (require :luassert))
(local {: view} (require :fennel))
(local {: ROOT-URI
: setup-server} (require :test.util))
(local dispatch (require :fennel-ls.dispatch))
(local message (require :fennel-ls.message))
(describe "diagnostic messages"
(it "handles compile errors"
(local state (doto [] setup-server))
(let
[responses
(dispatch.handle* state
(message.create-notification "textDocument/didOpen"
{:textDocument
{:uri (.. ROOT-URI "imaginary-file.fnl")
:languageId "fennel"
:version 1
:text "(do do)"}}))]
(is-matching
responses
[{:params {:diagnostics [diagnostic]}}]
"")))
(it "handles parse errors"
(local state (doto [] setup-server))
(let
[responses
(dispatch.handle* state
(message.create-notification "textDocument/didOpen"
{:textDocument
{:uri (.. ROOT-URI "imaginary-file.fnl")
:languageId "fennel"
:version 1
:text "(do (print :hello(]"}}))]
(is-matching
responses
[{:params {:diagnostics [diagnostic]}}]
""))))

View File

@ -1,7 +1,6 @@
(import-macros {: is-matching : describe : it : before-each} :test.macros) (import-macros {: is-matching : describe : it : before-each} :test)
(local is (require :luassert)) (local is (require :luassert))
(local fennel (require :fennel))
(local {: ROOT-URI (local {: ROOT-URI
: setup-server} (require :test.util)) : setup-server} (require :test.util))
@ -64,11 +63,11 @@
(check :goto-definition.fnl 38 15 :goto-definition.fnl 33 7 33 13)) (check :goto-definition.fnl 38 15 :goto-definition.fnl 33 7 33 13))
(it "can go up and down field accesses" (it "can go up and down field accesses"
(check :goto-definition.fnl 45 15 :goto-definition.fnl 40 7 40 13))) (check :goto-definition.fnl 45 15 :goto-definition.fnl 40 7 40 13))
;; (it "works directly on a require/include (require XXX))" (it "works directly on a require/include (require XXX))"
;; (check :goto-definition.fnl 1 5 :bar.fnl 0 0 0 0))) (check :goto-definition.fnl 1 5 :bar.fnl 0 0 0 2)))
;; (it "can go to a reference that occurs in a macro") ;; (it "can go to a reference that occurs in a macro")
;; (it "doesn't have ghost definitions from the same byte ranges as the macro files it's using") ;; (it "doesn't have ghost definitions from the same byte ranges as the macro files it's using")

View File

@ -1,7 +1,7 @@
(import-macros {: is-matching : describe : it : before-each} :test.macros) (import-macros {: is-matching : describe : it : before-each} :test)
(local is (require :luassert)) (local is (require :luassert))
(local fennel (require :fennel)) (local {: view} (require :fennel))
(local {: ROOT-URI (local {: ROOT-URI
: setup-server} (require :test.util)) : setup-server} (require :test.util))
@ -23,7 +23,7 @@
{:contents {:contents
{:kind "markdown" {:kind "markdown"
:value response-string}}}] :value response-string}}}]
(.. "expected response: " (fennel.view response-string))))) (.. "expected response: " (view response-string)))))
(it "hovers over a function" (it "hovers over a function"
(check "hover.fnl" 6 6 "```fnl\n(fn my-function [arg1 arg2 arg3] ...)\n```")) (check "hover.fnl" 6 6 "```fnl\n(fn my-function [arg1 arg2 arg3] ...)\n```"))

View File

@ -6,3 +6,4 @@
(require :test.goto-definition-test) (require :test.goto-definition-test)
(require :test.hover-test) (require :test.hover-test)
(require :test.misc-test) (require :test.misc-test)
(require :test.diagnostic-test)

View File

@ -1,7 +1,6 @@
(import-macros {: is-matching : describe : it} :test.macros) (import-macros {: is-matching : describe : it} :test)
(local is (require :luassert)) (local is (require :luassert))
(local fennel (require :fennel))
(local stringio (require :test.pl.stringio)) (local stringio (require :test.pl.stringio))
(local json-rpc (require :fennel-ls.json-rpc)) (local json-rpc (require :fennel-ls.json-rpc))

View File

@ -1,4 +1,4 @@
(import-macros {: is-matching : describe : it} :test.macros) (import-macros {: is-matching : describe : it} :test)
(local is (require :luassert)) (local is (require :luassert))
(local {: ROOT-PATH : ROOT-URI} (require :test.util)) (local {: ROOT-PATH : ROOT-URI} (require :test.util))

View File

@ -1,4 +1,4 @@
(import-macros {: is-matching : describe : it : before-each} :test.macros) (import-macros {: is-matching : describe : it : before-each} :test)
(local is (require :luassert)) (local is (require :luassert))
(local fennel (require :fennel)) (local fennel (require :fennel))

View File

@ -1,4 +1,4 @@
(import-macros {: is-matching : describe : it} :test.macros) (import-macros {: is-matching : describe : it} :test)
(local is (require :luassert)) (local is (require :luassert))
(local fennel (require :fennel)) (local fennel (require :fennel))

View File

@ -1 +1 @@
nil {}