Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

939 lines
39 KiB

  1. ;;; with-editor.el --- Use the Emacsclient as $EDITOR -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2014-2020 The Magit Project Contributors
  3. ;;
  4. ;; You should have received a copy of the AUTHORS.md file. If not,
  5. ;; see https://github.com/magit/with-editor/blob/master/AUTHORS.md.
  6. ;; Author: Jonas Bernoulli <jonas@bernoul.li>
  7. ;; Maintainer: Jonas Bernoulli <jonas@bernoul.li>
  8. ;; Package-Requires: ((emacs "24.4") (async "1.9"))
  9. ;; Keywords: tools
  10. ;; Homepage: https://github.com/magit/with-editor
  11. ;; This file is not part of GNU Emacs.
  12. ;; This file is free software; you can redistribute it and/or modify
  13. ;; it under the terms of the GNU General Public License as published by
  14. ;; the Free Software Foundation; either version 3, or (at your option)
  15. ;; any later version.
  16. ;; This file is distributed in the hope that it will be useful,
  17. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. ;; GNU General Public License for more details.
  20. ;; You should have received a copy of the GNU General Public License
  21. ;; along with Magit. If not, see http://www.gnu.org/licenses.
  22. ;;; Commentary:
  23. ;; This library makes it possible to reliably use the Emacsclient as
  24. ;; the `$EDITOR' of child processes. It makes sure that they know how
  25. ;; to call home. For remote processes a substitute is provided, which
  26. ;; communicates with Emacs on standard output/input instead of using a
  27. ;; socket as the Emacsclient does.
  28. ;; It provides the commands `with-editor-async-shell-command' and
  29. ;; `with-editor-shell-command', which are intended as replacements
  30. ;; for `async-shell-command' and `shell-command'. They automatically
  31. ;; export `$EDITOR' making sure the executed command uses the current
  32. ;; Emacs instance as "the editor". With a prefix argument these
  33. ;; commands prompt for an alternative environment variable such as
  34. ;; `$GIT_EDITOR'. To always use these variants add this to your init
  35. ;; file:
  36. ;;
  37. ;; (define-key (current-global-map)
  38. ;; [remap async-shell-command] 'with-editor-async-shell-command)
  39. ;; (define-key (current-global-map)
  40. ;; [remap shell-command] 'with-editor-shell-command)
  41. ;; Alternatively use the global `shell-command-with-editor-mode',
  42. ;; which always sets `$EDITOR' for all Emacs commands which ultimately
  43. ;; use `shell-command' to asynchronously run some shell command.
  44. ;; The command `with-editor-export-editor' exports `$EDITOR' or
  45. ;; another such environment variable in `shell-mode', `eshell-mode',
  46. ;; `term-mode' and `vterm-mode' buffers. Use this Emacs command
  47. ;; before executing a shell command which needs the editor set, or
  48. ;; always arrange for the current Emacs instance to be used as editor
  49. ;; by adding it to the appropriate mode hooks:
  50. ;;
  51. ;; (add-hook 'shell-mode-hook 'with-editor-export-editor)
  52. ;; (add-hook 'eshell-mode-hook 'with-editor-export-editor)
  53. ;; (add-hook 'term-exec-hook 'with-editor-export-editor)
  54. ;; (add-hook 'vterm-mode-hook 'with-editor-export-editor)
  55. ;; Some variants of this function exist, these two forms are
  56. ;; equivalent:
  57. ;;
  58. ;; (add-hook 'shell-mode-hook
  59. ;; (apply-partially 'with-editor-export-editor "GIT_EDITOR"))
  60. ;; (add-hook 'shell-mode-hook 'with-editor-export-git-editor)
  61. ;; This library can also be used by other packages which need to use
  62. ;; the current Emacs instance as editor. In fact this library was
  63. ;; written for Magit and its `git-commit-mode' and `git-rebase-mode'.
  64. ;; Consult `git-rebase.el' and the related code in `magit-sequence.el'
  65. ;; for a simple example.
  66. ;;; Code:
  67. (require 'cl-lib)
  68. (eval-when-compile
  69. (require 'pcase) ; `pcase-dolist' is not autoloaded on Emacs 24.
  70. (require 'subr-x))
  71. (require 'server)
  72. (require 'shell)
  73. (and (require 'async-bytecomp nil t)
  74. (let ((pkgs (bound-and-true-p async-bytecomp-allowed-packages)))
  75. (if (consp pkgs)
  76. (cl-intersection '(all magit) pkgs)
  77. (memq pkgs '(all t))))
  78. (fboundp 'async-bytecomp-package-mode)
  79. (async-bytecomp-package-mode 1))
  80. (eval-when-compile
  81. (progn (require 'dired nil t)
  82. (require 'eshell nil t)
  83. (require 'term nil t)
  84. (condition-case err
  85. (require 'vterm nil t)
  86. (error (message "Error: %S" err)))
  87. (require 'vterm nil t)
  88. (require 'warnings nil t)))
  89. (declare-function dired-get-filename 'dired)
  90. (declare-function term-emulate-terminal 'term)
  91. (declare-function vterm-send-return 'vterm)
  92. (declare-function vterm-send-string 'vterm)
  93. (defvar eshell-preoutput-filter-functions)
  94. (defvar git-commit-post-finish-hook)
  95. (defvar vterm--process)
  96. ;;; Options
  97. (defgroup with-editor nil
  98. "Use the Emacsclient as $EDITOR."
  99. :group 'external
  100. :group 'server)
  101. (defun with-editor-locate-emacsclient ()
  102. "Search for a suitable Emacsclient executable."
  103. (or (with-editor-locate-emacsclient-1
  104. (with-editor-emacsclient-path)
  105. (length (split-string emacs-version "\\.")))
  106. (prog1 nil (display-warning 'with-editor "\
  107. Cannot determine a suitable Emacsclient
  108. Determining an Emacsclient executable suitable for the
  109. current Emacs instance failed. For more information
  110. please see https://github.com/magit/magit/wiki/Emacsclient."))))
  111. (defun with-editor-locate-emacsclient-1 (path depth)
  112. (let* ((version-lst (cl-subseq (split-string emacs-version "\\.") 0 depth))
  113. (version-reg (concat "^" (mapconcat #'identity version-lst "\\."))))
  114. (or (locate-file-internal
  115. (if (equal (downcase invocation-name) "remacs")
  116. "remacsclient"
  117. "emacsclient")
  118. path
  119. (cl-mapcan
  120. (lambda (v) (cl-mapcar (lambda (e) (concat v e)) exec-suffixes))
  121. (nconc (and (boundp 'debian-emacs-flavor)
  122. (list (format ".%s" debian-emacs-flavor)))
  123. (cl-mapcon (lambda (v)
  124. (setq v (mapconcat #'identity (reverse v) "."))
  125. (list v (concat "-" v) (concat ".emacs" v)))
  126. (reverse version-lst))
  127. (list "" "-snapshot" ".emacs-snapshot")))
  128. (lambda (exec)
  129. (ignore-errors
  130. (string-match-p version-reg
  131. (with-editor-emacsclient-version exec)))))
  132. (and (> depth 1)
  133. (with-editor-locate-emacsclient-1 path (1- depth))))))
  134. (defun with-editor-emacsclient-version (exec)
  135. (let ((default-directory (file-name-directory exec)))
  136. (ignore-errors
  137. (cadr (split-string (car (process-lines exec "--version")))))))
  138. (defun with-editor-emacsclient-path ()
  139. (let ((path exec-path))
  140. (when invocation-directory
  141. (push (directory-file-name invocation-directory) path)
  142. (let* ((linkname (expand-file-name invocation-name invocation-directory))
  143. (truename (file-chase-links linkname)))
  144. (unless (equal truename linkname)
  145. (push (directory-file-name (file-name-directory truename)) path)))
  146. (when (eq system-type 'darwin)
  147. (let ((dir (expand-file-name "bin" invocation-directory)))
  148. (when (file-directory-p dir)
  149. (push dir path)))
  150. (when (string-match-p "Cellar" invocation-directory)
  151. (let ((dir (expand-file-name "../../../bin" invocation-directory)))
  152. (when (file-directory-p dir)
  153. (push dir path))))))
  154. (cl-remove-duplicates path :test 'equal)))
  155. (defcustom with-editor-emacsclient-executable (with-editor-locate-emacsclient)
  156. "The Emacsclient executable used by the `with-editor' macro."
  157. :group 'with-editor
  158. :type '(choice (string :tag "Executable")
  159. (const :tag "Don't use Emacsclient" nil)))
  160. (defcustom with-editor-sleeping-editor "\
  161. sh -c '\
  162. printf \"WITH-EDITOR: $$ OPEN $0\\037 IN $(pwd)\\n\"; \
  163. sleep 604800 & sleep=$!; \
  164. trap \"kill $sleep; exit 0\" USR1; \
  165. trap \"kill $sleep; exit 1\" USR2; \
  166. wait $sleep'"
  167. "The sleeping editor, used when the Emacsclient cannot be used.
  168. This fallback is used for asynchronous processes started inside
  169. the macro `with-editor', when the process runs on a remote machine
  170. or for local processes when `with-editor-emacsclient-executable'
  171. is nil (i.e. when no suitable Emacsclient was found, or the user
  172. decided not to use it).
  173. Where the latter uses a socket to communicate with Emacs' server,
  174. this substitute prints edit requests to its standard output on
  175. which a process filter listens for such requests. As such it is
  176. not a complete substitute for a proper Emacsclient, it can only
  177. be used as $EDITOR of child process of the current Emacs instance.
  178. Some shells do not execute traps immediately when waiting for a
  179. child process, but by default we do use such a blocking child
  180. process.
  181. If you use such a shell (e.g. `csh' on FreeBSD, but not Debian),
  182. then you have to edit this option. You can either replace \"sh\"
  183. with \"bash\" (and install that), or you can use the older, less
  184. performant implementation:
  185. \"sh -c '\\
  186. echo \\\"WITH-EDITOR: $$ OPEN $0 IN $(pwd)\\\"; \\
  187. trap \\\"exit 0\\\" USR1; \\
  188. trap \\\"exit 1\" USR2; \\
  189. while true; do sleep 1; done'\"
  190. Note that the unit separator character () right after the file
  191. name ($0) is required.
  192. Also note that using this alternative implementation leads to a
  193. delay of up to a second. The delay can be shortened by replacing
  194. \"sleep 1\" with \"sleep 0.01\", or if your implementation does
  195. not support floats, then by using \"nanosleep\" instead."
  196. :package-version '(with-editor . "2.8.0")
  197. :group 'with-editor
  198. :type 'string)
  199. (defcustom with-editor-finish-query-functions nil
  200. "List of functions called to query before finishing session.
  201. The buffer in question is current while the functions are called.
  202. If any of them returns nil, then the session is not finished and
  203. the buffer is not killed. The user should then fix the issue and
  204. try again. The functions are called with one argument. If it is
  205. non-nil then that indicates that the user used a prefix argument
  206. to force finishing the session despite issues. Functions should
  207. usually honor that and return non-nil."
  208. :group 'with-editor
  209. :type 'hook)
  210. (put 'with-editor-finish-query-functions 'permanent-local t)
  211. (defcustom with-editor-cancel-query-functions nil
  212. "List of functions called to query before canceling session.
  213. The buffer in question is current while the functions are called.
  214. If any of them returns nil, then the session is not canceled and
  215. the buffer is not killed. The user should then fix the issue and
  216. try again. The functions are called with one argument. If it is
  217. non-nil then that indicates that the user used a prefix argument
  218. to force canceling the session despite issues. Functions should
  219. usually honor that and return non-nil."
  220. :group 'with-editor
  221. :type 'hook)
  222. (put 'with-editor-cancel-query-functions 'permanent-local t)
  223. (defcustom with-editor-mode-lighter " WE"
  224. "The mode-line lighter of the With-Editor mode."
  225. :group 'with-editor
  226. :type '(choice (const :tag "No lighter" "") string))
  227. (defvar with-editor-server-window-alist nil
  228. "Alist of filename patterns vs corresponding `server-window'.
  229. Each element looks like (REGEXP . FUNCTION). Files matching
  230. REGEXP are selected using FUNCTION instead of the default in
  231. `server-window'.
  232. Note that when a package adds an entry here then it probably
  233. has a reason to disrespect `server-window' and it likely is
  234. not a good idea to change such entries.")
  235. (defvar with-editor-file-name-history-exclude nil
  236. "List of regexps for filenames `server-visit' should not remember.
  237. When a filename matches any of the regexps, then `server-visit'
  238. does not add it to the variable `file-name-history', which is
  239. used when reading a filename in the minibuffer.")
  240. (defcustom with-editor-shell-command-use-emacsclient t
  241. "Whether to use the emacsclient when running shell commands.
  242. This affects `with-editor-shell-command-async' and, if the input
  243. ends with \"&\" `with-editor-shell-command' .
  244. If `shell-command-with-editor-mode' is enabled, then it also
  245. affects `shell-command-async' and, if the input ends with \"&\"
  246. `shell-command'.
  247. This is a temporary kludge that lets you choose between two
  248. possible defects, the ones described in the issues #23 and #40.
  249. When t, then use the emacsclient. This has the disadvantage that
  250. `with-editor-mode' won't be enabled because we don't know whether
  251. this package was involved at all in the call to the emacsclient,
  252. and when it is not, then we really should. The problem is that
  253. the emacsclient doesn't pass a long any environment variables to
  254. the server. This will hopefully be fixed in Emacs eventually.
  255. When nil, then use the sleeping editor. Because in this case we
  256. know that this package is involved, we can enable the mode. But
  257. this makes it necessary that you invoke $EDITOR in shell scripts
  258. like so:
  259. eval \"$EDITOR\" file
  260. And some tools that do not handle $EDITOR properly also break."
  261. :package-version '(with-editor . "2.7.1")
  262. :group 'with-editor
  263. :type 'boolean)
  264. ;;; Mode Commands
  265. (defvar with-editor-pre-finish-hook nil)
  266. (defvar with-editor-pre-cancel-hook nil)
  267. (defvar with-editor-post-finish-hook nil)
  268. (defvar with-editor-post-finish-hook-1 nil)
  269. (defvar with-editor-post-cancel-hook nil)
  270. (defvar with-editor-post-cancel-hook-1 nil)
  271. (defvar with-editor-cancel-alist nil)
  272. (put 'with-editor-pre-finish-hook 'permanent-local t)
  273. (put 'with-editor-pre-cancel-hook 'permanent-local t)
  274. (put 'with-editor-post-finish-hook 'permanent-local t)
  275. (put 'with-editor-post-cancel-hook 'permanent-local t)
  276. (defvar with-editor-show-usage t)
  277. (defvar with-editor-cancel-message nil)
  278. (defvar with-editor-previous-winconf nil)
  279. (make-variable-buffer-local 'with-editor-show-usage)
  280. (make-variable-buffer-local 'with-editor-cancel-message)
  281. (make-variable-buffer-local 'with-editor-previous-winconf)
  282. (put 'with-editor-cancel-message 'permanent-local t)
  283. (put 'with-editor-previous-winconf 'permanent-local t)
  284. (defvar-local with-editor--pid nil "For internal use.")
  285. (put 'with-editor--pid 'permanent-local t)
  286. (defun with-editor-finish (force)
  287. "Finish the current edit session."
  288. (interactive "P")
  289. (when (run-hook-with-args-until-failure
  290. 'with-editor-finish-query-functions force)
  291. (let ((post-finish-hook with-editor-post-finish-hook)
  292. (post-commit-hook (bound-and-true-p git-commit-post-finish-hook))
  293. (dir default-directory))
  294. (run-hooks 'with-editor-pre-finish-hook)
  295. (with-editor-return nil)
  296. (accept-process-output nil 0.1)
  297. (with-temp-buffer
  298. (setq default-directory dir)
  299. (setq-local with-editor-post-finish-hook post-finish-hook)
  300. (when post-commit-hook
  301. (setq-local git-commit-post-finish-hook post-commit-hook))
  302. (run-hooks 'with-editor-post-finish-hook)))))
  303. (defun with-editor-cancel (force)
  304. "Cancel the current edit session."
  305. (interactive "P")
  306. (when (run-hook-with-args-until-failure
  307. 'with-editor-cancel-query-functions force)
  308. (let ((message with-editor-cancel-message))
  309. (when (functionp message)
  310. (setq message (funcall message)))
  311. (let ((post-cancel-hook with-editor-post-cancel-hook)
  312. (with-editor-cancel-alist nil)
  313. (dir default-directory))
  314. (run-hooks 'with-editor-pre-cancel-hook)
  315. (with-editor-return t)
  316. (accept-process-output nil 0.1)
  317. (with-temp-buffer
  318. (setq default-directory dir)
  319. (setq-local with-editor-post-cancel-hook post-cancel-hook)
  320. (run-hooks 'with-editor-post-cancel-hook)))
  321. (message (or message "Canceled by user")))))
  322. (defun with-editor-return (cancel)
  323. (let ((winconf with-editor-previous-winconf)
  324. (clients server-buffer-clients)
  325. (dir default-directory)
  326. (pid with-editor--pid))
  327. (remove-hook 'kill-buffer-query-functions
  328. 'with-editor-kill-buffer-noop t)
  329. (cond (cancel
  330. (save-buffer)
  331. (if clients
  332. (dolist (client clients)
  333. (ignore-errors
  334. (server-send-string client "-error Canceled by user"))
  335. (delete-process client))
  336. ;; Fallback for when emacs was used as $EDITOR
  337. ;; instead of emacsclient or the sleeping editor.
  338. ;; See https://github.com/magit/magit/issues/2258.
  339. (ignore-errors (delete-file buffer-file-name))
  340. (kill-buffer)))
  341. (t
  342. (save-buffer)
  343. (if clients
  344. ;; Don't use `server-edit' because we do not want to
  345. ;; show another buffer belonging to another client.
  346. ;; See https://github.com/magit/magit/issues/2197.
  347. (server-done)
  348. (kill-buffer))))
  349. (when pid
  350. (let ((default-directory dir))
  351. (process-file "kill" nil nil nil
  352. "-s" (if cancel "USR2" "USR1") pid)))
  353. (when (and winconf (eq (window-configuration-frame winconf)
  354. (selected-frame)))
  355. (set-window-configuration winconf))))
  356. ;;; Mode
  357. (defvar with-editor-mode-map
  358. (let ((map (make-sparse-keymap)))
  359. (define-key map "\C-c\C-c" 'with-editor-finish)
  360. (define-key map [remap server-edit] 'with-editor-finish)
  361. (define-key map [remap evil-save-and-close] 'with-editor-finish)
  362. (define-key map [remap evil-save-modified-and-close] 'with-editor-finish)
  363. (define-key map "\C-c\C-k" 'with-editor-cancel)
  364. (define-key map [remap kill-buffer] 'with-editor-cancel)
  365. (define-key map [remap ido-kill-buffer] 'with-editor-cancel)
  366. (define-key map [remap iswitchb-kill-buffer] 'with-editor-cancel)
  367. (define-key map [remap evil-quit] 'with-editor-cancel)
  368. map))
  369. (define-minor-mode with-editor-mode
  370. "Edit a file as the $EDITOR of an external process."
  371. :lighter with-editor-mode-lighter
  372. ;; Protect the user from killing the buffer without using
  373. ;; either `with-editor-finish' or `with-editor-cancel',
  374. ;; and from removing the key bindings for these commands.
  375. (unless with-editor-mode
  376. (user-error "With-Editor mode cannot be turned off"))
  377. (add-hook 'kill-buffer-query-functions
  378. 'with-editor-kill-buffer-noop nil t)
  379. ;; `server-execute' displays a message which is not
  380. ;; correct when using this mode.
  381. (when with-editor-show-usage
  382. (with-editor-usage-message)))
  383. (put 'with-editor-mode 'permanent-local t)
  384. (defun with-editor-kill-buffer-noop ()
  385. ;; We started doing this in response to #64, but it is not safe
  386. ;; to do so, because the client has already been killed, causing
  387. ;; `with-editor-return' (called by `with-editor-cancel') to delete
  388. ;; the file, see #66. The reason we delete the file in the first
  389. ;; place are https://github.com/magit/magit/issues/2258 and
  390. ;; https://github.com/magit/magit/issues/2248.
  391. ;; (if (memq this-command '(save-buffers-kill-terminal
  392. ;; save-buffers-kill-emacs))
  393. ;; (let ((with-editor-cancel-query-functions nil))
  394. ;; (with-editor-cancel nil)
  395. ;; t)
  396. ;; ...)
  397. ;; So go back to always doing this instead:
  398. (user-error (substitute-command-keys (format "\
  399. Don't kill this buffer %S. Instead cancel using \\[with-editor-cancel]"
  400. (current-buffer)))))
  401. (defvar-local with-editor-usage-message "\
  402. Type \\[with-editor-finish] to finish, \
  403. or \\[with-editor-cancel] to cancel")
  404. (defun with-editor-usage-message ()
  405. ;; Run after `server-execute', which is run using
  406. ;; a timer which starts immediately.
  407. (let ((buffer (current-buffer)))
  408. (run-with-timer
  409. 0.05 nil
  410. (lambda ()
  411. (with-current-buffer buffer
  412. (message (substitute-command-keys with-editor-usage-message)))))))
  413. ;;; Wrappers
  414. (defvar with-editor--envvar nil "For internal use.")
  415. (defmacro with-editor (&rest body)
  416. "Use the Emacsclient as $EDITOR while evaluating BODY.
  417. Modify the `process-environment' for processes started in BODY,
  418. instructing them to use the Emacsclient as $EDITOR. If optional
  419. ENVVAR is a literal string then bind that environment variable
  420. instead.
  421. \n(fn [ENVVAR] BODY...)"
  422. (declare (indent defun) (debug (body)))
  423. `(let ((with-editor--envvar ,(if (stringp (car body))
  424. (pop body)
  425. '(or with-editor--envvar "EDITOR")))
  426. (process-environment process-environment))
  427. (with-editor--setup)
  428. ,@body))
  429. (defmacro with-editor* (envvar &rest body)
  430. "Use the Emacsclient as the editor while evaluating BODY.
  431. Modify the `process-environment' for processes started in BODY,
  432. instructing them to use the Emacsclient as editor. ENVVAR is the
  433. environment variable that is exported to do so, it is evaluated
  434. at run-time.
  435. \n(fn [ENVVAR] BODY...)"
  436. (declare (indent defun) (debug (sexp body)))
  437. `(let ((with-editor--envvar ,envvar)
  438. (process-environment process-environment))
  439. (with-editor--setup)
  440. ,@body))
  441. (defun with-editor--setup ()
  442. (if (or (not with-editor-emacsclient-executable)
  443. (file-remote-p default-directory))
  444. (push (concat with-editor--envvar "=" with-editor-sleeping-editor)
  445. process-environment)
  446. ;; Make sure server-use-tcp's value is valid.
  447. (unless (featurep 'make-network-process '(:family local))
  448. (setq server-use-tcp t))
  449. ;; Make sure the server is running.
  450. (unless (process-live-p server-process)
  451. (when (server-running-p server-name)
  452. (setq server-name (format "server%s" (emacs-pid)))
  453. (when (server-running-p server-name)
  454. (server-force-delete server-name)))
  455. (server-start))
  456. ;; Tell $EDITOR to use the Emacsclient.
  457. (push (concat with-editor--envvar "="
  458. (shell-quote-argument with-editor-emacsclient-executable)
  459. ;; Tell the process where the server file is.
  460. (and (not server-use-tcp)
  461. (concat " --socket-name="
  462. (shell-quote-argument
  463. (expand-file-name server-name
  464. server-socket-dir)))))
  465. process-environment)
  466. (when server-use-tcp
  467. (push (concat "EMACS_SERVER_FILE="
  468. (expand-file-name server-name server-auth-dir))
  469. process-environment))
  470. ;; As last resort fallback to the sleeping editor.
  471. (push (concat "ALTERNATE_EDITOR=" with-editor-sleeping-editor)
  472. process-environment)))
  473. (defun with-editor-server-window ()
  474. (or (and buffer-file-name
  475. (cdr (cl-find-if (lambda (cons)
  476. (string-match-p (car cons) buffer-file-name))
  477. with-editor-server-window-alist)))
  478. server-window))
  479. (defun server-switch-buffer--with-editor-server-window-alist
  480. (fn &optional next-buffer &rest args)
  481. "Honor `with-editor-server-window-alist' (which see)."
  482. (let ((server-window (with-current-buffer
  483. (or next-buffer (current-buffer))
  484. (when with-editor-mode
  485. (setq with-editor-previous-winconf
  486. (current-window-configuration)))
  487. (with-editor-server-window))))
  488. (apply fn next-buffer args)))
  489. (advice-add 'server-switch-buffer :around
  490. 'server-switch-buffer--with-editor-server-window-alist)
  491. (defun start-file-process--with-editor-process-filter
  492. (fn name buffer program &rest program-args)
  493. "When called inside a `with-editor' form and the Emacsclient
  494. cannot be used, then give the process the filter function
  495. `with-editor-process-filter'. To avoid overriding the filter
  496. being added here you should use `with-editor-set-process-filter'
  497. instead of `set-process-filter' inside `with-editor' forms.
  498. When the `default-directory' is located on a remote machine,
  499. then also manipulate PROGRAM and PROGRAM-ARGS in order to set
  500. the appropriate editor environment variable."
  501. (if (not with-editor--envvar)
  502. (apply fn name buffer program program-args)
  503. (when (file-remote-p default-directory)
  504. (unless (equal program "env")
  505. (push program program-args)
  506. (setq program "env"))
  507. (push (concat with-editor--envvar "=" with-editor-sleeping-editor)
  508. program-args))
  509. (let ((process (apply fn name buffer program program-args)))
  510. (set-process-filter process 'with-editor-process-filter)
  511. (process-put process 'default-dir default-directory)
  512. process)))
  513. (advice-add 'start-file-process :around
  514. 'start-file-process--with-editor-process-filter)
  515. (cl-defun make-process--with-editor-process-filter
  516. (fn &rest keys &key name buffer command coding noquery stop
  517. connection-type filter sentinel stderr file-handler
  518. &allow-other-keys)
  519. "When called inside a `with-editor' form and the Emacsclient
  520. cannot be used, then give the process the filter function
  521. `with-editor-process-filter'. To avoid overriding the filter
  522. being added here you should use `with-editor-set-process-filter'
  523. instead of `set-process-filter' inside `with-editor' forms.
  524. When the `default-directory' is located on a remote machine and
  525. FILE-HANDLER is non-nil, then also manipulate COMMAND in order
  526. to set the appropriate editor environment variable."
  527. (if (or (not file-handler) (not with-editor--envvar))
  528. (apply fn keys)
  529. (when (file-remote-p default-directory)
  530. (unless (equal (car command) "env")
  531. (push "env" command))
  532. (push (concat with-editor--envvar "=" with-editor-sleeping-editor)
  533. (cdr command)))
  534. (let* ((filter (if filter
  535. (lambda (process output)
  536. (funcall filter process output)
  537. (with-editor-process-filter process output t))
  538. #'with-editor-process-filter))
  539. (process (funcall fn
  540. :name name
  541. :buffer buffer
  542. :command command
  543. :coding coding
  544. :noquery noquery
  545. :stop stop
  546. :connection-type connection-type
  547. :filter filter
  548. :sentinel sentinel
  549. :stderr stderr
  550. :file-handler file-handler)))
  551. (process-put process 'default-dir default-directory)
  552. process)))
  553. (advice-add #'make-process :around #'make-process--with-editor-process-filter)
  554. (defun with-editor-set-process-filter (process filter)
  555. "Like `set-process-filter' but keep `with-editor-process-filter'.
  556. Give PROCESS the new FILTER but keep `with-editor-process-filter'
  557. if that was added earlier by the advised `start-file-process'.
  558. Do so by wrapping the two filter functions using a lambda, which
  559. becomes the actual filter. It calls `with-editor-process-filter'
  560. first, passing t as NO-STANDARD-FILTER. Then it calls FILTER,
  561. which may or may not insert the text into the PROCESS's buffer."
  562. (set-process-filter
  563. process
  564. (if (eq (process-filter process) 'with-editor-process-filter)
  565. `(lambda (proc str)
  566. (,filter proc str)
  567. (with-editor-process-filter proc str t))
  568. filter)))
  569. (defvar with-editor-filter-visit-hook nil)
  570. (defun with-editor-output-filter (string)
  571. (save-match-data
  572. (if (string-match "^WITH-EDITOR: \
  573. \\([0-9]+\\) OPEN \\([^]+?\\)\
  574. \\(?: IN \\([^\r]+?\\)\\)?\r?$" string)
  575. (let ((pid (match-string 1 string))
  576. (file (match-string 2 string))
  577. (dir (match-string 3 string)))
  578. (unless (file-name-absolute-p file)
  579. (setq file (expand-file-name file dir)))
  580. (when default-directory
  581. (setq file (concat (file-remote-p default-directory) file)))
  582. (with-current-buffer (find-file-noselect file)
  583. (with-editor-mode 1)
  584. (setq with-editor--pid pid)
  585. (run-hooks 'with-editor-filter-visit-hook)
  586. (funcall (or (with-editor-server-window) 'switch-to-buffer)
  587. (current-buffer))
  588. (kill-local-variable 'server-window))
  589. nil)
  590. string)))
  591. (defun with-editor-process-filter
  592. (process string &optional no-default-filter)
  593. "Listen for edit requests by child processes."
  594. (let ((default-directory (process-get process 'default-dir)))
  595. (with-editor-output-filter string))
  596. (unless no-default-filter
  597. (internal-default-process-filter process string)))
  598. (advice-add 'server-visit-files :after
  599. 'server-visit-files--with-editor-file-name-history-exclude)
  600. (defun server-visit-files--with-editor-file-name-history-exclude
  601. (files _proc &optional _nowait)
  602. (pcase-dolist (`(,file . ,_) files)
  603. (when (cl-find-if (lambda (regexp)
  604. (string-match-p regexp file))
  605. with-editor-file-name-history-exclude)
  606. (setq file-name-history (delete file file-name-history)))))
  607. ;;; Augmentations
  608. ;;;###autoload
  609. (cl-defun with-editor-export-editor (&optional (envvar "EDITOR"))
  610. "Teach subsequent commands to use current Emacs instance as editor.
  611. Set and export the environment variable ENVVAR, by default
  612. \"EDITOR\". The value is automatically generated to teach
  613. commands to use the current Emacs instance as \"the editor\".
  614. This works in `shell-mode', `term-mode', `eshell-mode' and
  615. `vterm'."
  616. (interactive (list (with-editor-read-envvar)))
  617. (cond
  618. ((derived-mode-p 'comint-mode 'term-mode)
  619. (let ((process (get-buffer-process (current-buffer))))
  620. (goto-char (process-mark process))
  621. (process-send-string
  622. process (format " export %s=%s\n" envvar
  623. (shell-quote-argument with-editor-sleeping-editor)))
  624. (while (accept-process-output process 0.1))
  625. (if (derived-mode-p 'term-mode)
  626. (with-editor-set-process-filter process 'with-editor-emulate-terminal)
  627. (add-hook 'comint-output-filter-functions 'with-editor-output-filter
  628. nil t))))
  629. ((derived-mode-p 'eshell-mode)
  630. (add-to-list 'eshell-preoutput-filter-functions
  631. 'with-editor-output-filter)
  632. (setenv envvar with-editor-sleeping-editor))
  633. ((derived-mode-p 'vterm-mode)
  634. (if with-editor-emacsclient-executable
  635. (let ((with-editor--envvar envvar)
  636. (process-environment process-environment))
  637. (with-editor--setup)
  638. (while (accept-process-output vterm--process 0.1))
  639. (when-let ((v (getenv envvar)))
  640. (vterm-send-string (format "export %s=%S" envvar v))
  641. (vterm-send-return))
  642. (when-let ((v (getenv "EMACS_SERVER_FILE")))
  643. (vterm-send-string (format "export EMACS_SERVER_FILE=%S" v))
  644. (vterm-send-return)))
  645. (error "Cannot use sleeping editor in this buffer")))
  646. (t
  647. (error "Cannot export environment variables in this buffer")))
  648. (message "Successfully exported %s" envvar))
  649. ;;;###autoload
  650. (defun with-editor-export-git-editor ()
  651. "Like `with-editor-export-editor' but always set `$GIT_EDITOR'."
  652. (interactive)
  653. (with-editor-export-editor "GIT_EDITOR"))
  654. ;;;###autoload
  655. (defun with-editor-export-hg-editor ()
  656. "Like `with-editor-export-editor' but always set `$HG_EDITOR'."
  657. (interactive)
  658. (with-editor-export-editor "HG_EDITOR"))
  659. (defun with-editor-emulate-terminal (process string)
  660. "Like `term-emulate-terminal' but also handle edit requests."
  661. (when (with-editor-output-filter string)
  662. (term-emulate-terminal process string)))
  663. (defvar with-editor-envvars '("EDITOR" "GIT_EDITOR" "HG_EDITOR"))
  664. (cl-defun with-editor-read-envvar
  665. (&optional (prompt "Set environment variable")
  666. (default "EDITOR"))
  667. (let ((reply (completing-read (if default
  668. (format "%s (%s): " prompt default)
  669. (concat prompt ": "))
  670. with-editor-envvars nil nil nil nil default)))
  671. (if (string= reply "") (user-error "Nothing selected") reply)))
  672. ;;;###autoload
  673. (define-minor-mode shell-command-with-editor-mode
  674. "Teach `shell-command' to use current Emacs instance as editor.
  675. Teach `shell-command', and all commands that ultimately call that
  676. command, to use the current Emacs instance as editor by executing
  677. \"EDITOR=CLIENT COMMAND&\" instead of just \"COMMAND&\".
  678. CLIENT is automatically generated; EDITOR=CLIENT instructs
  679. COMMAND to use to the current Emacs instance as \"the editor\",
  680. assuming no other variable overrides the effect of \"$EDITOR\".
  681. CLIENT may be the path to an appropriate emacsclient executable
  682. with arguments, or a script which also works over Tramp.
  683. Alternatively you can use the `with-editor-async-shell-command',
  684. which also allows the use of another variable instead of
  685. \"EDITOR\"."
  686. :global t)
  687. ;;;###autoload
  688. (defun with-editor-async-shell-command
  689. (command &optional output-buffer error-buffer envvar)
  690. "Like `async-shell-command' but with `$EDITOR' set.
  691. Execute string \"ENVVAR=CLIENT COMMAND\" in an inferior shell;
  692. display output, if any. With a prefix argument prompt for an
  693. environment variable, otherwise the default \"EDITOR\" variable
  694. is used. With a negative prefix argument additionally insert
  695. the COMMAND's output at point.
  696. CLIENT is automatically generated; ENVVAR=CLIENT instructs
  697. COMMAND to use to the current Emacs instance as \"the editor\",
  698. assuming it respects ENVVAR as an \"EDITOR\"-like variable.
  699. CLIENT may be the path to an appropriate emacsclient executable
  700. with arguments, or a script which also works over Tramp.
  701. Also see `async-shell-command' and `shell-command'."
  702. (interactive (with-editor-shell-command-read-args "Async shell command: " t))
  703. (let ((with-editor--envvar envvar))
  704. (with-editor
  705. (async-shell-command command output-buffer error-buffer))))
  706. ;;;###autoload
  707. (defun with-editor-shell-command
  708. (command &optional output-buffer error-buffer envvar)
  709. "Like `shell-command' or `with-editor-async-shell-command'.
  710. If COMMAND ends with \"&\" behave like the latter,
  711. else like the former."
  712. (interactive (with-editor-shell-command-read-args "Shell command: "))
  713. (if (string-match "&[ \t]*\\'" command)
  714. (with-editor-async-shell-command
  715. command output-buffer error-buffer envvar)
  716. (shell-command command output-buffer error-buffer)))
  717. (defun with-editor-shell-command-read-args (prompt &optional async)
  718. (let ((command (read-shell-command
  719. prompt nil nil
  720. (let ((filename (or buffer-file-name
  721. (and (eq major-mode 'dired-mode)
  722. (dired-get-filename nil t)))))
  723. (and filename (file-relative-name filename))))))
  724. (list command
  725. (if (or async (setq async (string-match-p "&[ \t]*\\'" command)))
  726. (< (prefix-numeric-value current-prefix-arg) 0)
  727. current-prefix-arg)
  728. shell-command-default-error-buffer
  729. (and async current-prefix-arg (with-editor-read-envvar)))))
  730. (defun shell-command--shell-command-with-editor-mode
  731. (fn command &optional output-buffer error-buffer)
  732. ;; `shell-mode' and its hook are intended for buffers in which an
  733. ;; interactive shell is running, but `shell-command' also turns on
  734. ;; that mode, even though it only runs the shell to run a single
  735. ;; command. The `with-editor-export-editor' hook function is only
  736. ;; intended to be used in buffers in which an interactive shell is
  737. ;; running, so it has to be remove here.
  738. (let ((shell-mode-hook (remove 'with-editor-export-editor shell-mode-hook)))
  739. (cond ((or (not (or with-editor--envvar shell-command-with-editor-mode))
  740. (not (string-match-p "&\\'" command)))
  741. (funcall fn command output-buffer error-buffer))
  742. ((and with-editor-shell-command-use-emacsclient
  743. with-editor-emacsclient-executable
  744. (not (file-remote-p default-directory)))
  745. (with-editor (funcall fn command output-buffer error-buffer)))
  746. (t
  747. (apply fn (format "%s=%s %s"
  748. (or with-editor--envvar "EDITOR")
  749. (shell-quote-argument with-editor-sleeping-editor)
  750. command)
  751. output-buffer error-buffer)
  752. (ignore-errors
  753. (let ((process (get-buffer-process
  754. (or output-buffer
  755. (get-buffer "*Async Shell Command*")))))
  756. (set-process-filter
  757. process (lambda (proc str)
  758. (comint-output-filter proc str)
  759. (with-editor-process-filter proc str t)))
  760. process))))))
  761. (advice-add 'shell-command :around
  762. 'shell-command--shell-command-with-editor-mode)
  763. ;;; _
  764. (defun with-editor-debug ()
  765. "Debug configuration issues.
  766. See info node `(with-editor)Debugging' for instructions."
  767. (interactive)
  768. (with-current-buffer (get-buffer-create "*with-editor-debug*")
  769. (pop-to-buffer (current-buffer))
  770. (erase-buffer)
  771. (ignore-errors (with-editor))
  772. (insert
  773. (format "with-editor: %s\n" (locate-library "with-editor.el"))
  774. (format "emacs: %s (%s)\n"
  775. (expand-file-name invocation-name invocation-directory)
  776. emacs-version)
  777. "system:\n"
  778. (format " system-type: %s\n" system-type)
  779. (format " system-configuration: %s\n" system-configuration)
  780. (format " system-configuration-options: %s\n" system-configuration-options)
  781. "server:\n"
  782. (format " server-running-p: %s\n" (server-running-p))
  783. (format " server-process: %S\n" server-process)
  784. (format " server-use-tcp: %s\n" server-use-tcp)
  785. (format " server-name: %s\n" server-name)
  786. (format " server-socket-dir: %s\n" server-socket-dir))
  787. (if (and server-socket-dir (file-accessible-directory-p server-socket-dir))
  788. (dolist (file (directory-files server-socket-dir nil "^[^.]"))
  789. (insert (format " %s\n" file)))
  790. (insert (format " %s: not an accessible directory\n"
  791. (if server-use-tcp "WARNING" "ERROR"))))
  792. (insert (format " server-auth-dir: %s\n" server-auth-dir))
  793. (if (file-accessible-directory-p server-auth-dir)
  794. (dolist (file (directory-files server-auth-dir nil "^[^.]"))
  795. (insert (format " %s\n" file)))
  796. (insert (format " %s: not an accessible directory\n"
  797. (if server-use-tcp "ERROR" "WARNING"))))
  798. (let ((val with-editor-emacsclient-executable)
  799. (def (default-value 'with-editor-emacsclient-executable))
  800. (fun (let ((warning-minimum-level :error)
  801. (warning-minimum-log-level :error))
  802. (with-editor-locate-emacsclient))))
  803. (insert "with-editor-emacsclient-executable:\n"
  804. (format " value: %s (%s)\n" val
  805. (and val (with-editor-emacsclient-version val)))
  806. (format " default: %s (%s)\n" def
  807. (and def (with-editor-emacsclient-version def)))
  808. (format " funcall: %s (%s)\n" fun
  809. (and fun (with-editor-emacsclient-version fun)))))
  810. (insert "path:\n"
  811. (format " $PATH: %S\n" (getenv "PATH"))
  812. (format " exec-path: %s\n" exec-path))
  813. (insert (format " with-editor-emacsclient-path:\n"))
  814. (dolist (dir (with-editor-emacsclient-path))
  815. (insert (format " %s (%s)\n" dir (car (file-attributes dir))))
  816. (when (file-directory-p dir)
  817. ;; Don't match emacsclientw.exe, it makes popup windows.
  818. (dolist (exec (directory-files dir t "emacsclient\\(?:[^w]\\|\\'\\)"))
  819. (insert (format " %s (%s)\n" exec
  820. (with-editor-emacsclient-version exec))))))))
  821. (defconst with-editor-font-lock-keywords
  822. '(("(\\(with-\\(?:git-\\)?editor\\)\\_>" (1 'font-lock-keyword-face))))
  823. (font-lock-add-keywords 'emacs-lisp-mode with-editor-font-lock-keywords)
  824. (provide 'with-editor)
  825. ;; Local Variables:
  826. ;; indent-tabs-mode: nil
  827. ;; End:
  828. ;;; with-editor.el ends here