First attempt at code actions, no tests yet

This commit is contained in:
XeroOl 2024-03-26 14:12:34 -05:00
parent ca2dbf9237
commit 7c81ffcbb3
6 changed files with 90 additions and 28 deletions

View File

@ -10,6 +10,7 @@
:method "initialize"
:params {:capabilities {:general {:positionEncodings [:utf-8]}}
:clientInfo {:name "fennel-ls"}
; " don't think this is a valid URI, but I want to operate in the current directory
:rootUri "file://."}}))]
(var should-err? false)
(each [_ filename (ipairs filenames)]

View File

@ -9,6 +9,10 @@ the `file.diagnostics` field, filling it with diagnostics."
(local {:scopes {:global {: specials}}}
(require :fennel.compiler))
(local diagnostic-mt {:__json_exclude_keys {:quickfix true}})
(fn diagnostic [self]
(setmetatable self diagnostic-mt))
(local ops {"+" 1 "-" 1 "*" 1 "/" 1 "//" 1 "%" 1 "^" 1 ">" 1 "<" 1 ">=" 1 "<=" 1 "=" 1 "not=" 1 ".." 1 "." 1 "and" 1 "or" 1 "band" 1 "bor" 1 "bxor" 1 "bnot" 1 "lshift" 1 "rshift" 1})
(fn special? [item]
(and (sym? item)
@ -28,11 +32,14 @@ the `file.diagnostics` field, filling it with diagnostics."
&until reference]
(or (= ref.ref-type :read)
(= ref.ref-type :mutate)))))
{:range (message.ast->range self file symbol)
:message (.. "unused definition: " (tostring symbol))
:severity message.severity.WARN
:code 301
:codeDescription "unused-definition"}))
(diagnostic
{:range (message.ast->range self file symbol)
:message (.. "unused definition: " (tostring symbol))
:severity message.severity.WARN
:code 301
:codeDescription "unused-definition"
:quickfix #[{:range (message.ast->range symbol)
:newText (.. "_" (tostring symbol))}]})))
(λ unknown-module-field [self file]
"any multisym whose definition can't be found through a (require) call"
@ -73,15 +80,21 @@ the `file.diagnostics` field, filling it with diagnostics."
(sym? (. last-item 1) :table.unpack))
(. file.lexical last-item)
(. file.lexical call))
{:range (message.ast->range self file last-item)
:message (.. "faulty unpack call: " (tostring op) " isn't variadic at runtime."
(if (sym? op "..")
(let [unpackme (view (. last-item 2))]
(.. " Use (table.concat " unpackme ") instead of (.. (unpack " unpackme "))"))
(.. " Use a loop when you have a dynamic number of arguments to (" (tostring op) ")")))
:severity message.severity.WARN
:code 304
:codeDescription "bad-unpack"})))
(diagnostic
{:range (message.ast->range self file last-item)
:message (.. "faulty unpack call: " (tostring op) " isn't variadic at runtime."
(if (sym? op "..")
(let [unpackme (view (. last-item 2))]
(.. " Use (table.concat " unpackme ") instead of (.. (unpack " unpackme "))"))
(.. " Use a loop when you have a dynamic number of arguments to (" (tostring op) ")")))
:severity message.severity.WARN
:code 304
:codeDescription "bad-unpack"
:quickfix (if (and (= (length call) 2)
(= (length (. call 2)) 2)
(sym? op ".."))
#[{:range (message.ast->range self file call)
:newText (.. "(table.concat " (view (. call 2 2)) ")")}])}))))
(λ var-never-set [self file symbol definition]
(if (and definition.var? (not definition.var-set) (. file.lexical symbol))
@ -94,15 +107,23 @@ the `file.diagnostics` field, filling it with diagnostics."
(local op-identity-value {:+ 0 :* 1 :and true :or false :band -1 :bor 0 :.. ""})
(λ op-with-no-arguments [self file op call]
"A call like (+) that could be replaced with a literal"
(if (and (op? op)
(not (. call 2))
(. file.lexical call)
(not= nil (. op-identity-value (tostring op))))
{:range (message.ast->range self file call)
:message (.. "write " (view (. op-identity-value (tostring op))) " instead of (" (tostring op) ")")
:severity message.severity.WARN
:code 306
:codeDescription "op-with-no-arguments"}))
(let [identity (. op-identity-value (tostring op))]
(if (and (op? op)
(not (. call 2))
(. file.lexical call)
(not= nil identity))
(diagnostic
{:range (message.ast->range self file call)
:message (.. "write " (view identity) " instead of (" (tostring op) ")")
:severity message.severity.WARN
:code 306
:codeDescription "op-with-no-arguments"
:quickfix #[{:range (message.ast->range self file call)
:newText (view identity)}]}))))
; (fn quickfix.op-with-no-arguments [item]
; {:range item.range
; :newText (tostring item.data)})
(λ multival-in-middle-of-call [self file fun call arg index]
"generally, values and unpack are signs that the user is trying to do

View File

@ -11,7 +11,6 @@ Every time the client sends a message, it gets handled by a function in the corr
(local language (require :fennel-ls.language))
(local formatter (require :fennel-ls.formatter))
(local utils (require :fennel-ls.utils))
(local fennel (require :fennel))
(local requests [])
@ -33,7 +32,7 @@ Every time the client sends a message, it gets handled by a function in the corr
:referencesProvider {:workDoneProgress false}
;; :documentHighlightProvider nil
;; :documentSymbolProvider nil
;; :codeActionProvider nil
:codeActionProvider {:workDoneProgress false}
;; :codeLensProvider nil
;; :documentLinkProvider nil
;; :colorProvider nil
@ -229,6 +228,23 @@ Every time the client sends a message, it gets handled by a function in the corr
{:changes {definition.file.uri usages}})
(catch _ nil))))
(fn pos<= [pos-1 pos-2]
(or (< pos-1.line pos-2.line)
(and (= pos-1.line pos-2.line)
(<= pos-1.character pos-2.character))))
(fn overlap? [range-1 range-2]
(and (pos<= range-1.start range-2.end)
(pos<= range-2.start range-1.end)))
(λ requests.textDocument/codeAction [self send {: range :textDocument {: uri} &as params}]
(let [file (state.get-by-uri self uri)]
(icollect [_ diagnostic (ipairs file.diagnostics)]
(if (and (overlap? diagnostic.range range)
diagnostic.quickfix)
{:title diagnostic.codeDescription
:edit {:changes {uri (diagnostic.quickfix)}}}))))
(λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}]
(local file (state.get-by-uri self uri))
(state.set-uri-contents self uri (utils.apply-changes file.text contentChanges self.position-encoding))
@ -243,12 +259,12 @@ Every time the client sends a message, it gets handled by a function in the corr
(λ 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 []))
(set fennel.macro-loaded []))
(λ notifications.textDocument/didClose [self send {:textDocument {: uri}}]
(local file (state.get-by-uri self uri))
(set file.open? false)
(tset (require :fennel) :macro-loaded [])
(set fennel.macro-loaded [])
;; TODO only reload from disk if we didn't get a didSave, instead of always
(state.flush-uri self uri))

15
test/code-action.fnl Normal file
View File

@ -0,0 +1,15 @@
(local _faith (require :faith))
(local {: view} (require :fennel))
(local {: create-client-with-files} (require :test.utils))
(fn check [file-contents]
(let [{: self : uri :locations [range]} (create-client-with-files file-contents)
response (self:code-action uri range.range)]
;; (print "hello" (view response))))
nil))
(fn test-thing []
(check "(+====)"
"op-with-no-arguments"))
{: test-thing}

View File

@ -21,5 +21,6 @@
:test.completion
:test.references
:test.diagnostic
:test.code-action
:test.rename
:test.misc])

View File

@ -86,6 +86,13 @@
:textDocument {:uri file}
: newName})))
(fn code-action [self file range]
(dispatch.handle* self.server
(message.create-request (next-id! self) :textDocument/codeAction
{: range
:textDocument {:uri file}
:context {:diagnostics []}})))
(set mt.__index
{: open-file!
: pretend-this-file-exists!
@ -93,7 +100,8 @@
: definition
: hover
: references
: rename})
: rename
: code-action})
{: create-client
: default-encoding