diff --git a/lsp-mode.el b/lsp-mode.el index 7d29bd33ac3..36e40a77f57 100644 --- a/lsp-mode.el +++ b/lsp-mode.el @@ -5653,30 +5653,85 @@ perform the request synchronously." (seq-map #'lsp--symbol-information-to-xref (lsp-request "workspace/symbol" `(:query ,pattern)))) +(defcustom lsp-rename-use-prepare t + "Whether `lsp-rename' should do a prepareRename first. +For some language servers, textDocument/prepareRename might be +too slow, in which case this variable may be set to nil. +`lsp-rename' will then use `thing-at-point' `symbol' to determine +the symbol to rename at point.") + (defun lsp--get-symbol-to-rename () - "Get symbol to rename and placeholder at point." - (if (let ((rename-provider (or (lsp--capability :renameProvider) - (-some-> (lsp--registered-capability "textDocument/rename") - (lsp--registered-capability-options))))) - (lsp:rename-options-prepare-provider? rename-provider)) - (-when-let (response (lsp-request "textDocument/prepareRename" - (lsp--text-document-position-params))) - (-let* (((start . end) (lsp--range-to-region - (if (lsp-range? response) - response - (lsp:prepare-rename-result-range response)))) - (symbol (buffer-substring-no-properties start end)) - (placeholder (lsp:prepare-rename-result-placeholder response))) - (cons symbol (or placeholder symbol)))) - (let ((symbol (thing-at-point 'symbol t))) - (cons symbol symbol)))) + "Get a symbol to rename and placeholder at point. +Returns a cons ((START . END) . PLACEHOLDER?), and nil if +renaming is generally supported but cannot be done at point. +START and END are the bounds of the identifiers being renamed, +while PLACEHOLDER?, is either nil or a string suggested by the +language server as the initial input of a new-name prompt." + (unless (lsp-feature? "textDocument/rename") + (error "The connected server(s) doesn't support renaming")) + (if (and lsp-rename-use-prepare (lsp-feature? "textDocument/prepareRename")) + (when-let ((response + (lsp-request "textDocument/prepareRename" + (lsp--text-document-position-params)))) + (let* ((bounds (lsp--range-to-region + (if (lsp-range? response) + response + (lsp:prepare-rename-result-range response)))) + (placeholder + (and (not (lsp-range? response)) + (lsp:prepare-rename-result-placeholder response)))) + (cons bounds placeholder))) + (when-let ((bounds (bounds-of-thing-at-point 'symbol))) + (cons bounds nil)))) + +(defface lsp-face-rename '((t :underline t)) + "Face used to highlight the identifier being renamed. +Renaming can be done using `lsp-rename'." + :group 'lsp-faces) + +(defface lsp-rename-placeholder-face '((t :inherit font-lock-variable-name-face)) + "Face used to display the rename placeholder in. +When calling `lsp-rename' interactively, this will be the face of +the new name." + :group 'lsp-faces) + +(defvar lsp-rename-history '() + "History for `lsp--read-rename'.") + +(defun lsp--read-rename (at-point) + "Read a new name for a `lsp-rename' at `point' from the user. +AT-POINT shall be a structure as returned by +`lsp--get-symbol-to-rename'. + +Returns a string, which should be the new name for the identifier +at point. If renaming cannot be done at point (as determined from +AT-POINT), throw a `user-error'. + +This function is for use in `lsp-rename' only, and shall not be +relied upon." + (unless at-point + (user-error "`lsp-rename' is invalid here")) + (-let* ((((start . end) . placeholder?) at-point) + ;; Do the `buffer-substring' first to not include `lsp-face-rename' + (rename-me (buffer-substring start end)) + (placeholder (or placeholder? rename-me)) + (placeholder (propertize placeholder 'face 'lsp-rename-placeholder-face)) + + overlay) + ;; We need unwind protect, as the user might cancel here, causing the + ;; overlay to linger. + (unwind-protect + (progn + (setq overlay (make-overlay start end)) + (overlay-put overlay 'face 'lsp-face-rename) + + (read-string (format "Rename %s to: " rename-me) placeholder + 'lsp-rename-history)) + (and overlay (delete-overlay overlay))))) (defun lsp-rename (newname) "Rename the symbol (and all references to it) under point to NEWNAME." - (interactive (list (-when-let ((symbol . placeholder) (lsp--get-symbol-to-rename)) - (read-string (format "Rename %s to: " symbol) placeholder nil symbol)))) - (unless newname - (user-error "A rename is not valid at this position")) + (interactive (list (lsp--read-rename (lsp--get-symbol-to-rename)))) (when-let ((edits (lsp-request "textDocument/rename" `( :textDocument ,(lsp--text-document-identifier) :position ,(lsp--cur-position) diff --git a/test/lsp-methods-test.el b/test/lsp-methods-test.el index a17b3d9b508..9adef94ab80 100644 --- a/test/lsp-methods-test.el +++ b/test/lsp-methods-test.el @@ -302,3 +302,95 @@ private void extracted() { :kind "rename")])) (should-not (f-exists? old-file-name)) (should (f-exists? new-file-name)))) + +;;; `lsp-rename' +(defmacro lsp-test--simulated-input (keys &rest body) + "Execute body, while simulating the pressing KEYS. +KEYS is passed to `execute-kbd-macro', after being run trough +`kbd'. Returns the result of the last BODY form." + (declare (indent 1)) + `(let (result) + (execute-kbd-macro (kbd ,keys) 1 (lambda () (setq result (progn ,@body)))) + result)) + +(defun lsp-test--rename-overlays? (pos) + "Return non-nil if there are `lsp-rename' overlays at POS. +POS is a point in the current buffer." + (--any? (equal (overlay-get it 'face) 'lsp-face-rename) + (overlays-at pos))) + +(ert-deftest lsp--read-rename () + "Ensure that `lsp--read-rename' works. +If AT-POINT is nil, it throws a `user-error'. +If a placeholder is given, it shall be the default value, + +otherwise the bounds are to be used. + +Rename overlays are removed afterwards, even if the user presses +C-g." + (should-error (lsp--read-rename nil) :type 'user-error) + (with-temp-buffer + (insert "identifier") + (should (string= "identifier" + (lsp-test--simulated-input "RET" + (lsp--read-rename '((1 . 11) . nil))))) + (should (string= "ident" + (lsp-test--simulated-input "RET" + (lsp--read-rename '((1 . 10) . "ident"))))) + (goto-char 1) + (condition-case nil + (cl-letf (((symbol-function #'read-string) + (lambda (&rest _) + ;; NOTE: BEGIN and END means a range [BEGIN;END[, so at + ;; point 10, there shouldn't be an overlay anymore. This is + ;; consistent with of `bounds-thing-at-point', and it + ;; worked during manual testing. + (should (lsp-test--rename-overlays? 1)) + (should (lsp-test--rename-overlays? 9)) + (should-not (lsp-test--rename-overlays? 10))))) + (lsp--read-rename '((1 . 10) . "id"))) + (quit)) + ;; but not after `lsp--read-rename' + (should-not (lsp-test--rename-overlays? 1)) + (should-not (lsp-test--rename-overlays? 9)) + (should-not (lsp-test--rename-overlays? 10)))) + +(ert-deftest lsp--get-symbol-to-rename () + "Test `lsp--get-symbol-to-rename'. +It should error if renaming cannot be done, make use of +prepareRename as much as possible, with or without bounds, and it +should work without the latter." + ;; We don't support rename + (cl-letf (((symbol-function #'lsp-feature?) #'ignore)) + (should-error (lsp--get-symbol-to-rename) :type 'error)) + (cl-letf (((symbol-function #'lsp--text-document-position-params) #'ignore) + ((symbol-function #'lsp--range-to-region) #'identity)) + (with-temp-buffer + (insert "identifier") + (goto-char 1) + ;; We do support rename, but no prepareRename + (cl-letf (((symbol-function #'lsp-feature?) + (lambda (f) (member f '("textDocument/rename"))))) + (should (equal (cons (bounds-of-thing-at-point 'symbol) nil) + (lsp--get-symbol-to-rename))) + (goto-char (point-max)) + (insert " ") + ;; we are not on an identifier + (should (equal nil (lsp--get-symbol-to-rename)))) + ;; Do the following tests with an identifier at point + (goto-char 1) + (cl-letf (((symbol-function #'lsp-feature?) + (lambda (f) (member f '("textDocument/rename" + "textDocument/prepareRename"))))) + (cl-letf (((symbol-function #'lsp-request) + (lambda (&rest _) (lsp-make-prepare-rename-result + :range '(1 . 12) + :placeholder nil)))) + (should (equal '((1 . 12) . nil) (lsp--get-symbol-to-rename)))) + (cl-letf (((symbol-function #'lsp-request) + (lambda (&rest _) (lsp-make-prepare-rename-result + :range '(1 . 12) + :placeholder "_")))) + (should (equal '((1 . 12) . "_") (lsp--get-symbol-to-rename)))))))) + +;;; lsp-methods-test.el ends here