diff --git a/src/fennel-ls.fnl b/src/fennel-ls.fnl index 3316961..dcce084 100644 --- a/src/fennel-ls.fnl +++ b/src/fennel-ls.fnl @@ -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)] diff --git a/src/fennel-ls/diagnostics.fnl b/src/fennel-ls/diagnostics.fnl index 7f4cf0d..7470e3c 100644 --- a/src/fennel-ls/diagnostics.fnl +++ b/src/fennel-ls/diagnostics.fnl @@ -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 diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index 648ffe1..255f4ee 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -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)) diff --git a/test/code-action.fnl b/test/code-action.fnl new file mode 100644 index 0000000..fae02ba --- /dev/null +++ b/test/code-action.fnl @@ -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} diff --git a/test/init.fnl b/test/init.fnl index 7e8d0f2..9761b51 100644 --- a/test/init.fnl +++ b/test/init.fnl @@ -21,5 +21,6 @@ :test.completion :test.references :test.diagnostic + :test.code-action :test.rename :test.misc]) diff --git a/test/utils/client.fnl b/test/utils/client.fnl index d858d7e..6b3c858 100644 --- a/test/utils/client.fnl +++ b/test/utils/client.fnl @@ -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