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.
|
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
|
- [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)
|
- [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)
|
||||||
- [ ] Settings to configure lua / fennel path, allowed globals, etc
|
- [ ] People have tried fennel-ls in:
|
||||||
- [ ] 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] 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] 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] emacs
|
||||||
- [X] helix
|
- [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
|
- [ ] lua version
|
||||||
- [ ] allowed global list
|
- [ ] allowed global list
|
||||||
- [X] enable/disable various linters
|
- [X] enable/disable various linters
|
||||||
- [ ] rename
|
- [X] rename
|
||||||
- [ ] local symbols
|
- [X] local symbols
|
||||||
- [ ] module fields (may affect code behavior, may modify other files)
|
- [ ] module fields (may affect code behavior, may modify other files)
|
||||||
- [ ] arbitrary fields (may affect code behavior, may modify other files)
|
- [ ] arbitrary fields (may affect code behavior, may modify other files)
|
||||||
- [ ] formatting with fnlfmt
|
- [ ] 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
|
;; :documentFormattingProvider nil
|
||||||
;; :documentRangeFormattingProvider nil
|
;; :documentRangeFormattingProvider nil
|
||||||
;; :documentOnTypeFormattingProvider nil
|
;; :documentOnTypeFormattingProvider nil
|
||||||
;; :renameProvider nil
|
:renameProvider {:workDoneProgress false}
|
||||||
;; :foldingRangeProvider nil
|
;; :foldingRangeProvider nil
|
||||||
;; :executeCommandProvider nil
|
;; :executeCommandProvider nil
|
||||||
;; :selectionRangeProvider 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)]
|
byte (utils.position->byte file.text position self.position-encoding)]
|
||||||
(case-try (language.find-symbol file.ast byte)
|
(case-try (language.find-symbol file.ast byte)
|
||||||
symbol
|
symbol
|
||||||
(if (. file.definitions symbol)
|
(language.find-nearest-definition self file symbol byte)
|
||||||
(values (. file.definitions symbol) file)
|
(where (definition def-file) (not= definition.referenced-by nil))
|
||||||
(language.search-main self file symbol {:stop-early? true} byte))
|
(let [result (icollect [_ symbol (ipairs definition.referenced-by)]
|
||||||
({: referenced-by &as definition} result-file)
|
(message.range-and-uri self def-file symbol))]
|
||||||
(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))]
|
|
||||||
(if ?include-declaration?
|
(if ?include-declaration?
|
||||||
(table.insert result
|
(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
|
;; TODO don't include duplicates
|
||||||
result)
|
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)
|
def (formatter.completion-item-format name def)
|
||||||
_ {:label name}))
|
_ {:label name}))
|
||||||
|
|
||||||
|
|
||||||
(λ scope-completion [self file byte ?symbol parents]
|
(λ scope-completion [self file byte ?symbol parents]
|
||||||
(let [scope (or (accumulate [result nil
|
(let [scope (or (accumulate [result nil
|
||||||
_ parent (ipairs parents)
|
_ 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)
|
(where (or nil [_ nil])) (scope-completion self file byte ?symbol parents)
|
||||||
[_a _b &as split] (field-completion self file ?symbol split))))
|
[_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}}]
|
(λ notifications.textDocument/didChange [self send {: contentChanges :textDocument {: uri}}]
|
||||||
(local file (state.get-by-uri self uri))
|
(local file (state.get-by-uri self uri))
|
||||||
|
|||||||
@ -218,7 +218,6 @@ Returns:
|
|||||||
(recurse key byte)
|
(recurse key byte)
|
||||||
(contains? value byte)
|
(contains? value byte)
|
||||||
(recurse value byte)))))))
|
(recurse value byte)))))))
|
||||||
|
|
||||||
(values
|
(values
|
||||||
(accumulate [result nil i top-level-form (ipairs ast) &until result]
|
(accumulate [result nil i top-level-form (ipairs ast) &until result]
|
||||||
(if (contains? top-level-form byte)
|
(if (contains? top-level-form byte)
|
||||||
@ -226,8 +225,13 @@ Returns:
|
|||||||
(fcollect [i 1 (length parents)]
|
(fcollect [i 1 (length parents)]
|
||||||
(. parents (- (length parents) i -1)))))
|
(. 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-symbol
|
||||||
|
: find-nearest-definition
|
||||||
: search-main
|
: search-main
|
||||||
: search-assignment
|
: search-assignment
|
||||||
: search-name-and-scope
|
: 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)
|
{:start (utils.byte->position file.text bytestart self.position-encoding)
|
||||||
:end (utils.byte->position file.text (+ byteend 1) 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]
|
(λ range-and-uri [self {: uri &as file} ?ast]
|
||||||
"if possible, returns the location of a symbol"
|
"if possible, returns the location of a symbol"
|
||||||
(case (ast->range self file ?ast)
|
(case (ast->range self file ?ast)
|
||||||
@ -75,6 +89,7 @@ to look to fix this in the future."
|
|||||||
: create-response
|
: create-response
|
||||||
: create-error
|
: create-error
|
||||||
: ast->range
|
: ast->range
|
||||||
|
: multisym->range
|
||||||
: range-and-uri
|
: range-and-uri
|
||||||
: diagnostics
|
: diagnostics
|
||||||
: severity}
|
: severity}
|
||||||
|
|||||||
@ -119,10 +119,22 @@ These functions are all pure functions, which makes me happy."
|
|||||||
;; Handle a change
|
;; Handle a change
|
||||||
{:range {: start : end} : text}
|
{:range {: start : end} : text}
|
||||||
(replace contents start end text encoding)
|
(replace contents start end text encoding)
|
||||||
;; A replacment of the entire body
|
;; A replacement of the entire body
|
||||||
{: text}
|
{: text}
|
||||||
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]
|
(λ get-ast-info [?ast info]
|
||||||
;; find a given key of info from an AST object
|
;; find a given key of info from an AST object
|
||||||
(or (?. (getmetatable ?ast) info)
|
(or (?. (getmetatable ?ast) info)
|
||||||
@ -151,6 +163,7 @@ These functions are all pure functions, which makes me happy."
|
|||||||
: byte->position
|
: byte->position
|
||||||
: position->byte
|
: position->byte
|
||||||
: apply-changes
|
: apply-changes
|
||||||
|
: apply-edits
|
||||||
: multi-sym-split
|
: multi-sym-split
|
||||||
: get-ast-info
|
: get-ast-info
|
||||||
: type=}
|
: type=}
|
||||||
|
|||||||
@ -75,12 +75,20 @@
|
|||||||
:textDocument {:uri file}
|
:textDocument {:uri file}
|
||||||
:context {:includeDeclaration (not (not ?includeDeclaration))}})))
|
: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
|
(set mt.__index
|
||||||
{: open-file!
|
{: open-file!
|
||||||
: completion
|
: completion
|
||||||
: definition
|
: definition
|
||||||
: hover
|
: hover
|
||||||
: references})
|
: references
|
||||||
|
: rename})
|
||||||
|
|
||||||
{: create-client
|
{: create-client
|
||||||
: ROOT-URI
|
: ROOT-URI
|
||||||
|
|||||||
@ -57,8 +57,8 @@
|
|||||||
(is-matching responses
|
(is-matching responses
|
||||||
[{:params
|
[{:params
|
||||||
{:diagnostics
|
{:diagnostics
|
||||||
[{:range {:start {:character a :line b}
|
[{:range {:start {:character 0 :line 0}
|
||||||
:end {:character c :line d}}}]}}]
|
:end {:character 7 :line 0}}}]}}]
|
||||||
"diagnostics should always have a range")))
|
"diagnostics should always have a range")))
|
||||||
|
|
||||||
(it "gives more than one error"
|
(it "gives more than one error"
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
(require :test.json-rpc-test)
|
(require :test.json-rpc-test)
|
||||||
(require :test.misc-test)
|
(require :test.misc-test)
|
||||||
(require :test.references-test)
|
(require :test.references-test)
|
||||||
|
(require :test.rename-test)
|
||||||
(require :test.settings-test)
|
(require :test.settings-test)
|
||||||
(require :test.string-processing-test)
|
(require :test.string-processing-test)
|
||||||
|
|
||||||
|
|||||||
@ -40,9 +40,9 @@
|
|||||||
(check-references "(fn x []) x" 0 10
|
(check-references "(fn x []) x" 0 10
|
||||||
[{:uri filename :range (range 0 10 0 11)}]))
|
[{:uri filename :range (range 0 10 0 11)}]))
|
||||||
|
|
||||||
; (it "finds a reference from fn"
|
(it "finds a reference from fn"
|
||||||
; (check-references "(fn x []) x" 0 4
|
(check-references "(fn x []) x" 0 4
|
||||||
; [{:uri filename :range (range 0 10 0 11)}]))
|
[{:uri filename :range (range 0 10 0 11)}]))
|
||||||
|
|
||||||
(it "doesn't crash here"
|
(it "doesn't crash here"
|
||||||
(check-references "(let [x nil] x.y)" 0 14
|
(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