Add primitive support for renaming

This commit is contained in:
XeroOl 2023-07-12 23:30:59 -05:00
parent 9d534c161f
commit fe257c061a
10 changed files with 117 additions and 25 deletions

10
TODO.md
View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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}

View File

@ -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=}

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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
View 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)")))