Skip to content

Commit 57ea180

Browse files
authored
lsp-rename: improve UI (#2317)
* `lsp-rename': check if supported `lsp-rename' doesn't check whether renaming is supported first. Do that, throwing an error otherwise. Fixes #2314. * `lsp-rename': support ommitted :placeholder In `lsp--get-symbol-to-rename', handle the server not providing a placeholder by limiting the `when-let' block to the prepare result. * Address PR review - Use an `unless' form instead of `if-let' to check for "textDocument/rename". The latter was wrong anyway, and should've been an `if'. - Remove useless comments. - Revert the variable name change "response" -> "prepare" - Use `lsp-feature?' instead of reinventing it inline * `lsp-rename': use built-in error functions Drop `lsp--fatal-error' and `lsp--user-error', since they cannot display propertized messages anyway. Use `user-error' and `error' in `lsp-rename' and `lsp--get-symbol-to-rename' instead. * Fix renaming without prepareRename support In `lsp--get-symbol-to-rename', return nil instead of (nil . nil) if there is no symbol at point. * `lsp-rename': move `user-error' into `interactive' "rename is invalid here" makes no sense if `lsp-rename' is not called interactively. * Revert unrelated changes * `lsp-rename': highlight renaming `lsp-rename' now highlights the identifier being renamed. To do so, refactor `lsp--get-symbol-to-rename' to return the bounds of the symbol being renamed instead of it, and allow its placeholder to be nil. The latter is necessary to reduce code duplication, as the two code paths of that function (has prepareRename, doesn't have prepareRename) would need to do the same `buffer-substring' with the same bounds. Add a new function, `lsp--read-rename', which takes the result of a call to `lsp--get-symbol-to-rename' and either `user-error's (if the result was nil) or asks the user for a new symbol, highlighting the identifier being renamed. The split was done to make them independently testable. `lsp-rename': no longer `user-error' if NEW-NAME is nil, since that only makes sense if it is called interactively. However, in the latter case, `lsp--read-rename' would do the necessary error handling already. * `lsp-rename': add tests `lsp--read-rename' and `lsp--get-symbol-to-rename' now should really work, since they have a comprehensive test-suite to cover them. It has to rely on mocking a lot, though. Fix a bug in `lsp--get-symbol-to-rename': if RESPONSE is a range, getting its placeholder is wrong. Guard that with an `and' `not' `lsp-range?'. * `lsp--read-rename': use a variable to hold overlay Using `lsp--remove-overlay' didn't actually bring any additional safety, given that the user could still theoretically C-g between `overlay-put' `lsp-face-rename' and `make-overlay', causing it to stay. Use a local variable to hold it instead, which doesn't decrease safety but reduces overhead. Note that it is still very unlikely (probably impossible) that a user could do that using only C-g, without a debugger. There is also no 100% safe way to do this. * `lsp-rename-face': add missing :group * `lsp-rename': tests: check overlays The test for `lsp--read-rename' now checks whether the highlighting overlay appears and disappears again, in-between calls to a mocked `read-string', not just whether it disappears again. This is now done by checking the `face' attribute of overlays, not by checking the `lsp--read-rename' property, since that was removed with the last commit. * `lsp-rename': make prepareRename configurable The new defcustom `lsp-rename-use-prepare' now controls whether prepareRename should be used or not for `lsp--get-symbol-to-rename'. * Address nit: `-doto' The overlay is modified once, and assigned to a variable anyway. `setq' it to `make-overlay' directly and `overlay-put' the face to it afterwards. * Use a consistent placeholder face `lsp-rename-placeholder-face' is now used to display the new name instead of reusing the face of the identifier in the buffer. The reasoning here is that reusing the symbols's face directly would yield inconsistent placeholders, while conveying no additional information, given that the thing being renamed is shown in that very same prompt as it appears in the buffer. * `lsp-face-rename': underline instead of highlight Because `lsp-face-highlight-*' also derive from `highlight', the symbol being renamed could only have either a documentHighlight or a rename face. This caused inconsistent results with `lsp-idle-delay' = 0, where either would sometimes win. By using :underline instead, the symbol being renamed can be marked as such while having a documentHighlight overlay active. * Make `lsp-rename-history' public `lsp-rename-history' should be private so that it can be excluded from savehist. In addition, from looking at the code of other packages, history variables tend to be public: `magit-revision-history', `evil-ex-history', ....
1 parent 325096b commit 57ea180

File tree

2 files changed

+167
-20
lines changed

2 files changed

+167
-20
lines changed

lsp-mode.el

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5653,30 +5653,85 @@ perform the request synchronously."
56535653
(seq-map #'lsp--symbol-information-to-xref
56545654
(lsp-request "workspace/symbol" `(:query ,pattern))))
56555655

5656+
(defcustom lsp-rename-use-prepare t
5657+
"Whether `lsp-rename' should do a prepareRename first.
5658+
For some language servers, textDocument/prepareRename might be
5659+
too slow, in which case this variable may be set to nil.
5660+
`lsp-rename' will then use `thing-at-point' `symbol' to determine
5661+
the symbol to rename at point.")
5662+
56565663
(defun lsp--get-symbol-to-rename ()
5657-
"Get symbol to rename and placeholder at point."
5658-
(if (let ((rename-provider (or (lsp--capability :renameProvider)
5659-
(-some-> (lsp--registered-capability "textDocument/rename")
5660-
(lsp--registered-capability-options)))))
5661-
(lsp:rename-options-prepare-provider? rename-provider))
5662-
(-when-let (response (lsp-request "textDocument/prepareRename"
5663-
(lsp--text-document-position-params)))
5664-
(-let* (((start . end) (lsp--range-to-region
5665-
(if (lsp-range? response)
5666-
response
5667-
(lsp:prepare-rename-result-range response))))
5668-
(symbol (buffer-substring-no-properties start end))
5669-
(placeholder (lsp:prepare-rename-result-placeholder response)))
5670-
(cons symbol (or placeholder symbol))))
5671-
(let ((symbol (thing-at-point 'symbol t)))
5672-
(cons symbol symbol))))
5664+
"Get a symbol to rename and placeholder at point.
5665+
Returns a cons ((START . END) . PLACEHOLDER?), and nil if
5666+
renaming is generally supported but cannot be done at point.
5667+
START and END are the bounds of the identifiers being renamed,
5668+
while PLACEHOLDER?, is either nil or a string suggested by the
5669+
language server as the initial input of a new-name prompt."
5670+
(unless (lsp-feature? "textDocument/rename")
5671+
(error "The connected server(s) doesn't support renaming"))
5672+
(if (and lsp-rename-use-prepare (lsp-feature? "textDocument/prepareRename"))
5673+
(when-let ((response
5674+
(lsp-request "textDocument/prepareRename"
5675+
(lsp--text-document-position-params))))
5676+
(let* ((bounds (lsp--range-to-region
5677+
(if (lsp-range? response)
5678+
response
5679+
(lsp:prepare-rename-result-range response))))
5680+
(placeholder
5681+
(and (not (lsp-range? response))
5682+
(lsp:prepare-rename-result-placeholder response))))
5683+
(cons bounds placeholder)))
5684+
(when-let ((bounds (bounds-of-thing-at-point 'symbol)))
5685+
(cons bounds nil))))
5686+
5687+
(defface lsp-face-rename '((t :underline t))
5688+
"Face used to highlight the identifier being renamed.
5689+
Renaming can be done using `lsp-rename'."
5690+
:group 'lsp-faces)
5691+
5692+
(defface lsp-rename-placeholder-face '((t :inherit font-lock-variable-name-face))
5693+
"Face used to display the rename placeholder in.
5694+
When calling `lsp-rename' interactively, this will be the face of
5695+
the new name."
5696+
:group 'lsp-faces)
5697+
5698+
(defvar lsp-rename-history '()
5699+
"History for `lsp--read-rename'.")
5700+
5701+
(defun lsp--read-rename (at-point)
5702+
"Read a new name for a `lsp-rename' at `point' from the user.
5703+
AT-POINT shall be a structure as returned by
5704+
`lsp--get-symbol-to-rename'.
5705+
5706+
Returns a string, which should be the new name for the identifier
5707+
at point. If renaming cannot be done at point (as determined from
5708+
AT-POINT), throw a `user-error'.
5709+
5710+
This function is for use in `lsp-rename' only, and shall not be
5711+
relied upon."
5712+
(unless at-point
5713+
(user-error "`lsp-rename' is invalid here"))
5714+
(-let* ((((start . end) . placeholder?) at-point)
5715+
;; Do the `buffer-substring' first to not include `lsp-face-rename'
5716+
(rename-me (buffer-substring start end))
5717+
(placeholder (or placeholder? rename-me))
5718+
(placeholder (propertize placeholder 'face 'lsp-rename-placeholder-face))
5719+
5720+
overlay)
5721+
;; We need unwind protect, as the user might cancel here, causing the
5722+
;; overlay to linger.
5723+
(unwind-protect
5724+
(progn
5725+
(setq overlay (make-overlay start end))
5726+
(overlay-put overlay 'face 'lsp-face-rename)
5727+
5728+
(read-string (format "Rename %s to: " rename-me) placeholder
5729+
'lsp-rename-history))
5730+
(and overlay (delete-overlay overlay)))))
56735731

56745732
(defun lsp-rename (newname)
56755733
"Rename the symbol (and all references to it) under point to NEWNAME."
5676-
(interactive (list (-when-let ((symbol . placeholder) (lsp--get-symbol-to-rename))
5677-
(read-string (format "Rename %s to: " symbol) placeholder nil symbol))))
5678-
(unless newname
5679-
(user-error "A rename is not valid at this position"))
5734+
(interactive (list (lsp--read-rename (lsp--get-symbol-to-rename))))
56805735
(when-let ((edits (lsp-request "textDocument/rename"
56815736
`( :textDocument ,(lsp--text-document-identifier)
56825737
:position ,(lsp--cur-position)

test/lsp-methods-test.el

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,3 +302,95 @@ private void extracted() {
302302
:kind "rename")]))
303303
(should-not (f-exists? old-file-name))
304304
(should (f-exists? new-file-name))))
305+
306+
;;; `lsp-rename'
307+
(defmacro lsp-test--simulated-input (keys &rest body)
308+
"Execute body, while simulating the pressing KEYS.
309+
KEYS is passed to `execute-kbd-macro', after being run trough
310+
`kbd'. Returns the result of the last BODY form."
311+
(declare (indent 1))
312+
`(let (result)
313+
(execute-kbd-macro (kbd ,keys) 1 (lambda () (setq result (progn ,@body))))
314+
result))
315+
316+
(defun lsp-test--rename-overlays? (pos)
317+
"Return non-nil if there are `lsp-rename' overlays at POS.
318+
POS is a point in the current buffer."
319+
(--any? (equal (overlay-get it 'face) 'lsp-face-rename)
320+
(overlays-at pos)))
321+
322+
(ert-deftest lsp--read-rename ()
323+
"Ensure that `lsp--read-rename' works.
324+
If AT-POINT is nil, it throws a `user-error'.
325+
If a placeholder is given, it shall be the default value,
326+
327+
otherwise the bounds are to be used.
328+
329+
Rename overlays are removed afterwards, even if the user presses
330+
C-g."
331+
(should-error (lsp--read-rename nil) :type 'user-error)
332+
(with-temp-buffer
333+
(insert "identifier")
334+
(should (string= "identifier"
335+
(lsp-test--simulated-input "RET"
336+
(lsp--read-rename '((1 . 11) . nil)))))
337+
(should (string= "ident"
338+
(lsp-test--simulated-input "RET"
339+
(lsp--read-rename '((1 . 10) . "ident")))))
340+
(goto-char 1)
341+
(condition-case nil
342+
(cl-letf (((symbol-function #'read-string)
343+
(lambda (&rest _)
344+
;; NOTE: BEGIN and END means a range [BEGIN;END[, so at
345+
;; point 10, there shouldn't be an overlay anymore. This is
346+
;; consistent with of `bounds-thing-at-point', and it
347+
;; worked during manual testing.
348+
(should (lsp-test--rename-overlays? 1))
349+
(should (lsp-test--rename-overlays? 9))
350+
(should-not (lsp-test--rename-overlays? 10)))))
351+
(lsp--read-rename '((1 . 10) . "id")))
352+
(quit))
353+
;; but not after `lsp--read-rename'
354+
(should-not (lsp-test--rename-overlays? 1))
355+
(should-not (lsp-test--rename-overlays? 9))
356+
(should-not (lsp-test--rename-overlays? 10))))
357+
358+
(ert-deftest lsp--get-symbol-to-rename ()
359+
"Test `lsp--get-symbol-to-rename'.
360+
It should error if renaming cannot be done, make use of
361+
prepareRename as much as possible, with or without bounds, and it
362+
should work without the latter."
363+
;; We don't support rename
364+
(cl-letf (((symbol-function #'lsp-feature?) #'ignore))
365+
(should-error (lsp--get-symbol-to-rename) :type 'error))
366+
(cl-letf (((symbol-function #'lsp--text-document-position-params) #'ignore)
367+
((symbol-function #'lsp--range-to-region) #'identity))
368+
(with-temp-buffer
369+
(insert "identifier")
370+
(goto-char 1)
371+
;; We do support rename, but no prepareRename
372+
(cl-letf (((symbol-function #'lsp-feature?)
373+
(lambda (f) (member f '("textDocument/rename")))))
374+
(should (equal (cons (bounds-of-thing-at-point 'symbol) nil)
375+
(lsp--get-symbol-to-rename)))
376+
(goto-char (point-max))
377+
(insert " ")
378+
;; we are not on an identifier
379+
(should (equal nil (lsp--get-symbol-to-rename))))
380+
;; Do the following tests with an identifier at point
381+
(goto-char 1)
382+
(cl-letf (((symbol-function #'lsp-feature?)
383+
(lambda (f) (member f '("textDocument/rename"
384+
"textDocument/prepareRename")))))
385+
(cl-letf (((symbol-function #'lsp-request)
386+
(lambda (&rest _) (lsp-make-prepare-rename-result
387+
:range '(1 . 12)
388+
:placeholder nil))))
389+
(should (equal '((1 . 12) . nil) (lsp--get-symbol-to-rename))))
390+
(cl-letf (((symbol-function #'lsp-request)
391+
(lambda (&rest _) (lsp-make-prepare-rename-result
392+
:range '(1 . 12)
393+
:placeholder "_"))))
394+
(should (equal '((1 . 12) . "_") (lsp--get-symbol-to-rename))))))))
395+
396+
;;; lsp-methods-test.el ends here

0 commit comments

Comments
 (0)