Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- `cljr-rename-file-or-dir` now runs asynchronously. Renaming a file rewrites references to it across the whole project, which can take a while; instead of freezing Emacs on a synchronous round-trip, the command returns immediately and finishes (renaming the file, revisiting affected buffers) once the middleware responds.

- Preview project-wide refactorings before they touch disk. `cljr-rename-symbol`, `cljr-change-function-signature` and `cljr-inline-symbol` now gather all their edits first and show them as a diff, and nothing is written until you confirm. Controlled by the new `cljr-preview-refactorings` (default on); set it to nil for the old apply-immediately behavior.
- New `cljr-undo-last-refactoring` reverts every file changed by the last applied rename or change-signature in one step (bound to `ur`, and in `clj-refactor-menu`).

Expand Down
82 changes: 64 additions & 18 deletions clj-refactor.el
Original file line number Diff line number Diff line change
Expand Up @@ -1193,24 +1193,30 @@ See: https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-rename-file-or-d
(nrepl-new-path (funcall cider-to-nrepl-filename-function new-path))
(nrepl-old-path (funcall cider-to-nrepl-filename-function old-path)))
(when (y-or-n-p (format "Really rename %s to %s?" old-path new-path))
(let* ((changed-files (cljr--call-middleware-sync
(cljr--create-msg "rename-file-or-dir"
"old-path" nrepl-old-path
"new-path" nrepl-new-path
"print-right-margin" cljr-print-right-margin
"print-miser-width" cljr-print-miser-width)
"touched"))
(changed-files-count (length changed-files)))
(cond
((null changed-files) (cljr--post-command-message "Rename complete! No files affected."))
((= changed-files-count 1) (cljr--post-command-message "Renamed %s to %s." old-path new-path))
(t (cljr--post-command-message "Rename complete! %s files affected." changed-files-count)))
(when (and (> changed-files-count 0) (not cljr-warn-on-eval))
(cljr--warm-ast-cache)))
(if affected-buffers
(cljr--revisit-buffers affected-buffers new-path active-buffer)
(kill-buffer active-buffer)
(find-file new-path))))))
;; Renaming a file rewrites references to it across the whole project,
;; which can take a while, so run it asynchronously and finish up in the
;; callback rather than freezing Emacs on a synchronous round-trip.
(message "Renaming %s..." old-path)
(cljr--call-middleware-async-collect
(cljr--create-msg "rename-file-or-dir"
"old-path" nrepl-old-path
"new-path" nrepl-new-path
"print-right-margin" cljr-print-right-margin
"print-miser-width" cljr-print-miser-width)
"touched"
(lambda (changed-files)
(let ((changed-files-count (length changed-files)))
(cond
((null changed-files) (message "Rename complete! No files affected."))
((= changed-files-count 1) (message "Renamed %s to %s." old-path new-path))
(t (message "Rename complete! %s files affected." changed-files-count)))
(when (and (> changed-files-count 0) (not cljr-warn-on-eval))
(cljr--warm-ast-cache)))
(if affected-buffers
(cljr--revisit-buffers affected-buffers new-path active-buffer)
(when (buffer-live-p active-buffer)
(kill-buffer active-buffer))
(find-file new-path))))))))

;;;###autoload
(defun cljr-rename-file (new-path)
Expand Down Expand Up @@ -2607,6 +2613,46 @@ If it's present KEY indicates the key to extract from the response."
(defun cljr--call-middleware-async (request &optional callback)
(cider-nrepl-send-request request callback))

(defconst cljr--async-request-timeout 120
"Seconds to wait for an async middleware request before giving up.
A safety net so a hung request or dropped connection doesn't leave a
command waiting forever; generous enough not to cut off a legitimately
slow project-wide analysis.")

(defun cljr--call-middleware-async-collect (request key callback)
"Send REQUEST to the middleware asynchronously and keep Emacs responsive.

The response messages are accumulated; once the request is done the merged
response is checked for a middleware error and then CALLBACK is called with
the value of KEY (or the whole response when KEY is nil). A middleware
error is reported to the user rather than signaled out of the nREPL filter,
and the request is abandoned with a message if it doesn't finish within
`cljr--async-request-timeout'. This is the non-blocking counterpart to
`cljr--call-middleware-sync' for commands that just act on a single result."
(let ((accumulator (nrepl-dict))
(settled nil)
timer)
(setq timer (run-at-time cljr--async-request-timeout nil
(lambda ()
(unless settled
(setq settled t)
(message "clj-refactor: middleware request timed out")))))
(cljr--call-middleware-async
request
(lambda (response)
(setq accumulator (nrepl-dict-merge accumulator response))
(when (and (not settled)
(member "done" (nrepl-dict-get response "status")))
(setq settled t)
(when (timerp timer) (cancel-timer timer))
(condition-case err
(progn
(cljr--maybe-rethrow-error accumulator)
(funcall callback (if key
(nrepl-dict-get accumulator key)
accumulator)))
(error (message "clj-refactor: %s" (error-message-string err)))))))))

(defcustom cljr-artifact-cache-ttl 300
"Number of seconds to cache middleware lookups whose results change rarely.

Expand Down
29 changes: 29 additions & 0 deletions tests/unit-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -940,3 +940,32 @@ str/"))))
(expect (buffer-string) :to-equal "(def foo 1)\n(foo)\n;; unsaved work\n")
(expect (buffer-modified-p) :to-be-truthy))
(expect (cljr--test-file-contents tmpfile) :to-equal "(def foo 1)\n(foo)\n")))))

(describe "cljr--call-middleware-async-collect"
(it "accumulates split response messages and delivers the value on done"
(spy-on 'cljr--call-middleware-async :and-call-fake
(lambda (_request cb)
(funcall cb (nrepl-dict "touched" '("a.clj" "b.clj")))
(funcall cb (nrepl-dict "status" '("done")))))
(let (result (calls 0))
(cljr--call-middleware-async-collect
(cljr--create-msg "rename-file-or-dir") "touched"
(lambda (v) (setq result v calls (1+ calls))))
(expect calls :to-equal 1)
(expect result :to-equal '("a.clj" "b.clj"))))
(it "fires once when value and done arrive together"
(spy-on 'cljr--call-middleware-async :and-call-fake
(lambda (_request cb)
(funcall cb (nrepl-dict "touched" '("a.clj") "status" '("done")))))
(let ((calls 0))
(cljr--call-middleware-async-collect
(cljr--create-msg "x") "touched" (lambda (_v) (setq calls (1+ calls))))
(expect calls :to-equal 1)))
(it "doesn't fire before the request is done"
(spy-on 'cljr--call-middleware-async :and-call-fake
(lambda (_request cb)
(funcall cb (nrepl-dict "touched" '("a.clj")))))
(let ((calls 0))
(cljr--call-middleware-async-collect
(cljr--create-msg "x") "touched" (lambda (_v) (setq calls (1+ calls))))
(expect calls :to-equal 0))))
Loading