Add primitive support for renaming
This commit is contained in:
parent
9d534c161f
commit
fe257c061a
10
TODO.md
10
TODO.md
@ -7,10 +7,8 @@ My current goal is to work on completions a little bit more.
|
||||
|
||||
Here is my feature wishlist. I don't expect to ever get all of this done, but these are the sort of enhancements I am thinking about.
|
||||
- [X] Able to connect to a client
|
||||
- [ ] Support for UTF-8 characters that aren't just plain ASCII. (especially `λ`) (perhaps just tell the IDE that I want to communicate with utf-8 offsets)
|
||||
- [ ] Settings to configure lua / fennel path, allowed globals, etc
|
||||
- [ ] Supporting builds for anything other than arch linux
|
||||
- [ ] Testing/support/instructions for any clients: (LSP is supposed to be editor-agnostic, but that's only if you're able to actually follow the spec, and I'm not sure that fennel-ls is compliant)
|
||||
- [X] Support for UTF-8 characters that aren't just plain ASCII. (especially `λ`) (perhaps just tell the IDE that I want to communicate with utf-8 offsets)
|
||||
- [ ] People have tried fennel-ls in:
|
||||
- [X] Neovim (This project isn't a neovim plugin, but there are instructions on how to inform neovim of the fennel-ls binary once you build it.)
|
||||
- [X] emacs
|
||||
- [X] helix
|
||||
@ -86,8 +84,8 @@ Here is my feature wishlist. I don't expect to ever get all of this done, but th
|
||||
- [ ] lua version
|
||||
- [ ] allowed global list
|
||||
- [X] enable/disable various linters
|
||||
- [ ] rename
|
||||
- [ ] local symbols
|
||||
- [X] rename
|
||||
- [X] local symbols
|
||||
- [ ] module fields (may affect code behavior, may modify other files)
|
||||
- [ ] arbitrary fields (may affect code behavior, may modify other files)
|
||||
- [ ] formatting with fnlfmt
|
||||
|
||||
@ -40,7 +40,7 @@ Every time the client sends a message, it gets handled by a function in the corr
|
||||
;; :documentFormattingProvider nil
|
||||
;; :documentRangeFormattingProvider nil
|
||||
;; :documentOnTypeFormattingProvider nil
|
||||
;; :renameProvider nil
|
||||
:renameProvider {:workDoneProgress false}
|
||||
;; :foldingRangeProvider nil
|
||||
;; :executeCommandProvider nil
|
||||
;; :selectionRangeProvider nil
|
||||
@ -89,17 +89,13 @@ Every time the client sends a message, it gets handled by a function in the corr
|
||||
byte (utils.position->byte file.text position self.position-encoding)]
|
||||
(case-try (language.find-symbol file.ast byte)
|
||||
symbol
|
||||
(if (. file.definitions symbol)
|
||||
(values (. file.definitions symbol) file)
|
||||
(language.search-main self file symbol {:stop-early? true} byte))
|
||||
({: referenced-by &as definition} result-file)
|
||||
(let [result
|
||||
(icollect [_ symbol (ipairs referenced-by)]
|
||||
;; TODO I currently assume all references are in the same file
|
||||
(message.range-and-uri self result-file symbol))]
|
||||
(language.find-nearest-definition self file symbol byte)
|
||||
(where (definition def-file) (not= definition.referenced-by nil))
|
||||
(let [result (icollect [_ symbol (ipairs definition.referenced-by)]
|
||||
(message.range-and-uri self def-file symbol))]
|
||||
(if ?include-declaration?
|
||||
(table.insert result
|
||||
(message.range-and-uri self result-file definition.binding)))
|
||||
(message.range-and-uri self def-file definition.binding)))
|
||||
|
||||
;; TODO don't include duplicates
|
||||
result)
|
||||
@ -138,7 +134,6 @@ Every time the client sends a message, it gets handled by a function in the corr
|
||||
def (formatter.completion-item-format name def)
|
||||
_ {:label name}))
|
||||
|
||||
|
||||
(λ scope-completion [self file byte ?symbol parents]
|
||||
(let [scope (or (accumulate [result nil
|
||||
_ parent (ipairs parents)
|
||||
@ -182,6 +177,24 @@ Every time the client sends a message, it gets handled by a function in the corr
|
||||
(where (or nil [_ nil])) (scope-completion self file byte ?symbol parents)
|
||||
[_a _b &as split] (field-completion self file ?symbol split))))
|
||||
|
||||
(λ requests.textDocument/rename [self send {: position :textDocument {: uri} :newName new-name}]
|
||||
(let [file (state.get-by-uri self uri)
|
||||
byte (utils.position->byte file.text position self.position-encoding)]
|
||||
(case-try (language.find-symbol file.ast byte)
|
||||
symbol
|
||||
(language.find-nearest-definition self file symbol symbol.bytestart)
|
||||
;; TODO we are assuming that every reference is in the same file
|
||||
(where (definition def-file) (not= definition.referenced-by nil))
|
||||
(let [usages (icollect [_ symbol (ipairs definition.referenced-by)
|
||||
&into [{:range (message.multisym->range self def-file definition.binding 1)
|
||||
:newText new-name}]]
|
||||
{:newText new-name
|
||||
:range (message.multisym->range self def-file symbol 1)})]
|
||||
;; NOTE: I don't care about encoding here because we just need the relative positions
|
||||
(table.sort usages #(> (utils.position->byte def-file.text $1.range.start :utf-8)
|
||||
(utils.position->byte def-file.text $2.range.start :utf-8)))
|
||||
{:changes {def-file.uri usages}})
|
||||
(catch _ nil))))
|
||||
|
||||
(λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}]
|
||||
(local file (state.get-by-uri self uri))
|
||||
|
||||
@ -218,7 +218,6 @@ Returns:
|
||||
(recurse key byte)
|
||||
(contains? value byte)
|
||||
(recurse value byte)))))))
|
||||
|
||||
(values
|
||||
(accumulate [result nil i top-level-form (ipairs ast) &until result]
|
||||
(if (contains? top-level-form byte)
|
||||
@ -226,8 +225,13 @@ Returns:
|
||||
(fcollect [i 1 (length parents)]
|
||||
(. parents (- (length parents) i -1)))))
|
||||
|
||||
(λ find-nearest-definition [self file symbol ?byte]
|
||||
(if (. file.definitions symbol)
|
||||
(values (. file.definitions symbol) file)
|
||||
(search-main self file symbol {:stop-early? true} ?byte)))
|
||||
|
||||
{: find-symbol
|
||||
: find-nearest-definition
|
||||
: search-main
|
||||
: search-assignment
|
||||
: search-name-and-scope
|
||||
|
||||
@ -59,6 +59,20 @@ to look to fix this in the future."
|
||||
{:start (utils.byte->position file.text bytestart self.position-encoding)
|
||||
:end (utils.byte->position file.text (+ byteend 1) self.position-encoding)}))
|
||||
|
||||
(λ multisym->range [self file ast n]
|
||||
(let [spl (utils.multi-sym-split ast)]
|
||||
(case (values (utils.get-ast-info ast :bytestart)
|
||||
(utils.get-ast-info ast :byteend))
|
||||
(bytestart byteend)
|
||||
(let [bytesubstart (faccumulate [b bytestart
|
||||
i 1 (- n 1)]
|
||||
(+ b (length (. spl i)) 1))
|
||||
bytesubend (faccumulate [b byteend
|
||||
i (+ n 1) (length spl)]
|
||||
(- b (length (. spl i)) 1))]
|
||||
{:start (utils.byte->position file.text bytesubstart self.position-encoding)
|
||||
:end (utils.byte->position file.text (+ bytesubend 1) self.position-encoding)}))))
|
||||
|
||||
(λ range-and-uri [self {: uri &as file} ?ast]
|
||||
"if possible, returns the location of a symbol"
|
||||
(case (ast->range self file ?ast)
|
||||
@ -75,6 +89,7 @@ to look to fix this in the future."
|
||||
: create-response
|
||||
: create-error
|
||||
: ast->range
|
||||
: multisym->range
|
||||
: range-and-uri
|
||||
: diagnostics
|
||||
: severity}
|
||||
|
||||
@ -119,10 +119,22 @@ These functions are all pure functions, which makes me happy."
|
||||
;; Handle a change
|
||||
{:range {: start : end} : text}
|
||||
(replace contents start end text encoding)
|
||||
;; A replacment of the entire body
|
||||
;; A replacement of the entire body
|
||||
{: text}
|
||||
text)))
|
||||
|
||||
(λ apply-edits [initial-text edits encoding]
|
||||
"Takes a list of Language-Server-Protocol `TextEdit` or `AnnotatedTextEdit` and applies them to a piece of text.
|
||||
|
||||
WARNING: this is only used in the test code, not in the real language server"
|
||||
(accumulate
|
||||
[contents initial-text
|
||||
_ edit (ipairs edits)]
|
||||
(case edit
|
||||
;; Handle a change
|
||||
{:range {: start : end} : newText}
|
||||
(replace contents start end newText encoding))))
|
||||
|
||||
(λ get-ast-info [?ast info]
|
||||
;; find a given key of info from an AST object
|
||||
(or (?. (getmetatable ?ast) info)
|
||||
@ -151,6 +163,7 @@ These functions are all pure functions, which makes me happy."
|
||||
: byte->position
|
||||
: position->byte
|
||||
: apply-changes
|
||||
: apply-edits
|
||||
: multi-sym-split
|
||||
: get-ast-info
|
||||
: type=}
|
||||
|
||||
@ -75,12 +75,20 @@
|
||||
:textDocument {:uri file}
|
||||
:context {:includeDeclaration (not (not ?includeDeclaration))}})))
|
||||
|
||||
(fn rename [self file line character newName]
|
||||
(dispatch.handle* self.server
|
||||
(message.create-request (next-id! self) :textDocument/rename
|
||||
{:position {: line : character}
|
||||
:textDocument {:uri file}
|
||||
: newName})))
|
||||
|
||||
(set mt.__index
|
||||
{: open-file!
|
||||
: completion
|
||||
: definition
|
||||
: hover
|
||||
: references})
|
||||
: references
|
||||
: rename})
|
||||
|
||||
{: create-client
|
||||
: ROOT-URI
|
||||
|
||||
@ -57,8 +57,8 @@
|
||||
(is-matching responses
|
||||
[{:params
|
||||
{:diagnostics
|
||||
[{:range {:start {:character a :line b}
|
||||
:end {:character c :line d}}}]}}]
|
||||
[{:range {:start {:character 0 :line 0}
|
||||
:end {:character 7 :line 0}}}]}}]
|
||||
"diagnostics should always have a range")))
|
||||
|
||||
(it "gives more than one error"
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
(require :test.json-rpc-test)
|
||||
(require :test.misc-test)
|
||||
(require :test.references-test)
|
||||
(require :test.rename-test)
|
||||
(require :test.settings-test)
|
||||
(require :test.string-processing-test)
|
||||
|
||||
|
||||
@ -40,9 +40,9 @@
|
||||
(check-references "(fn x []) x" 0 10
|
||||
[{:uri filename :range (range 0 10 0 11)}]))
|
||||
|
||||
; (it "finds a reference from fn"
|
||||
; (check-references "(fn x []) x" 0 4
|
||||
; [{:uri filename :range (range 0 10 0 11)}]))
|
||||
(it "finds a reference from fn"
|
||||
(check-references "(fn x []) x" 0 4
|
||||
[{:uri filename :range (range 0 10 0 11)}]))
|
||||
|
||||
(it "doesn't crash here"
|
||||
(check-references "(let [x nil] x.y)" 0 14
|
||||
|
||||
40
test/rename-test.fnl
Normal file
40
test/rename-test.fnl
Normal file
@ -0,0 +1,40 @@
|
||||
(import-macros {: is-matching : is-casing : describe : it : before-each} :test)
|
||||
(local utils (require :fennel-ls.utils))
|
||||
(local is (require :test.is))
|
||||
|
||||
(local {: view} (require :fennel))
|
||||
(local {: ROOT-URI
|
||||
: create-client} (require :test.client))
|
||||
|
||||
(local filename (.. ROOT-URI "/imaginary-file.fnl"))
|
||||
|
||||
(fn check-rename [body line col new-name new-body]
|
||||
(let [client (doto (create-client)
|
||||
(: :open-file! filename body))
|
||||
[{: result}] (client:rename filename line col new-name)
|
||||
changes (. result.changes filename)
|
||||
body (. client.server.files filename :text)]
|
||||
(is.equal
|
||||
(utils.apply-edits body changes client.server.position-encoding)
|
||||
new-body)))
|
||||
|
||||
(describe "rename"
|
||||
(it "renames a variable"
|
||||
(check-rename "(let [old-name 100] old-name)" 0 9 :new-name
|
||||
"(let [new-name 100] new-name)"))
|
||||
|
||||
(it "renames a variable 2"
|
||||
(check-rename "(let [old-name 100] (print old-name) (print old-name))" 0 9 :new-name!!
|
||||
"(let [new-name!! 100] (print new-name!!) (print new-name!!))"))
|
||||
|
||||
(it "renames a multisym"
|
||||
(check-rename "(let [old-name {:field 10}] old-name.field)" 0 9 :new-name
|
||||
"(let [new-name {:field 10}] new-name.field)")
|
||||
(check-rename "(let [old-name {:field 10}] old-name.field)" 0 30 :new-name
|
||||
"(let [new-name {:field 10}] new-name.field)"))
|
||||
|
||||
(it "renames from destructure/args"
|
||||
(check-rename "(fn [{: x}] x)" 0 8 :foo "(fn [{: foo}] foo)")
|
||||
(check-rename "(fn [{:x x}] x)" 0 9 :foo "(fn [{:x foo}] foo)")))
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user