diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index ccdc384..61b4158 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -108,7 +108,7 @@ Every time the client sends a message, it gets handled by a function in the corr symbol (analyzer.find-nearest-definition server this-file symbol byte) {: referenced-by :file {:uri this-file.uri &as file} : binding} - (let [result (icollect [_ {:symbol reference} (ipairs referenced-by)] + (let [result (icollect [_ {:symbol reference} (ipairs referenced-by) &into (message.array)] {:range (message.ast->range server file reference) :kind documentHighlightKind.Read})] (table.insert result {:range (message.ast->range server file binding) @@ -142,12 +142,12 @@ Every time the client sends a message, it gets handled by a function in the corr (let [file (files.get-by-uri server uri) byte (utils.position->byte file.text position server.position-encoding)] (case-try (analyzer.find-nearest-call server file byte) - (symbol active-parameter) - (analyzer.find-definition server file symbol) + (call active-parameter) + (analyzer.find-definition server file call) {:indeterminate nil &as definition} (formatter.signature-help-format definition) signature - (message.symbol->signature-help server file symbol + (message.call->signature-help server file call signature active-parameter) (catch _ nil)))) @@ -207,9 +207,8 @@ Every time the client sends a message, it gets handled by a function in the corr (λ requests.textDocument/codeAction [server _send {: range :textDocument {: uri}}] (let [file (files.get-by-uri server uri)] - (icollect [_ diagnostic (ipairs file.diagnostics)] - (if (and (overlap? diagnostic.range range) - diagnostic.quickfix) + (icollect [_ diagnostic (ipairs file.diagnostics) &into (message.array)] + (if (overlap? diagnostic.range range) (message.diagnostic->code-action server file diagnostic :quickfix))))) (λ notifications.textDocument/didChange [server send {: contentChanges :textDocument {: uri}}] diff --git a/src/fennel-ls/lint.fnl b/src/fennel-ls/lint.fnl index 2d2f816..71ea540 100644 --- a/src/fennel-ls/lint.fnl +++ b/src/fennel-ls/lint.fnl @@ -13,8 +13,10 @@ the `file.diagnostics` field, filling it with diagnostics." (local diagnostic-mt {:__tojson (fn [{: self} state] (dkjson.encode self state)) :__index #(. $1 :self $2)}) -(fn diagnostic [self quickfix] - (setmetatable {: self : quickfix} diagnostic-mt)) +(fn diagnostic [self] + (let [fix self.fix] + (set self.fix nil) + (setmetatable {: self : fix} diagnostic-mt))) (fn could-be-rewritten-as-sym? [str] (and (= :string (type str)) (not (str:find "^%d")) @@ -33,9 +35,10 @@ the `file.diagnostics` field, filling it with diagnostics." {:range (message.ast->range server file symbol) :message (.. "unused definition: " (tostring symbol)) :severity message.severity.WARN - :code :unused-definition} - #[{:range (message.ast->range server file symbol) - :newText (.. "_" (tostring symbol))}]))) + :code :unused-definition + :fix #{:title (.. "Replace " (tostring symbol) " with _" (tostring symbol)) + :changes [{:range (message.ast->range server file symbol) + :newText (.. "_" (tostring symbol))}]}}))) ;; this is way too specific; it's also safe to do this inside an `if` or `case` (fn in-or? [calls symbol] @@ -107,9 +110,10 @@ the `file.diagnostics` field, filling it with diagnostics." (diagnostic {:range (message.ast->range server file call) :message (.. "unnecessary " (tostring head)) :severity message.severity.WARN - :code :unnecessary-tset} - #[{:range (message.ast->range server file call) - :newText (make-new-text call)}]))) + :code :unnecessary-tset + :fix #{:title "Replace tset with set" + :changes [{:range (message.ast->range server file call) + :newText (make-new-text call)}]}}))) (λ unnecessary-do-values [server file head call] (if (and (or (sym? head :do) (sym? head :values)) @@ -117,26 +121,31 @@ the `file.diagnostics` field, filling it with diagnostics." (diagnostic {:range (message.ast->range server file call) :message (.. "unnecessary " (tostring head)) :severity message.severity.WARN - :code :unnecessary-do-values} - #[{:range (message.ast->range server file call) - :newText (view (. call 2))}]))) + :code :unnecessary-do-values + :fix #{:title "Unwrap the expression" + :changes [{:range (message.ast->range server file call) + :newText (view (. call 2))}]}}))) (local implicit-do-forms (collect [form {: body-form?} (pairs (fennel.syntax))] (values form body-form?))) (λ redundant-do [server file head call] (let [last-body (. call (length call))] - (if (and (. implicit-do-forms (tostring head)) (. file.lexical call) - (list? last-body) (sym? (. last-body 1) :do)) + (if (and (. implicit-do-forms (tostring head)) + (. file.lexical call) + (list? last-body) + (sym? (. last-body 1) :do) + (not (unnecessary-do-values server file head call))) ;; we don't want two lints to trigger for same call (diagnostic {:range (message.ast->range server file last-body) :message "redundant do" :severity message.severity.WARN - :code :redundant-do} - #[{:range (message.ast->range server file last-body) - :newText (table.concat - (fcollect [i 2 (length last-body)] - (view (. last-body i))) - " ")}])))) + :code :redundant-do + :fix #{:title "Unwrap the expression" + :changes [{:range (message.ast->range server file last-body) + :newText (table.concat + (fcollect [i 2 (length last-body)] + (view (. last-body i))) + " ")}]}})))) (λ bad-unpack [server file op call] "an unpack call leading into an operator" @@ -160,12 +169,12 @@ the `file.diagnostics` field, filling it with diagnostics." (.. " Use a loop when you have a dynamic number of " "arguments to (" (tostring op) ")"))) :severity message.severity.WARN - :code :bad-unpack} - (if (and (= (length call) 2) - (= (length (. call 2)) 2) - (sym? op "..")) - #[{:range (message.ast->range server file call) - :newText (.. "(table.concat " (view (. call 2 2)) ")")}]))))) + :code :bad-unpack + :fix (if (and (= (length last-item) 2) + (sym? op "..")) + #{:title "Replace with a call to table.concat" + :changes [{:range (message.ast->range server file (if (= 2 (length call)) call last-item)) + :newText (.. "(table.concat " (view (. last-item 2)) ")")}]})})))) (λ var-never-set [server file symbol definition] (if (and definition.var? (not definition.var-set) (. file.lexical symbol)) @@ -189,9 +198,10 @@ the `file.diagnostics` field, filling it with diagnostics." {:range (message.ast->range server file call) :message (.. "write " (view identity) " instead of (" (tostring op) ")") :severity message.severity.WARN - :code :op-with-no-arguments} - #[{:range (message.ast->range server file call) - :newText (view identity)}])))) + :code :op-with-no-arguments + :fix #{:title (.. "Replace (" (tostring op) ") with " (view identity)) + :changes [{:range (message.ast->range server file call) + :newText (view identity)}]}})))) (λ no-decreasing-comparison [server file op call] (if (or (sym? op :>) (sym? op :>=)) @@ -199,13 +209,14 @@ the `file.diagnostics` field, filling it with diagnostics." {:range (message.ast->range server file call) :message "Use increasing operator instead of decreasing" :severity message.severity.WARN - :code :no-decreasing-comparison} - #[{:range (message.ast->range server file call) - :newText (let [new (if (sym? op :>=) (fennel.sym :<=) (fennel.sym :<)) - reversed (fcollect [i (length call) 2 -1 - &into (list (sym new))] - (. call i))] - (view reversed))}]))) + :code :no-decreasing-comparison + :fix #{:title "Reverse the comparison" + :changes [{:range (message.ast->range server file call) + :newText (let [new (if (sym? op :>=) (fennel.sym :<=) (fennel.sym :<)) + reversed (fcollect [i (length call) 2 -1 + &into (list (sym new))] + (. call i))] + (view reversed))}]}}))) (λ match-reference? [ast references] (if (sym? ast) (?. references ast :target) @@ -221,9 +232,10 @@ the `file.diagnostics` field, filling it with diagnostics." (diagnostic {:range (message.ast->range server file (. ast 1)) :message "no pinned patterns; use case instead of match" :severity message.severity.WARN - :code :match-should-case} - #[{:range (message.ast->range server file (. ast 1)) - :newText "case"}]))) + :code :match-should-case + :fix #{:title "Replace match with case" + :changes [{:range (message.ast->range server file (. ast 1)) + :newText "case"}]}}))) (λ multival-in-middle-of-call [server file fun call arg index] "generally, values and unpack are signs that the user is trying to do @@ -252,11 +264,12 @@ the `file.diagnostics` field, filling it with diagnostics." (diagnostic {:range (message.ast->range server file binding) :message "use do instead of let with no bindings" :severity message.severity.WARN - :code :empty-let} - #[(let [{: start} (message.ast->range server file let*) - {: end} (message.ast->range server file binding)] - {:range {: start : end} - :newText "do"})]))) + :code :empty-let + :fix #{:title "Replace (let [] ...) with (do ...)" + :changes [(let [{: start} (message.ast->range server file let*) + {: end} (message.ast->range server file binding)] + {:range {: start : end} + :newText "do"})]}}))) (λ add-lint-diagnostics [server file] "fill up the file.diagnostics table with linting things" diff --git a/src/fennel-ls/message.fnl b/src/fennel-ls/message.fnl index e8ac597..f0d4223 100644 --- a/src/fennel-ls/message.fnl +++ b/src/fennel-ls/message.fnl @@ -9,6 +9,8 @@ LSP json objects." (local utils (require :fennel-ls.utils)) (local json (require :dkjson)) +(local json-array-mt {:__jsontype :array}) + (λ nullify [?value] (case ?value nil json.null @@ -73,25 +75,18 @@ LSP json objects." :end (utils.byte->position file.text (+ byteend 1) server.position-encoding)})) -(λ code-action-title [diag] - (let [titles {:match-should-case "Replace match with case" - :unused-definition "Prefix with _ to silence warning" - :op-with-no-arguments "Replace with the corresponding literal" - :no-decreasing-comparison "Reverse comparison" - :unnecessary-tset "Replace with set" - :unnecessary-do-values "Remove unnecessary do or values" - :redundant-do "Remove redundant do" - :bad-unpack "Replace with table.concat call"}] - (or (. titles diag.code) - (.. "Action title missing - " diag.code)))) +(λ array [?t] + (setmetatable (or ?t []) json-array-mt)) -(λ diagnostic->code-action [_server file diagnostic ?kind] - (let [{: uri} file] - {:title (code-action-title diagnostic) - :kind ?kind - :edit {:changes {uri (diagnostic.quickfix)}}})) +(λ diagnostic->code-action [_server {: uri} diagnostic ?kind] + (case-try diagnostic.fix + fix (fix) + {: title : changes} {: title + :kind ?kind + :diagnostics [diagnostic] + :edit {:changes {uri (array changes)}}})) -(λ symbol->signature-help [_server _file _call signature active-parameter] +(λ call->signature-help [_server _file _call signature active-parameter] (let [params-count (length signature.parameters)] {:signatures [signature] :activeSignature 0 ; we only ever have one signature @@ -101,8 +96,7 @@ LSP json objects." active-parameter)})) (λ multisym->range [server file ast n] - (let [spl (utils.multi-sym-split ast) - n (if (< n 0) (+ n 1 (length spl)) n)] + (let [spl (utils.multi-sym-split ast)] (case (values (utils.get-ast-info ast :bytestart) (utils.get-ast-info ast :byteend)) (bytestart byteend) @@ -138,10 +132,11 @@ LSP json objects." : create-error : ast->range : diagnostic->code-action - : symbol->signature-help + : call->signature-help : multisym->range : range-and-uri : diagnostics : severity : severity->string - : show-message} + : show-message + : array} diff --git a/test/code-action.fnl b/test/code-action.fnl index 94f277c..a0d7000 100644 --- a/test/code-action.fnl +++ b/test/code-action.fnl @@ -33,32 +33,47 @@ (fn test-fix-op-no-arguments [] (check "(let [x (+====)] (print x))" - "Replace with the corresponding literal" + "Replace (+) with 0" "(let [x 0] - (print x))")) + (print x))") + nil) (fn test-fix-unused-definition [] (check "(local x==== 10)" - "Prefix with _ to silence warning" - "(local _x 10)")) + "Replace x with _x" + "(local _x 10)") + nil) (fn test-unnecessary-tset [] (check "==(tset state :mouse 496)==" - "Replace with set" + "Replace tset with set" "(set state.mouse 496)") (check "==(tset state :mouse :cursor 496)==" - "Replace with set" + "Replace tset with set" "(set state.mouse.cursor 496)") (check "==(tset state :mouse :cursor {:x 4 :y 7})==" - "Replace with set" + "Replace tset with set" "(set state.mouse.cursor {:x 4 :y 7})") (check "==(tset state :mouse :cursor :x 496)==" - "Replace with set" - "(set state.mouse.cursor.x 496)")) + "Replace tset with set" + "(set state.mouse.cursor.x 496)") + nil) + +(fn test-fix-unpack [] + (check "(.. (table.unpack my-ta====ble))" + "Replace with a call to table.concat" + "(table.concat my-table)") + (check "(.. 1 2 3 (table.unpa====ck my-table))" + "Replace with a call to table.concat" + "(.. 1 2 3 (table.concat my-table))") + (check-negative "(+ 1 2 3 (table.unpa====ck my-table))" + "Replace with a call to table.concat") + nil) {: test-fix-op-no-arguments : test-fix-unused-definition - : test-unnecessary-tset} + : test-unnecessary-tset + : test-fix-unpack}