rewrite diagnostics to support pull diagnostics

This commit is contained in:
XeroOl 2025-08-07 22:57:07 -05:00
parent c127c54937
commit a366e5996d
14 changed files with 142 additions and 123 deletions

View File

@ -42,6 +42,7 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
(local {: get-ast-info &as utils} (require :fennel-ls.utils)) (local {: get-ast-info &as utils} (require :fennel-ls.utils))
(local files (require :fennel-ls.files)) (local files (require :fennel-ls.files))
(local docs (require :fennel-ls.docs)) (local docs (require :fennel-ls.docs))
(local compiler (require :fennel-ls.compiler))
(var search-multival nil) ;; all of the search functions are mutually recursive (var search-multival nil) ;; all of the search functions are mutually recursive
@ -142,6 +143,7 @@ find the definition `10`, but if `opts.stop-early?` is set, it would find
(when (= :string (type mod)) (when (= :string (type mod))
(let [newfile (files.get-by-module server mod file.macro-file?)] (let [newfile (files.get-by-module server mod file.macro-file?)]
(when newfile (when newfile
(compiler.compile server newfile)
(let [newitem (. newfile.ast (length newfile.ast))] (let [newitem (. newfile.ast (length newfile.ast))]
(when (= (length stack) 1) (when (= (length stack) 1)
(set opts.searched-through-require-with-stack-size-1 true)) (set opts.searched-through-require-with-stack-size-1 true))
@ -267,9 +269,10 @@ initialization-opts: {:stack ?list[ast]
byte byte
(+ 1 (get-ast-info ?ast :byteend)))))) (+ 1 (get-ast-info ?ast :byteend))))))
(λ find-symbol [ast byte] (λ find-symbol [server file byte]
"tries to find a sym, and a list of all of its parents/grandparents" "tries to find a sym, and a list of all of its parents/grandparents"
(local parents [ast]) (compiler.compile server file)
(local parents [file.ast])
(λ recurse [ast] (λ recurse [ast]
(if (if
(sym? ast) (sym? ast)
@ -292,40 +295,34 @@ initialization-opts: {:stack ?list[ast]
(contains? value byte) (contains? value byte)
(recurse value))))))) (recurse value)))))))
(values (values
(accumulate [result nil _ top-level-form (ipairs ast) &until result] (accumulate [result nil _ top-level-form (ipairs file.ast) &until result]
(if (contains? top-level-form byte) (if (contains? top-level-form byte)
(recurse top-level-form))) (recurse top-level-form)))
(fcollect [i 1 (length parents)] (fcollect [i 1 (length parents)]
(. parents (- (length parents) i -1))))) (. parents (- (length parents) i -1)))))
(λ find-nearest-call [_server file byte] (λ find-nearest-call [server file byte]
"Find the nearest call "Find the nearest call
returns the called symbol and the number of the argument closest to byte" returns the called symbol and the number of the argument closest to byte"
(λ find-list [[call & parents]] (case-try (find-symbol server file byte)
(if (. file.calls call) (_symbol parents) (accumulate [result nil _ v (ipairs parents) &until result]
call (if (. file.calls v)
(next parents) v))
(find-list parents))) [callee &as call] (values callee ;; TODO: special handling for binding forms so we can point to the
;; individual arguments in an each or accumulate call.
;; Also need to split them up in formatter.fnl
(faccumulate [index nil
i (length call) 1 -1 &until index]
(if (contains? (. call i) byte)
; -2 because this is the 3rd element of the list, but
; the 2nd argument to the call, and LSP is 0-indexed
(- i 2)
(past? (. call i) byte)
; this means we are either at the end of the list or
; inserting between two arguments
(- i 1))))
(λ arg-index [call byte]
;; TODO: special handling for binding forms so we can point to the
;; individual arguments in an each or accumulate call.
;; Also need to split them up in formatter.fnl
(faccumulate [index nil
i (length call) 1 -1 &until index]
(if (contains? (. call i) byte)
; -2 because this is the 3rd element of the list, but
; the 2nd argument to the call, and LSP is 0-indexed
(- i 2)
(past? (. call i) byte)
; this means we are either at the end of the list or
; inserting between two arguments
(- i 1))))
(case-try (find-symbol file.ast byte)
(_symbol parents) (find-list parents)
[callee &as call] (values callee (arg-index call byte))
(catch _ nil))) (catch _ nil)))
(λ find-definition [server file symbol ?byte] (λ find-definition [server file symbol ?byte]

View File

@ -62,12 +62,12 @@ identifiers are declared / referenced in which places."
position (utils.pos->position file.text line byte server.position-encoding)] position (utils.pos->position file.text line byte server.position-encoding)]
{:start position :end position})) {:start position :end position}))
(λ compile [{:configuration {: macro-path} :root-uri ?root-uri &as server} file] do-compile [{:configuration {: macro-path} :root-uri ?root-uri &as server} 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))
definitions {} ; symbol -> binding definitions {} ; symbol -> binding
diagnostics {} ; [diagnostic] compile-diagnostics {} ; [diagnostic]
references {} ; symbol -> references references {} ; symbol -> references
macro-refs {} ; symbol -> macro macro-refs {} ; symbol -> macro
scopes {} ; ast -> scope scopes {} ; ast -> scope
@ -307,7 +307,7 @@ identifiers are declared / referenced in which places."
(λ on-compile-error [_ msg ast call-me-to-reset-the-compiler] (λ on-compile-error [_ msg ast call-me-to-reset-the-compiler]
(let [range (or (message.ast->range server file ast) (let [range (or (message.ast->range server file ast)
message.unknown-range)] message.unknown-range)]
(table.insert diagnostics (table.insert compile-diagnostics
{:range range {:range range
:message msg :message msg
:severity message.severity.ERROR :severity message.severity.ERROR
@ -321,7 +321,7 @@ identifiers are declared / referenced in which places."
(λ on-parse-error [msg _filename line byte _source call-me-to-reset-the-compiler] (λ on-parse-error [msg _filename line byte _source call-me-to-reset-the-compiler]
(let [line (if (= line "?") 1 line) (let [line (if (= line "?") 1 line)
range (line+byte->range server file line byte)] range (line+byte->range server file line byte)]
(table.insert diagnostics (table.insert compile-diagnostics
{:range range {:range range
:message msg :message msg
:severity message.severity.ERROR :severity message.severity.ERROR
@ -334,7 +334,7 @@ identifiers are declared / referenced in which places."
(λ warn [msg ?ast _file ?line ?col] (λ warn [msg ?ast _file ?line ?col]
(let [range (or (message.ast->range server file ?ast) (line+byte->range server file (or ?line 1) (or ?col 0)))] (let [range (or (message.ast->range server file ?ast) (line+byte->range server file (or ?line 1) (or ?col 0)))]
(table.insert diagnostics (table.insert compile-diagnostics
{:range range {:range range
:message msg :message msg
:severity message.severity.WARN :severity message.severity.WARN
@ -380,7 +380,7 @@ identifiers are declared / referenced in which places."
(if (os.getenv :DEV) (if (os.getenv :DEV)
(error (.. "\nYou have crashed fennel-ls (or the fennel " component ") with the following message\n:" err (error (.. "\nYou have crashed fennel-ls (or the fennel " component ") with the following message\n:" err
"\n\n^^^ the error message above here is the root problem\n\n")) "\n\n^^^ the error message above here is the root problem\n\n"))
(table.insert diagnostics (table.insert compile-diagnostics
{:range (line+byte->range server file 1 1) {:range (line+byte->range server file 1 1)
:message (.. "unrecoverable " component " error: " err)})))) :message (.. "unrecoverable " component " error: " err)}))))
@ -434,7 +434,7 @@ identifiers are declared / referenced in which places."
(fn hook [] (fn hook []
(debug.sethook nil nil) (debug.sethook nil nil)
(if _G.jit (_G.jit.on)) (if _G.jit (_G.jit.on))
(table.insert diagnostics (table.insert compile-diagnostics
{:range message.unknown-range {:range message.unknown-range
:message "instruction limit reached" :message "instruction limit reached"
:severity message.severity.ERROR}) :severity message.severity.ERROR})
@ -459,11 +459,15 @@ identifiers are declared / referenced in which places."
(set file.scopes scopes) (set file.scopes scopes)
(set file.definitions definitions) (set file.definitions definitions)
(set file.definitions-by-scope definitions-by-scope) (set file.definitions-by-scope definitions-by-scope)
(set file.diagnostics diagnostics) (set file.compile-errors compile-diagnostics)
(set file.references references) (set file.references references)
(set file.require-calls require-calls) (set file.require-calls require-calls)
(set file.allowed-globals allowed-globals) (set file.allowed-globals allowed-globals)
(set file.macro-refs macro-refs) (set file.macro-refs macro-refs)
(set file.macro-calls macro-calls)))) (set file.macro-calls macro-calls))))
(fn compile [server file]
(when (not file.ast)
(do-compile server file)))
{: compile} {: compile}

View File

@ -25,8 +25,9 @@ actually recover the original completion. If the client supports both
CompletionClientCapabilites.completionList.itemDefaults.editRange and CompletionClientCapabilites.completionList.itemDefaults.editRange and
CompletionClientCapabilites.completionList.itemDefaults.data, then we can ask CompletionClientCapabilites.completionList.itemDefaults.data, then we can ask
the client to forward information to the resolve request by setting the `data` the client to forward information to the resolve request by setting the `data`
to {: uri : byte}. When this capability exists, `server.can-do-good-completions?` to {: uri : byte}. When this capability exists,
is set to true and we report that we support completionItem/resolve." `server.client-capable-of-good-completions?` is set to true and we report that
we support completionItem/resolve."
(local fennel (require :fennel)) (local fennel (require :fennel))
(local {:metadata METADATA} (require :fennel.compiler)) (local {:metadata METADATA} (require :fennel.compiler))
@ -49,7 +50,7 @@ is set to true and we report that we support completionItem/resolve."
file {:text (.. (file.text:sub 1 (- byte 1)) "|" (file.text:sub byte)) :uri file.uri} file {:text (.. (file.text:sub 1 (- byte 1)) "|" (file.text:sub byte)) :uri file.uri}
_ (compiler.compile server file) _ (compiler.compile server file)
;; find what ast objects are under the cursor ;; find what ast objects are under the cursor
(symbol parents) (analyzer.find-symbol file.ast byte) (symbol parents) (analyzer.find-symbol server file byte)
;; check what context I'm in ;; check what context I'm in
in-call-position? (or (and (fennel.list? (. parents 1)) in-call-position? (or (and (fennel.list? (. parents 1))
(= symbol (. parents 1 1))) (= symbol (. parents 1 1)))
@ -136,7 +137,7 @@ is set to true and we report that we support completionItem/resolve."
(fn binding-completions [] (fn binding-completions []
"completions when you're writing a destructure pattern. We suggest identifiers which are unknown" "completions when you're writing a destructure pattern. We suggest identifiers which are unknown"
(each [_ {: message} (ipairs file.diagnostics)] (each [_ {: message} (ipairs file.compile-errors)]
(case (message:match "unknown identifier: ([a-zA-Z0-9_-]+)") (case (message:match "unknown identifier: ([a-zA-Z0-9_-]+)")
identifier (add-completion! identifier {} :Variable)))) identifier (add-completion! identifier {} :Variable))))
@ -145,8 +146,8 @@ is set to true and we report that we support completionItem/resolve."
(binding-completions) (binding-completions)
(expression-completions))) (expression-completions)))
(if server.can-do-good-completions? (if server.client-capable-of-good-completions?
{:itemDefaults {:editRange (if server.can-do-insert-replace-completions? {:itemDefaults {:editRange (if server.client-capable-of-insert-replace-completions?
{:insert {:start range.start :end position} {:insert {:start range.start :end position}
:replace range} :replace range}
range) range)
@ -157,7 +158,7 @@ is set to true and we report that we support completionItem/resolve."
(fn completionItem/resolve [server _send completion-item] (fn completionItem/resolve [server _send completion-item]
(let [{: uri : byte} completion-item.data (let [{: uri : byte} completion-item.data
file (files.get-by-uri server uri) file (files.get-by-uri server uri)
(_symbol parents) (analyzer.find-symbol file.ast byte) (_symbol parents) (analyzer.find-symbol server file byte)
scope (or (. file.scopes _symbol) scope (or (. file.scopes _symbol)
(accumulate [?find nil _ parent (ipairs parents) &until ?find] (accumulate [?find nil _ parent (ipairs parents) &until ?find]
(. file.scopes parent)) (. file.scopes parent))

View File

@ -126,13 +126,15 @@ However, when not an option, fennel-ls will fall back to positionEncoding=\"utf-
(set server.macro-modules {}) (set server.macro-modules {})
(set server.root-uri params.rootUri) (set server.root-uri params.rootUri)
(set server.position-encoding (choose-position-encoding params)) (set server.position-encoding (choose-position-encoding params))
(set server.can-do-good-completions? (set server.client-capable-of-good-completions?
;; if client supports CompletionClientCapabilites.completionList.itemDefaults.editRange ;; if client supports CompletionClientCapabilites.completionList.itemDefaults.editRange
;; and CompletionClientCapabilites.completionList.itemDefaults.data ;; and CompletionClientCapabilites.completionList.itemDefaults.data
(case (?. params :capabilities :textDocument :completion :completionList :itemDefaults) (case (?. params :capabilities :textDocument :completion :completionList :itemDefaults)
completion-item-defaults (and (accumulate [found nil _ v (ipairs completion-item-defaults) &until found] (= v :editRange)) completion-item-defaults (and (accumulate [found nil _ v (ipairs completion-item-defaults) &until found] (= v :editRange))
(accumulate [found nil _ v (ipairs completion-item-defaults) &until found] (= v :data))))) (accumulate [found nil _ v (ipairs completion-item-defaults) &until found] (= v :data)))))
(set server.can-do-insert-replace-completions? (?. params :capabilities :textDocument :completion :completionItem :insertReplaceSupport)) (set server.client-capable-of-insert-replace-completions? (?. params :capabilities :textDocument :completion :completionItem :insertReplaceSupport))
(set server.client-capable-of-pull-diagnostics? (or (?. params :capabilities :textDocument :diagnostic)
(not (?. params :capabilities :textDocument :publishDiagnostics))))
(reload server)) (reload server))
{: initialize {: initialize

View File

@ -3,7 +3,6 @@ This module has high level helpers for creating/getting \"file\" objects."
(local searcher (require :fennel-ls.searcher)) (local searcher (require :fennel-ls.searcher))
(local utils (require :fennel-ls.utils)) (local utils (require :fennel-ls.utils))
(local {: compile} (require :fennel-ls.compiler))
(λ read-file [server uri] (λ read-file [server uri]
;; preload is here so that tests can inject files ;; preload is here so that tests can inject files
@ -23,7 +22,6 @@ This module has high level helpers for creating/getting \"file\" objects."
(or (. server.files uri) (or (. server.files uri)
(case (read-file server uri) (case (read-file server uri)
file (do file (do
(compile server file)
(tset server.files uri file) (tset server.files uri file)
file)))) file))))
@ -44,21 +42,9 @@ This module has high level helpers for creating/getting \"file\" objects."
(get-by-uri server uri)))))) (get-by-uri server uri))))))
(λ set-uri-contents [server uri text] (λ set-uri-contents [server uri text]
(case (. server.files uri) (let [file {: uri : text}]
;; modify existing file (tset server.files uri file)
file file))
(do
(when (not= text file.text)
(set file.text text)
(compile server file))
file)
;; create new file
nil
(let [file {: uri : text}]
(tset server.files uri file)
(compile server file)
file)))
(λ flush-uri [server uri] (λ flush-uri [server uri]
"get rid of data about a file, in case it changed in some way" "get rid of data about a file, in case it changed in some way"

View File

@ -209,9 +209,9 @@ fntype is one of fn or λ or lambda"
metadata (?. kinds metadata.fls/itemKind)) metadata (?. kinds metadata.fls/itemKind))
(if (name:find "%.") kinds.Field kinds.Value))] (if (name:find "%.") kinds.Field kinds.Value))]
{:label name {:label name
:documentation (when (not server.can-do-good-completions?) (hover-format server name definition)) :documentation (when (not server.client-capable-of-good-completions?) (hover-format server name definition))
:sortText (sort-text name kind) :sortText (sort-text name kind)
:textEdit (when (not server.can-do-good-completions?) {:newText name : range}) :textEdit (when (not server.client-capable-of-good-completions?) {:newText name : range})
: kind})) : kind}))
{: signature-help-format {: signature-help-format

View File

@ -26,7 +26,7 @@ Every time the client sends a message, it gets handled by a function in the corr
:save true} :save true}
;; :notebookDocumentSync nil ;; :notebookDocumentSync nil
:completionProvider {:workDoneProgress false :completionProvider {:workDoneProgress false
:resolveProvider server.can-do-good-completions? :resolveProvider server.client-capable-of-good-completions?
:triggerCharacters ["(" "[" "{" :triggerCharacters ["(" "[" "{"
;; The LSP spec claims that "characters that make up identifiers don't need to be listed here" ;; The LSP spec claims that "characters that make up identifiers don't need to be listed here"
;; > https://github.com/microsoft/language-server-protocol/blob/4a4ff53db00d8c5e57630ff364dedf1918cbb612/_specifications/lsp/3.18/language/completion.md?plain=1#L205 ;; > https://github.com/microsoft/language-server-protocol/blob/4a4ff53db00d8c5e57630ff364dedf1918cbb612/_specifications/lsp/3.18/language/completion.md?plain=1#L205
@ -51,7 +51,7 @@ Every time the client sends a message, it gets handled by a function in the corr
;; :documentFormattingProvider {:workDoneProgress false} ;; :documentFormattingProvider {:workDoneProgress false}
;; :documentRangeFormattingProvider nil ;; :documentRangeFormattingProvider nil
;; :documentOnTypeFormattingProvider nil ;; :documentOnTypeFormattingProvider nil
:renameProvider {:workDoneProgress false}}] :renameProvider {:workDoneProgress false}
;; :foldingRangeProvider nil ;; :foldingRangeProvider nil
;; :executeCommandProvider nil ;; :executeCommandProvider nil
;; :selectionRangeProvider nil ;; :selectionRangeProvider nil
@ -62,8 +62,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
;; ;; this is for PULL diagnostics, but fennel-ls currently does PUSH diagnostics :diagnosticProvider {:workDoneProgress false}}]
;; :diagnosticProvider {:workDoneProgress false}})
;; :workspaceSymbolProvider nil ;; :workspaceSymbolProvider nil
;; :workspace {:workspaceFolders nil ;; :workspace {:workspaceFolders nil
;; :fileOperations {:didCreate nil ;; :fileOperations {:didCreate nil
@ -79,7 +78,7 @@ Every time the client sends a message, it gets handled by a function in the corr
(λ requests.textDocument/definition [server _send {: position :textDocument {: uri}}] (λ requests.textDocument/definition [server _send {: position :textDocument {: uri}}]
(let [file (files.get-by-uri server uri) (let [file (files.get-by-uri server uri)
byte (utils.position->byte file.text position server.position-encoding)] byte (utils.position->byte file.text position server.position-encoding)]
(case-try (analyzer.find-symbol file.ast byte) (case-try (analyzer.find-symbol server file byte)
(symbol [parent]) (symbol [parent])
(let [search-target (if (. file.require-calls parent) parent symbol)] (let [search-target (if (. file.require-calls parent) parent symbol)]
(analyzer.search server file search-target {:stop-early? true} {: byte})) (analyzer.search server file search-target {:stop-early? true} {: byte}))
@ -95,7 +94,7 @@ Every time the client sends a message, it gets handled by a function in the corr
:textDocument {: uri}}] :textDocument {: uri}}]
(let [this-file (files.get-by-uri server uri) (let [this-file (files.get-by-uri server uri)
byte (utils.position->byte this-file.text position server.position-encoding)] byte (utils.position->byte this-file.text position server.position-encoding)]
(match-try (analyzer.find-symbol this-file.ast byte) (match-try (analyzer.find-symbol server this-file byte)
symbol symbol
(analyzer.find-nearest-definition server this-file symbol byte) (analyzer.find-nearest-definition server this-file symbol byte)
{: referenced-by :file {:uri this-file.uri &as file} : binding} {: referenced-by :file {:uri this-file.uri &as file} : binding}
@ -113,7 +112,7 @@ Every time the client sends a message, it gets handled by a function in the corr
include-declaration?}}] include-declaration?}}]
(let [file (files.get-by-uri server uri) (let [file (files.get-by-uri server uri)
byte (utils.position->byte file.text position server.position-encoding)] byte (utils.position->byte file.text position server.position-encoding)]
(case-try (analyzer.find-symbol file.ast byte) (case-try (analyzer.find-symbol server file byte)
symbol symbol
(analyzer.find-nearest-definition server file symbol byte) (analyzer.find-nearest-definition server file symbol byte)
{: referenced-by : file : binding} {: referenced-by : file : binding}
@ -146,7 +145,7 @@ Every time the client sends a message, it gets handled by a function in the corr
(λ requests.textDocument/hover [server _send {: position :textDocument {: uri}}] (λ requests.textDocument/hover [server _send {: position :textDocument {: uri}}]
(let [file (files.get-by-uri server uri) (let [file (files.get-by-uri server uri)
byte (utils.position->byte file.text position server.position-encoding)] byte (utils.position->byte file.text position server.position-encoding)]
(case-try (analyzer.find-symbol file.ast byte) (case-try (analyzer.find-symbol server file byte)
(symbol parents) (analyzer.search server file symbol {} {: byte}) (symbol parents) (analyzer.search server file symbol {} {: byte})
{:indeterminate nil &as result} {:indeterminate nil &as result}
(let [opts {:macroexpansion (case-try parents (let [opts {:macroexpansion (case-try parents
@ -166,7 +165,7 @@ Every time the client sends a message, it gets handled by a function in the corr
(λ requests.textDocument/rename [server _send {: position :textDocument {: uri} :newName new-name}] (λ requests.textDocument/rename [server _send {: position :textDocument {: uri} :newName new-name}]
(let [file (files.get-by-uri server uri) (let [file (files.get-by-uri server uri)
byte (utils.position->byte file.text position server.position-encoding)] byte (utils.position->byte file.text position server.position-encoding)]
(case-try (analyzer.find-symbol file.ast byte) (case-try (analyzer.find-symbol server file byte)
symbol symbol
(analyzer.find-nearest-definition server file symbol symbol.bytestart) (analyzer.find-nearest-definition server file symbol symbol.bytestart)
;; TODO we are assuming that every reference is in the same file ;; TODO we are assuming that every reference is in the same file
@ -206,7 +205,8 @@ Every time the client sends a message, it gets handled by a function in the corr
(let [file (files.get-by-uri server uri) (let [file (files.get-by-uri server uri)
byte (utils.position->byte file.text range.start server.position-encoding) byte (utils.position->byte file.text range.start server.position-encoding)
results []] results []]
(case-try (analyzer.find-symbol file.ast byte) (lint.add-lint-diagnostics server file)
(case-try (analyzer.find-symbol server file byte)
(symbol_ [[symbol_ &as parent]]) file.macro-calls (symbol_ [[symbol_ &as parent]]) file.macro-calls
{parent expansion} (table.insert results {parent expansion} (table.insert results
{:title "Expand macro" {:title "Expand macro"
@ -216,16 +216,25 @@ Every time the client sends a message, it gets handled by a function in the corr
(if (overlap? diagnostic.range range) (if (overlap? diagnostic.range range)
(message.diagnostic->code-action server file diagnostic :quickfix))))) (message.diagnostic->code-action server file diagnostic :quickfix)))))
(λ requests.textDocument/diagnostic [server _send {:textDocument {: uri}}]
(let [file (files.get-by-uri server uri)]
(lint.add-lint-diagnostics server file)
{:kind "full"
:items file.diagnostics}))
(fn push-diagnostics [server file send]
(when (not server.client-capable-of-pull-diagnostics?)
(lint.add-lint-diagnostics server file)
(send (message.diagnostics file))))
(λ notifications.textDocument/didChange [server send {: contentChanges :textDocument {: uri}}] (λ notifications.textDocument/didChange [server send {: contentChanges :textDocument {: uri}}]
(local file (files.get-by-uri server uri)) (local file (files.get-by-uri server uri))
(files.set-uri-contents server uri (utils.apply-changes file.text contentChanges server.position-encoding)) (files.set-uri-contents server uri (utils.apply-changes file.text contentChanges server.position-encoding))
(lint.add-lint-diagnostics server file) (push-diagnostics server file send))
(send (message.diagnostics file)))
(λ notifications.textDocument/didOpen [server send {:textDocument {: text : uri}}] (λ notifications.textDocument/didOpen [server send {:textDocument {: text : uri}}]
(local file (files.set-uri-contents server uri text)) (local file (files.set-uri-contents server uri text))
(lint.add-lint-diagnostics server file) (push-diagnostics server file send)
(send (message.diagnostics file))
(set file.open? true)) (set file.open? true))
(λ notifications.textDocument/didSave [server _send {:textDocument {: uri}}] (λ notifications.textDocument/didSave [server _send {:textDocument {: uri}}]

View File

@ -14,6 +14,7 @@ You can read more about how to add lints in docs/linting.md"
(local navigate (require :fennel-ls.navigate)) (local navigate (require :fennel-ls.navigate))
(local docs (require :fennel-ls.docs)) (local docs (require :fennel-ls.docs))
(local dkjson (require :dkjson)) (local dkjson (require :dkjson))
(local compiler (require :fennel-ls.compiler))
(fn special? [item] (fn special? [item]
@ -855,30 +856,33 @@ You can read more about how to add lints in docs/linting.md"
(setmetatable {: self : fix} lint-mt))) (setmetatable {: self : fix} lint-mt)))
(λ add-lint-diagnostics [server file] (λ add-lint-diagnostics [server file]
(fn run [lints ...] (when (not file.diagnostics)
(each [_ lint (ipairs lints)] (compiler.compile server file)
(when (. server.configuration.lints lint.name) (set file.diagnostics file.compile-errors)
(case (lint.impl ...) (fn run [lints ...]
diagnostic (each [_ lint (ipairs lints)]
(table.insert file.diagnostics (when (. server.configuration.lints lint.name)
(wrap (doto diagnostic (case (lint.impl ...)
(tset :code lint.name)))))))) diagnostic
(icollect [diagnostic (coroutine.wrap #(run lints.other server file)) &into file.diagnostics] (table.insert file.diagnostics
(wrap diagnostic)) (wrap (doto diagnostic
(each [symbol definition (pairs file.definitions)] (tset :code lint.name))))))))
(when (. file.lexical symbol) (icollect [diagnostic (coroutine.wrap #(run lints.other server file)) &into file.diagnostics]
(run lints.definition server file symbol definition))) (wrap diagnostic))
(each [symbol (pairs file.references)] (each [symbol definition (pairs file.definitions)]
(when (. file.lexical symbol) (when (. file.lexical symbol)
(run lints.reference server file symbol))) (run lints.definition server file symbol definition)))
(each [[head &as ast] (pairs file.calls)] (each [symbol (pairs file.references)]
(when (and (. file.lexical ast) (not= nil head)) (when (. file.lexical symbol)
(run (if (special? head) lints.special-call lints.function-call) (run lints.reference server file symbol)))
server file ast))) (each [[head &as ast] (pairs file.calls)]
(each [ast macroexpanded (pairs file.macro-calls)] (when (and (. file.lexical ast) (not= nil head))
(when (. file.lexical ast) (run (if (special? head) lints.special-call lints.function-call)
(run lints.macro-call server file ast)))
server file ast macroexpanded)))) (each [ast macroexpanded (pairs file.macro-calls)]
(when (. file.lexical ast)
(run lints.macro-call
server file ast macroexpanded)))))
{: add-lint-diagnostics {: add-lint-diagnostics
:list all-lints} :list all-lints}

View File

@ -21,7 +21,8 @@
i))) i)))
(fn check [file-contents expected unexpected ?opts] (fn check [file-contents expected unexpected ?opts]
(let [{: diagnostics} (create-client file-contents nil ?opts)] (let [{: uri : client} (create-client file-contents nil ?opts)
[{:result {:items diagnostics}}] (client:diagnostic uri)]
(each [_ e (ipairs unexpected)] (each [_ e (ipairs unexpected)]
(let [i (find diagnostics e)] (let [i (find diagnostics e)]
(faith.= nil i (.. "Lint matching " (view e) "\n" (faith.= nil i (.. "Lint matching " (view e) "\n"

View File

@ -22,7 +22,8 @@
i)))) i))))
(fn check [file-contents expected ?unexpected] (fn check [file-contents expected ?unexpected]
(let [{: diagnostics} (create-client file-contents)] (let [{: uri : client} (create-client file-contents)
[{:result {:items diagnostics}}] (client:diagnostic uri)]
(each [_ e (ipairs (or ?unexpected []))] (each [_ e (ipairs (or ?unexpected []))]
(let [i (find diagnostics e)] (let [i (find diagnostics e)]
(faith.= nil i (.. "Lint matching " (view e) "\n" (faith.= nil i (.. "Lint matching " (view e) "\n"
@ -38,7 +39,8 @@
(table.remove diagnostics i))))) (table.remove diagnostics i)))))
(fn assert-ok [file-contents] (fn assert-ok [file-contents]
(let [{: diagnostics} (create-client file-contents)] (let [{: uri : client} (create-client file-contents)
[{:result {:items diagnostics}}] (client:diagnostic uri)]
(faith.= nil (next diagnostics) (view diagnostics)))) (faith.= nil (next diagnostics) (view diagnostics))))
(fn test-unused [] (fn test-unused []

View File

@ -19,7 +19,7 @@
(fn test-find-symbol [] (fn test-find-symbol []
(let [{: server : uri} (create-client "(match [1 2 4] [1 2 sym-one] sym-one)") (let [{: server : uri} (create-client "(match [1 2 4] [1 2 sym-one] sym-one)")
file (. server.files uri) file (. server.files uri)
(symbol parents) (analyzer.find-symbol file.ast 23)] (symbol parents) (analyzer.find-symbol server file 23)]
(faith.= symbol (fennel.sym :sym-one)) (faith.= symbol (fennel.sym :sym-one))
(faith.= (faith.=
"[[1 2 sym-one] (match [1 2 4] [1 2 sym-one] sym-one) [(match [1 2 4] [1 2 sym-one] sym-one)]]" "[[1 2 sym-one] (match [1 2 4] [1 2 sym-one] sym-one) [(match [1 2 4] [1 2 sym-one] sym-one)]]"
@ -28,7 +28,7 @@
(let [{: server : uri} (create-client "(match [1 2 4] [1 2 sym-one] sym-one)") (let [{: server : uri} (create-client "(match [1 2 4] [1 2 sym-one] sym-one)")
file (. server.files uri) file (. server.files uri)
(symbol parents) (analyzer.find-symbol file.ast 18)] (symbol parents) (analyzer.find-symbol server file 18)]
(faith.= symbol nil) (faith.= symbol nil)
(faith.= (faith.=
"[[1 2 sym-one] (match [1 2 4] [1 2 sym-one] sym-one) [(match [1 2 4] [1 2 sym-one] sym-one)]]" "[[1 2 sym-one] (match [1 2 4] [1 2 sym-one] sym-one) [(match [1 2 4] [1 2 sym-one] sym-one)]]"

View File

@ -12,6 +12,10 @@
(faith.= location response.result (faith.= location response.result
"error message"))) "error message")))
(fn get-diagnostics [file-contents]
(let [{: uri : client} (create-client file-contents)]
(. (client:diagnostic uri) 1 :result :items)))
;; TODO fix macros to use a custom searcher ;; TODO fix macros to use a custom searcher
; (let [{: diagnostics} ; (let [{: diagnostics}
; (create-client ; (create-client
@ -29,10 +33,10 @@
;; (local client (doto [] ({:settings {:fennel-ls {:fennel-path "./?/?/?/?.fnl"}})))) ;; (local client (doto [] ({:settings {:fennel-ls {:fennel-path "./?/?/?/?.fnl"}}))))
(fn test-extra-globals [] (fn test-extra-globals []
(let [{:diagnostics good} (create-client {:main.fnl "(foo-100 bar :baz)" (let [good (get-diagnostics {:main.fnl "(foo-100 bar :baz)"
:flsproject.fnl "{:extra-globals \"foo-100 bar\"}"}) :flsproject.fnl "{:extra-globals \"foo-100 bar\"}"})
{:diagnostics bad} (create-client {:main.fnl "(foo-100 bar :baz)" bad (get-diagnostics {:main.fnl "(foo-100 bar :baz)"
:flsproject.fnl "{}"})] :flsproject.fnl "{}"})]
(faith.= [] good) (faith.= [] good)
(faith.not= [] bad)) (faith.not= [] bad))
nil) nil)
@ -44,10 +48,10 @@
;; (local client (doto [] (setup-server {:fennel-ls {:diagnostics {:E202 "warning"}}}))))) ;; (local client (doto [] (setup-server {:fennel-ls {:diagnostics {:E202 "warning"}}})))))
(fn test-lints [] (fn test-lints []
(let [{:diagnostics good} (create-client {:main.fnl "(local x 10)" (let [good (get-diagnostics {:main.fnl "(local x 10)"
:flsproject.fnl "{:lints {:unused-definition false}}"}) :flsproject.fnl "{:lints {:unused-definition false}}"})
{:diagnostics bad} (create-client {:main.fnl "(local x 10)" bad (get-diagnostics {:main.fnl "(local x 10)"
:flsproject.fnl "{}"})] :flsproject.fnl "{}"})]
(faith.= [] good) (faith.= [] good)
(faith.not= [] bad)) (faith.not= [] bad))
nil) nil)
@ -76,20 +80,20 @@
(faith.match "Could not find docset for library nasilemak" (faith.match "Could not find docset for library nasilemak"
show.params.message) show.params.message)
;; diagnostic ;; diagnostic
(let [[diagnostics] (client:open-file! (.. client.server.root-uri "/" :flsproject.fnl) "{:libraries {:nasilemak true}}")] (client:open-file! (.. client.server.root-uri "/" :flsproject.fnl) "{:libraries {:nasilemak true}}")
(faith.= "textDocument/publishDiagnostics" diagnostics.method) (let [[diagnostics] (client:diagnostic (.. client.server.root-uri "/" :flsproject.fnl))]
(faith.match "Could not find docset for library nasilemak" (. diagnostics.params.diagnostics 1 :message)))) (faith.match "Could not find docset for library nasilemak" (. diagnostics.result.items 1 :message))))
nil) nil)
(fn test-infinite-macro [] (fn test-infinite-macro []
(when (not debug.debug) (when (not debug.debug)
(faith.skip)) (faith.skip))
(let [{: diagnostics} (create-client {:main.fnl "(macro infinite [] (while true nil))\n(infinite)\n" (let [diagnostics (get-diagnostics {:main.fnl "(macro infinite [] (while true nil))\n(infinite)\n"
:flsproject.fnl "{:compiler-instruction-limit 25000}"})] :flsproject.fnl "{:compiler-instruction-limit 25000}"})]
(faith.= "instruction limit reached" (faith.= "instruction limit reached"
(?. diagnostics 1 :message))) (?. diagnostics 1 :message)))
(let [{: diagnostics} (create-client {:main.fnl "(macro finite [] (while false nil))\n(finite)\n" (let [diagnostics (get-diagnostics {:main.fnl "(macro finite [] (while false nil))\n(finite)\n"
:flsproject.fnl "{:compiler-instruction-limit 25000}"})] :flsproject.fnl "{:compiler-instruction-limit 25000}"})]
(faith.= [] diagnostics)) (faith.= [] diagnostics))
nil) nil)

View File

@ -75,6 +75,14 @@
:textDocument {:uri file} :textDocument {:uri file}
: newName}))) : newName})))
(fn diagnostic [self file ?previousResultId]
(dispatch.handle* self.server
(message.create-request (next-id! self) :textDocument/diagnostic
(let [params {:textDocument {:uri file}}]
(when ?previousResultId
(set params.previousResultId ?previousResultId))
params))))
(fn code-action [self file range] (fn code-action [self file range]
(dispatch.handle* self.server (dispatch.handle* self.server
(message.create-request (next-id! self) :textDocument/codeAction (message.create-request (next-id! self) :textDocument/codeAction
@ -100,6 +108,7 @@
: signature-help : signature-help
: rename : rename
: code-action : code-action
: diagnostic
: did-save}}) : did-save}})
{: client-mt {: client-mt

View File

@ -89,10 +89,10 @@
: params}) : params})
_ (each [k v (pairs (or ?config []))] _ (each [k v (pairs (or ?config []))]
(tset server.configuration k v)) (tset server.configuration k v))
[{:params {: diagnostics}}] (client:open-file! uri text)] ?diagnostics (?. (client:open-file! uri text) 1 :params :diagnostics)]
{: client {: client
: server : server
: diagnostics :diagnostics ?diagnostics
: cursor : cursor
: locations : locations
: highlights : highlights