fordon_greeman / voidfiles

Branch: main
Switch branches/tags
main
theidioticdev add emacs 2079ec5 1d ago
1108 lines · 47.6 KB Raw Blame History
1 ;;; with-editor.el --- Use the Emacsclient as $EDITOR -*- lexical-binding:t -*-
2
3 ;; Copyright (C) 2014-2026 The Magit Project Contributors
4
5 ;; Author: Jonas Bernoulli <emacs.with-editor@jonas.bernoulli.dev>
6 ;; Homepage: https://github.com/magit/with-editor
7 ;; Keywords: processes terminals
8
9 ;; Package-Version: 20261101.1104
10 ;; Package-Revision: 3218d480d2c2
11 ;; Package-Requires: (
12 ;; (emacs "28.1")
13 ;; (compat "31.1")
14 ;; (cond-let "1.1")
15 ;; (llama "1.0"))
16
17 ;; SPDX-License-Identifier: GPL-3.0-or-later
18
19 ;; This file is free software: you can redistribute it and/or modify
20 ;; it under the terms of the GNU General Public License as published
21 ;; by the Free Software Foundation, either version 3 of the License,
22 ;; or (at your option) any later version.
23 ;;
24 ;; This file is distributed in the hope that it will be useful,
25 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
26 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27 ;; GNU General Public License for more details.
28 ;;
29 ;; You should have received a copy of the GNU General Public License
30 ;; along with this file. If not, see <https://www.gnu.org/licenses/>.
31
32 ;;; Commentary:
33
34 ;; This library makes it possible to reliably use the Emacsclient as
35 ;; the `$EDITOR' of child processes. It makes sure that they know how
36 ;; to call home. For remote processes a substitute is provided, which
37 ;; communicates with Emacs on standard output/input instead of using a
38 ;; socket as the Emacsclient does.
39
40 ;; It provides the commands `with-editor-async-shell-command' and
41 ;; `with-editor-shell-command', which are intended as replacements
42 ;; for `async-shell-command' and `shell-command'. They automatically
43 ;; export `$EDITOR' making sure the executed command uses the current
44 ;; Emacs instance as "the editor". With a prefix argument these
45 ;; commands prompt for an alternative environment variable such as
46 ;; `$GIT_EDITOR'. To always use these variants add this to your init
47 ;; file:
48 ;;
49 ;; (keymap-global-set "<remap> <async-shell-command>"
50 ;; #'with-editor-async-shell-command)
51 ;; (keymap-global-set "<remap> <shell-command>"
52 ;; #'with-editor-shell-command)
53
54 ;; Alternatively use the global `shell-command-with-editor-mode',
55 ;; which always sets `$EDITOR' for all Emacs commands which ultimately
56 ;; use `shell-command' to asynchronously run some shell command.
57
58 ;; The command `with-editor-export-editor' exports `$EDITOR' or another
59 ;; such environment variable in `shell-mode', `eshell-mode', `term-mode',
60 ;; `vterm-mode', `eat-mode' and `ghostel-mode' buffers. Use this Emacs
61 ;; command before executing a shell command which needs the editor set,
62 ;; or always arrange for the current Emacs instance to be used as editor
63 ;; by adding it to the appropriate mode hooks:
64 ;;
65 ;; (add-hook 'shell-mode-hook #'with-editor-export-editor)
66 ;; (add-hook 'eshell-mode-hook #'with-editor-export-editor)
67 ;; (add-hook 'term-exec-hook #'with-editor-export-editor)
68 ;; (add-hook 'vterm-mode-hook #'with-editor-export-editor)
69 ;; (add-hook 'eat-exec-hook #'with-editor-export-editor)
70 ;; (add-hook 'ghostel-pre-spawn-hook #'with-editor-export-editor)
71
72 ;; Some variants of this function exist, these two forms are
73 ;; equivalent:
74 ;;
75 ;; (add-hook 'shell-mode-hook
76 ;; (apply-partially #'with-editor-export-editor "GIT_EDITOR"))
77 ;; (add-hook 'shell-mode-hook #'with-editor-export-git-editor)
78
79 ;; This library can also be used by other packages which need to use
80 ;; the current Emacs instance as editor. In fact this library was
81 ;; written for Magit and its `git-commit-mode' and `git-rebase-mode'.
82 ;; Consult `git-rebase.el' and the related code in `magit-sequence.el'
83 ;; for a simple example.
84
85 ;;; Code:
86
87 (require 'cl-lib)
88 (require 'compat)
89 (require 'cond-let)
90 (require 'llama)
91 (require 'server)
92 (require 'shell)
93
94 (declare-function dired-get-filename "dired"
95 (&optional localp no-error-if-not-filep))
96 (declare-function eat-self-input "ext:eat" (n &optional e))
97 (declare-function eat-term-parameter "ext:eat" (terminal parameter))
98 (declare-function eat-term-send-string "ext:eat" (terminal string))
99 (declare-function ghostel-send-key "ext:ghostel" (key-name &optional mods))
100 (declare-function ghostel-send-string "ext:ghostel" (string))
101 (declare-function term-emulate-terminal "term" (proc str))
102 (declare-function vterm-send-return "ext:vterm" ())
103 (declare-function vterm-send-string "ext:vterm" (string &optional paste-p))
104
105 (defvar eat-terminal)
106 (defvar eshell-preoutput-filter-functions)
107 (defvar git-commit-post-finish-hook)
108 (defvar vterm--process)
109 (defvar magit--default-directory)
110 (defvar warning-minimum-level)
111 (defvar warning-minimum-log-level)
112
113 ;;; Options
114
115 (defgroup with-editor nil
116 "Use the Emacsclient as $EDITOR."
117 :group 'external
118 :group 'server)
119
120 (defun with-editor-locate-emacsclient ()
121 "Search for a suitable Emacsclient executable."
122 (or (with-editor-locate-emacsclient-1
123 (with-editor-emacsclient-path)
124 (length (split-string emacs-version "\\.")))
125 (prog1 nil (display-warning 'with-editor "\
126 Cannot determine a suitable Emacsclient
127
128 Determining an Emacsclient executable suitable for the
129 current Emacs instance failed. For more information
130 please see https://github.com/magit/magit/wiki/Emacsclient."))))
131
132 (defvar with-editor-emacsclient-program-suffixes
133 (list "-snapshot" ".emacs-snapshot")
134 "Suffixes to append to append when looking for a Emacsclient executables.")
135
136 (defun with-editor-locate-emacsclient-1 (path depth)
137 (let* ((version-lst (cl-subseq (split-string emacs-version "\\.") 0 depth))
138 (version-reg (concat "^" (string-join version-lst "\\."))))
139 (or (locate-file
140 (cond ((equal (downcase invocation-name) "remacs")
141 "remacsclient")
142 ((bound-and-true-p emacsclient-program-name))
143 ("emacsclient"))
144 path
145 (mapcan (lambda (suffix) (mapcar (##concat suffix %) exec-suffixes))
146 (nconc (and (boundp 'debian-emacs-flavor)
147 (list (format ".%s" debian-emacs-flavor)))
148 (cl-mapcon (lambda (ver)
149 (setq ver (string-join (reverse ver) "."))
150 (list ver
151 (concat "-" ver)
152 (concat ".emacs" ver)))
153 (reverse version-lst))
154 (cons "" with-editor-emacsclient-program-suffixes)))
155 (lambda (exec)
156 (ignore-errors
157 (string-match-p version-reg
158 (with-editor-emacsclient-version exec)))))
159 (and (> depth 1)
160 (with-editor-locate-emacsclient-1 path (1- depth))))))
161
162 (defun with-editor-emacsclient-version (exec)
163 (let ((default-directory (file-name-directory exec)))
164 (ignore-errors
165 (cadr (split-string (car (process-lines exec "--version")))))))
166
167 (defun with-editor-emacsclient-path ()
168 (let ((path exec-path))
169 (when invocation-directory
170 (push (directory-file-name invocation-directory) path)
171 (let* ((linkname (expand-file-name invocation-name invocation-directory))
172 (truename (file-chase-links linkname)))
173 (unless (equal truename linkname)
174 (push (directory-file-name (file-name-directory truename)) path)))
175 (when (eq system-type 'darwin)
176 (let ((dir (expand-file-name "bin" invocation-directory)))
177 (when (file-directory-p dir)
178 (push dir path)))
179 (cond
180 ((string-search "Cellar" invocation-directory)
181 (let ((dir (expand-file-name "../../../bin" invocation-directory)))
182 (when (file-directory-p dir)
183 (push dir path))))
184 ((string-search "Emacs.app" invocation-directory)
185 (let ((dir (expand-file-name "../../../../bin" invocation-directory)))
186 (when (file-directory-p dir)
187 (push dir path)))))))
188 (cl-remove-duplicates path :test #'equal)))
189
190 (defcustom with-editor-emacsclient-executable (with-editor-locate-emacsclient)
191 "The Emacsclient executable used by the `with-editor' macro."
192 :group 'with-editor
193 :type '(choice (string :tag "Executable")
194 (const :tag "Don't use Emacsclient" nil)))
195
196 (defcustom with-editor-sleeping-editor "\
197 sh -c '\
198 printf \"\\nWITH-EDITOR: $$ OPEN $0\\037$1\\037 IN $(pwd)\\n\"; \
199 sleep 604800 & sleep=$!; \
200 trap \"kill $sleep; exit 0\" USR1; \
201 trap \"kill $sleep; exit 1\" USR2; \
202 wait $sleep'"
203 "The sleeping editor, used when the Emacsclient cannot be used.
204
205 This fallback is used for asynchronous processes started inside
206 the macro `with-editor', when the process runs on a remote machine
207 or for local processes when `with-editor-emacsclient-executable'
208 is nil (i.e., when no suitable Emacsclient was found, or the user
209 decided not to use it).
210
211 Where the latter uses a socket to communicate with Emacs' server,
212 this substitute prints edit requests to its standard output on
213 which a process filter listens for such requests. As such it is
214 not a complete substitute for a proper Emacsclient, it can only
215 be used as $EDITOR of child process of the current Emacs instance.
216
217 Some shells do not execute traps immediately when waiting for a
218 child process, but by default we do use such a blocking child
219 process.
220
221 If you use such a shell (e.g., `csh' on FreeBSD, but not Debian),
222 then you have to edit this option. You can either replace \"sh\"
223 with \"bash\" (and install that), or you can use the older, less
224 performant implementation:
225
226 \"sh -c '\\
227 echo -e \\\"\\nWITH-EDITOR: $$ OPEN $0$1 IN $(pwd)\\n\\\"; \\
228 trap \\\"exit 0\\\" USR1; \\
229 trap \\\"exit 1\" USR2; \\
230 while true; do sleep 1; done'\"
231
232 Note that the two unit separator characters () right after $0
233 and $1 are required. Normally $0 is the file name and $1 is
234 missing or else gets ignored. But if $0 has the form \"+N[:N]\",
235 then it is treated as a position in the file and $1 is expected
236 to be the file.
237
238 Also note that using this alternative implementation leads to a
239 delay of up to a second. The delay can be shortened by replacing
240 \"sleep 1\" with \"sleep 0.01\", or if your implementation does
241 not support floats, then by using \"nanosleep\" instead."
242 :package-version '(with-editor . "2.8.0")
243 :group 'with-editor
244 :type 'string)
245
246 (defcustom with-editor-finish-query-functions nil
247 "List of functions called to query before finishing session.
248
249 The buffer in question is current while the functions are called.
250 If any of them returns nil, then the session is not finished and
251 the buffer is not killed. The user should then fix the issue and
252 try again. The functions are called with one argument. If it is
253 non-nil then that indicates that the user used a prefix argument
254 to force finishing the session despite issues. Functions should
255 usually honor that and return non-nil."
256 :group 'with-editor
257 :local 'permanent-only
258 :type 'hook)
259 (put 'with-editor-finish-query-functions 'permanent-local t) ;< 31.1
260
261 (defcustom with-editor-cancel-query-functions nil
262 "List of functions called to query before canceling session.
263
264 The buffer in question is current while the functions are called.
265 If any of them returns nil, then the session is not canceled and
266 the buffer is not killed. The user should then fix the issue and
267 try again. The functions are called with one argument. If it is
268 non-nil then that indicates that the user used a prefix argument
269 to force canceling the session despite issues. Functions should
270 usually honor that and return non-nil."
271 :group 'with-editor
272 :local 'permanent-only
273 :type 'hook)
274 (put 'with-editor-cancel-query-functions 'permanent-local t) ;< 31.1
275
276 (defcustom with-editor-mode-lighter " WE"
277 "The mode-line lighter of the With-Editor mode."
278 :group 'with-editor
279 :type '(choice (const :tag "No lighter" "") string))
280
281 (defvar with-editor-server-window-alist nil
282 "Alist of filename patterns vs corresponding `server-window'.
283
284 Each element looks like (REGEXP . FUNCTION). Files matching
285 REGEXP are selected using FUNCTION instead of the default in
286 `server-window'.
287
288 Note that when a package adds an entry here then it probably
289 has a reason to disrespect `server-window' and it likely is
290 not a good idea to change such entries.")
291
292 (defvar with-editor-file-name-history-exclude nil
293 "List of regexps for filenames `server-visit' should not remember.
294 When a filename matches any of the regexps, then `server-visit'
295 does not add it to the variable `file-name-history', which is
296 used when reading a filename in the minibuffer.")
297
298 (defcustom with-editor-shell-command-use-emacsclient t
299 "Whether to use the emacsclient when running shell commands.
300
301 This affects `with-editor-async-shell-command' and, if the input
302 ends with \"&\" `with-editor-shell-command' .
303
304 If `shell-command-with-editor-mode' is enabled, then it also
305 affects `shell-command-async' and, if the input ends with \"&\"
306 `shell-command'.
307
308 This is a temporary kludge that lets you choose between two
309 possible defects, the ones described in the issues #23 and #40.
310
311 When t, then use the emacsclient. This has the disadvantage that
312 `with-editor-mode' won't be enabled because we don't know whether
313 this package was involved at all in the call to the emacsclient,
314 and when it is not, then we really should. The problem is that
315 the emacsclient doesn't pass along any environment variables to
316 the server. This will hopefully be fixed in Emacs eventually.
317
318 When nil, then use the sleeping editor. Because in this case we
319 know that this package is involved, we can enable the mode. But
320 this makes it necessary that you invoke $EDITOR in shell scripts
321 like so:
322
323 eval \"$EDITOR\" file
324
325 And some tools that do not handle $EDITOR properly also break."
326 :package-version '(with-editor . "2.7.1")
327 :group 'with-editor
328 :type 'boolean)
329
330 ;;; Mode Commands
331
332 (defvar with-editor-pre-finish-hook nil)
333 (defvar with-editor-pre-cancel-hook nil)
334 (defvar with-editor-post-finish-hook nil)
335 (defvar with-editor-post-finish-hook-1 nil)
336 (defvar with-editor-post-cancel-hook nil)
337 (defvar with-editor-post-cancel-hook-1 nil)
338 (defvar with-editor-cancel-alist nil)
339 (put 'with-editor-pre-finish-hook 'permanent-local t)
340 (put 'with-editor-pre-cancel-hook 'permanent-local t)
341 (put 'with-editor-post-finish-hook 'permanent-local t)
342 (put 'with-editor-post-cancel-hook 'permanent-local t)
343
344 (defvar-local with-editor-show-usage t)
345 (defvar-local with-editor-cancel-message nil)
346 (defvar-local with-editor-previous-winconf nil)
347 (put 'with-editor-cancel-message 'permanent-local t)
348 (put 'with-editor-previous-winconf 'permanent-local t)
349
350 (defvar-local with-editor--pid nil "For internal use.")
351 (put 'with-editor--pid 'permanent-local t)
352
353 (defun with-editor-finish (force)
354 "Finish the current edit session."
355 (interactive "P")
356 (when (run-hook-with-args-until-failure
357 'with-editor-finish-query-functions force)
358 (let ((post-finish-hook with-editor-post-finish-hook)
359 (post-commit-hook (bound-and-true-p git-commit-post-finish-hook))
360 (dir default-directory))
361 (run-hooks 'with-editor-pre-finish-hook)
362 (with-editor-return nil)
363 (accept-process-output nil 0.1)
364 (with-temp-buffer
365 (setq default-directory dir)
366 (setq-local magit--default-directory dir)
367 (setq-local with-editor-post-finish-hook post-finish-hook)
368 (when post-commit-hook
369 (setq-local git-commit-post-finish-hook post-commit-hook))
370 (run-hooks 'with-editor-post-finish-hook)))))
371
372 (defun with-editor-cancel (force)
373 "Cancel the current edit session."
374 (interactive "P")
375 (when (run-hook-with-args-until-failure
376 'with-editor-cancel-query-functions force)
377 (let ((message with-editor-cancel-message))
378 (when (functionp message)
379 (setq message (funcall message)))
380 (let ((post-cancel-hook with-editor-post-cancel-hook)
381 (with-editor-cancel-alist nil)
382 (dir default-directory))
383 (run-hooks 'with-editor-pre-cancel-hook)
384 (with-editor-return t)
385 (accept-process-output nil 0.1)
386 (with-temp-buffer
387 (setq default-directory dir)
388 (setq-local with-editor-post-cancel-hook post-cancel-hook)
389 (run-hooks 'with-editor-post-cancel-hook)))
390 (message (or message "Canceled by user")))))
391
392 (defun with-editor-return (cancel)
393 (let ((winconf with-editor-previous-winconf)
394 (clients server-buffer-clients)
395 (dir default-directory)
396 (pid with-editor--pid))
397 (remove-hook 'kill-buffer-query-functions
398 #'with-editor-kill-buffer-noop t)
399 (cond (cancel
400 (save-buffer)
401 (if clients
402 (let ((buf (current-buffer)))
403 (dolist (client clients)
404 (when-let* ((terminal (process-get client 'terminal))
405 (_(eq (terminal-live-p terminal) t)))
406 (delete-terminal terminal))
407 (server-send-string client "-error Canceled by user\n")
408 (server-delete-client client))
409 (when (buffer-live-p buf)
410 (kill-buffer buf)))
411 ;; Fallback for when emacs was used as $EDITOR
412 ;; instead of emacsclient or the sleeping editor.
413 ;; See https://github.com/magit/magit/issues/2258.
414 (ignore-errors (delete-file buffer-file-name))
415 (kill-buffer)))
416 (t
417 (save-buffer)
418 (if clients
419 ;; Don't use `server-edit' because we do not want to
420 ;; show another buffer belonging to another client.
421 ;; See https://github.com/magit/magit/issues/2197.
422 (server-done)
423 (kill-buffer))))
424 (when pid
425 (let ((default-directory dir))
426 (process-file "kill" nil nil nil
427 "-s" (if cancel "USR2" "USR1") pid)))
428 (when (and winconf (eq (window-configuration-frame winconf)
429 (selected-frame)))
430 (set-window-configuration winconf))))
431
432 ;;; Mode
433
434 (defvar-keymap with-editor-mode-map
435 "C-c C-c" #'with-editor-finish
436 "<remap> <server-edit>" #'with-editor-finish
437 "<remap> <evil-save-and-close>" #'with-editor-finish
438 "<remap> <evil-save-modified-and-close>" #'with-editor-finish
439 "C-c C-k" #'with-editor-cancel
440 "<remap> <kill-buffer>" #'with-editor-cancel
441 "<remap> <ido-kill-buffer>" #'with-editor-cancel
442 "<remap> <iswitchb-kill-buffer>" #'with-editor-cancel
443 "<remap> <evil-quit>" #'with-editor-cancel)
444
445 (define-minor-mode with-editor-mode
446 "Edit a file as the $EDITOR of an external process."
447 :lighter with-editor-mode-lighter
448 ;; Protect the user from enabling or disabling the mode interactively.
449 ;; Manually enabling the mode is dangerous because canceling the buffer
450 ;; deletes the visited file. The mode must not be disabled manually,
451 ;; either `with-editor-finish' or `with-editor-cancel' must be used.
452 :interactive nil
453 ;; The buffer must also not be killed using regular kill commands.
454 (add-hook 'kill-buffer-query-functions
455 #'with-editor-kill-buffer-noop nil t)
456 ;; `server-execute' displays a message which is not
457 ;; correct when using this mode.
458 (when with-editor-show-usage
459 (with-editor-usage-message)))
460
461 (put 'with-editor-mode 'permanent-local t)
462
463 (defun with-editor-kill-buffer-noop ()
464 ;; We started doing this in response to #64, but it is not safe
465 ;; to do so, because the client has already been killed, causing
466 ;; `with-editor-return' (called by `with-editor-cancel') to delete
467 ;; the file, see #66. The reason we delete the file in the first
468 ;; place are https://github.com/magit/magit/issues/2258 and
469 ;; https://github.com/magit/magit/issues/2248.
470 ;; (if (memq this-command '(save-buffers-kill-terminal
471 ;; save-buffers-kill-emacs))
472 ;; (let ((with-editor-cancel-query-functions nil))
473 ;; (with-editor-cancel nil)
474 ;; t)
475 ;; ...)
476 ;; So go back to always doing this instead:
477 (user-error (substitute-command-keys (format "\
478 Don't kill this buffer %S. Instead cancel using \\[with-editor-cancel]"
479 (current-buffer)))))
480
481 (defvar-local with-editor-usage-message "\
482 Type \\[with-editor-finish] to finish, \
483 or \\[with-editor-cancel] to cancel")
484
485 (defun with-editor-usage-message ()
486 ;; Run after `server-execute', which is run using
487 ;; a timer which starts immediately.
488 (let ((buffer (current-buffer)))
489 (run-with-timer
490 0.05 nil
491 (lambda ()
492 (with-current-buffer buffer
493 (message (substitute-command-keys with-editor-usage-message)))))))
494
495 ;;; Wrappers
496
497 (defvar with-editor--envvar nil "For internal use.")
498
499 (defmacro with-editor (&rest body)
500 "Use the Emacsclient as $EDITOR while evaluating BODY.
501 Modify the `process-environment' for processes started in BODY,
502 instructing them to use the Emacsclient as $EDITOR. If optional
503 ENVVAR is a literal string then bind that environment variable
504 instead.
505 \n(fn [ENVVAR] BODY...)"
506 (declare (indent defun) (debug (body)))
507 `(let ((with-editor--envvar ,(if (stringp (car body))
508 (pop body)
509 '(or with-editor--envvar "EDITOR")))
510 (process-environment (copy-sequence process-environment)))
511 (with-editor--setup)
512 ,@body))
513
514 (defmacro with-editor* (envvar &rest body)
515 "Use the Emacsclient as the editor while evaluating BODY.
516 Modify the `process-environment' for processes started in BODY,
517 instructing them to use the Emacsclient as editor. ENVVAR is the
518 environment variable that is exported to do so, it is evaluated
519 at run-time.
520 \n(fn ENVVAR BODY...)"
521 (declare (indent defun) (debug (sexp body)))
522 `(let ((with-editor--envvar ,envvar)
523 (process-environment (copy-sequence process-environment)))
524 (with-editor--setup)
525 ,@body))
526
527 (defun with-editor--setup ()
528 (if (or (not with-editor-emacsclient-executable)
529 (file-remote-p default-directory))
530 (setenv with-editor--envvar with-editor-sleeping-editor)
531 ;; Make sure server-use-tcp's value is valid.
532 (unless (featurep 'make-network-process '(:family local))
533 (setq server-use-tcp t))
534 ;; Make sure the server is running.
535 (unless (process-live-p server-process)
536 (when (server-running-p server-name)
537 (setq server-name (format "server%s" (emacs-pid)))
538 (when (server-running-p server-name)
539 (server-force-delete server-name)))
540 (server-start))
541 ;; Tell $EDITOR to use the Emacsclient.
542 (setenv with-editor--envvar
543 (concat
544 ;; Quoting is the right thing to do. Applications that
545 ;; fail because of that, are the ones that need fixing,
546 ;; e.g., by using 'eval "$EDITOR" file'. See #121.
547 (shell-quote-argument
548 ;; If users set the executable manually, they might
549 ;; begin the path with "~", which would get quoted.
550 (if (string-prefix-p "~" with-editor-emacsclient-executable)
551 (concat (expand-file-name "~")
552 (substring with-editor-emacsclient-executable 1))
553 with-editor-emacsclient-executable))
554 ;; Tell the process where the server file is.
555 (and (not server-use-tcp)
556 (concat " --socket-name="
557 (shell-quote-argument
558 (expand-file-name server-name
559 server-socket-dir))))))
560 (when server-use-tcp
561 (setenv "EMACS_SERVER_FILE"
562 (expand-file-name server-name server-auth-dir)))
563 ;; As last resort fallback to the sleeping editor.
564 (setenv "ALTERNATE_EDITOR" with-editor-sleeping-editor)
565 ;; Work around bug in server.el of Emacs < 31.1. #139
566 (when (member (getenv "TERM") '(nil ""))
567 (setenv "TERM" "dumb"))))
568
569 (defun with-editor-server-window ()
570 (or (and buffer-file-name
571 (cdr (cl-find-if (##string-match-p (car %) buffer-file-name)
572 with-editor-server-window-alist)))
573 server-window))
574
575 (define-advice server-switch-buffer
576 (:around (fn &optional next-buffer &rest args)
577 with-editor-server-window-alist)
578 "Honor `with-editor-server-window-alist' (which see)."
579 (let ((server-window (with-current-buffer
580 (or next-buffer (current-buffer))
581 (when with-editor-mode
582 (setq with-editor-previous-winconf
583 (current-window-configuration)))
584 (with-editor-server-window))))
585 (apply fn next-buffer args)))
586
587 (define-advice start-file-process
588 (:around (fn name buffer program &rest program-args)
589 with-editor-process-filter)
590 "When called inside a `with-editor' form and the Emacsclient
591 cannot be used, then give the process the filter function
592 `with-editor-process-filter'. To avoid overriding the filter
593 being added here you should use `with-editor-set-process-filter'
594 instead of `set-process-filter' inside `with-editor' forms.
595
596 When the `default-directory' is located on a remote machine,
597 then also manipulate PROGRAM and PROGRAM-ARGS in order to set
598 the appropriate editor environment variable."
599 (if (not with-editor--envvar)
600 (apply fn name buffer program program-args)
601 (when (file-remote-p default-directory)
602 (unless (equal program "env")
603 (push program program-args)
604 (setq program "env"))
605 (push (concat with-editor--envvar "=" with-editor-sleeping-editor)
606 program-args))
607 (let ((process (apply fn name buffer program program-args)))
608 (set-process-filter process #'with-editor-process-filter)
609 (process-put process 'default-dir default-directory)
610 process)))
611
612 (advice-add #'make-process :around
613 #'make-process@with-editor-process-filter)
614 (cl-defun make-process@with-editor-process-filter
615 (fn &rest keys &key name buffer command coding noquery stop
616 connection-type filter sentinel stderr file-handler
617 &allow-other-keys)
618 "When called inside a `with-editor' form and the Emacsclient
619 cannot be used, then give the process the filter function
620 `with-editor-process-filter'. To avoid overriding the filter
621 being added here you should use `with-editor-set-process-filter'
622 instead of `set-process-filter' inside `with-editor' forms.
623
624 When the `default-directory' is located on a remote machine and
625 FILE-HANDLER is non-nil, then also manipulate COMMAND in order
626 to set the appropriate editor environment variable."
627 (if (or (not file-handler) (not with-editor--envvar))
628 (apply fn keys)
629 (when (file-remote-p default-directory)
630 (unless (equal (car command) "env")
631 (push "env" command))
632 (push (concat with-editor--envvar "=" with-editor-sleeping-editor)
633 (cdr command)))
634 (let* ((filter (if filter
635 (lambda (process output)
636 (funcall filter process output)
637 (with-editor-process-filter process output t))
638 #'with-editor-process-filter))
639 (process (funcall fn
640 :name name
641 :buffer buffer
642 :command command
643 :coding coding
644 :noquery noquery
645 :stop stop
646 :connection-type connection-type
647 :filter filter
648 :sentinel sentinel
649 :stderr stderr
650 :file-handler file-handler)))
651 (process-put process 'default-dir default-directory)
652 process)))
653
654 (defun with-editor-set-process-filter (process filter)
655 "Like `set-process-filter' but keep `with-editor-process-filter'.
656 Give PROCESS the new FILTER but keep `with-editor-process-filter'
657 if that was added earlier by the advised `start-file-process'.
658
659 Do so by wrapping the two filter functions using a lambda, which
660 becomes the actual filter. It calls FILTER first, which may or
661 may not insert the text into the PROCESS's buffer. Then it calls
662 `with-editor-process-filter', passing t as NO-STANDARD-FILTER."
663 (set-process-filter
664 process
665 (if (eq (process-filter process) 'with-editor-process-filter)
666 `(lambda (proc str)
667 (,filter proc str)
668 (with-editor-process-filter proc str t))
669 filter)))
670
671 (defvar with-editor-filter-visit-hook nil)
672
673 (defconst with-editor-sleeping-editor-regexp "^\
674 WITH-EDITOR: \\([0-9]+\\) \
675 OPEN \\([^]+?\\)\
676 \\(?:\\([^]*\\)\\)?\
677 \\(?: IN \\([^\r]+?\\)\\)?\r?$")
678
679 (defvar with-editor--max-incomplete-length 1000)
680
681 (defun with-editor-sleeping-editor-filter (process string)
682 (when-let ((incomplete (and process (process-get process 'incomplete))))
683 (setq string (concat incomplete string)))
684 (save-match-data
685 (cond
686 ((and process (not (string-suffix-p "\n" string)))
687 (let ((length (length string)))
688 (when (> length with-editor--max-incomplete-length)
689 (setq string
690 (substring string
691 (- length with-editor--max-incomplete-length)))))
692 (process-put process 'incomplete string)
693 nil)
694 ((string-match with-editor-sleeping-editor-regexp string)
695 (when process
696 (process-put process 'incomplete nil))
697 (let ((pid (match-string 1 string))
698 (arg0 (match-string 2 string))
699 (arg1 (match-string 3 string))
700 (dir (match-string 4 string))
701 file line column)
702 (cond ((string-match "\\`\\+\\([0-9]+\\)\\(?::\\([0-9]+\\)\\)?\\'" arg0)
703 (setq file arg1)
704 (setq line (string-to-number (match-string 1 arg0)))
705 (setq column (match-string 2 arg0))
706 (setq column (and column (string-to-number column))))
707 ((setq file arg0)))
708 (unless (file-name-absolute-p file)
709 (setq file (expand-file-name file dir)))
710 (when default-directory
711 (setq file (concat (file-remote-p default-directory) file)))
712 (with-current-buffer (find-file-noselect file)
713 (with-editor-mode 1)
714 (setq with-editor--pid pid)
715 (setq with-editor-previous-winconf
716 (current-window-configuration))
717 (when line
718 (let ((pos (save-excursion
719 (save-restriction
720 (goto-char (point-min))
721 (forward-line (1- line))
722 (when column
723 (move-to-column column))
724 (point)))))
725 (when (and (buffer-narrowed-p)
726 widen-automatically
727 (not (<= (point-min) pos (point-max))))
728 (widen))
729 (goto-char pos)))
730 (run-hooks 'with-editor-filter-visit-hook)
731 (funcall (or (with-editor-server-window) #'switch-to-buffer)
732 (current-buffer))
733 (kill-local-variable 'server-window)))
734 nil)
735 (t string))))
736
737 (defun with-editor-process-filter
738 (process string &optional no-default-filter)
739 "Listen for edit requests by child processes."
740 (let ((default-directory (process-get process 'default-dir)))
741 (with-editor-sleeping-editor-filter process string))
742 (unless no-default-filter
743 (internal-default-process-filter process string)))
744
745 (define-advice server-visit-files
746 (:after (files _proc &optional _nowait)
747 with-editor-file-name-history-exclude)
748 "Prevent certain files from being added to `file-name-history'.
749 Files matching a regexp in `with-editor-file-name-history-exclude'
750 are prevented from being added to that list."
751 (pcase-dolist (`(,file . ,_) files)
752 (when (cl-find-if (##string-match-p % file)
753 with-editor-file-name-history-exclude)
754 (setq file-name-history
755 (delete (abbreviate-file-name file) file-name-history)))))
756
757 ;;; Augmentations
758
759 ;;;###autoload
760 (cl-defun with-editor-export-editor
761 (&optional (envvar "EDITOR") process interactive)
762 "Teach subsequent commands to use current Emacs instance as editor.
763
764 Set and export the environment variable ENVVAR, by default \"EDITOR\".
765 The value is automatically generated to teach commands to use the
766 current Emacs instance as \"the editor\".
767
768 PROCESS is only intended for use by `eat-exec-hook'. When invoked
769 interactively, INTERACTIVE is non-nil, which suppresses the call to
770 \"clear\" (only relevant in `vterm-mode', `eat-mode', and `ghostel-mode').
771
772 This command can be used in `shell-mode', `term-mode', `eshell-mode',
773 `vterm-mode', `eat-mode' and `ghostel-mode'."
774 (interactive (list (with-editor-read-envvar) nil t))
775 (with-editor--export-editor envvar interactive process))
776
777 (cl-defmethod with-editor--export-editor (_ _ _)
778 (user-error "with-editor-export-editor cannot be used in %s buffers"
779 major-mode))
780
781 (cl-defmethod with-editor--export-editor ;commit-mode
782 (envvar _ _ &context (major-mode comint-mode))
783 (when-let ((process (get-buffer-process (current-buffer))))
784 (goto-char (process-mark process))
785 (process-send-string
786 process (with-editor--format-export envvar with-editor-sleeping-editor t))
787 (while (accept-process-output process 1 nil t))
788 (add-hook 'comint-output-filter-functions #'with-editor-output-filter nil t)))
789
790 (cl-defmethod with-editor--export-editor ;term-mode
791 (envvar _ _ &context (major-mode term-mode))
792 (when-let ((process (get-buffer-process (current-buffer))))
793 (goto-char (process-mark process))
794 (process-send-string
795 process (with-editor--format-export envvar with-editor-sleeping-editor t))
796 (while (accept-process-output process 1 nil t))
797 (with-editor-set-process-filter process #'with-editor-emulate-terminal)))
798
799 (cl-defmethod with-editor--export-editor ;eshell-mode
800 (envvar _ _ &context (major-mode eshell-mode))
801 (add-to-list 'eshell-preoutput-filter-functions #'with-editor-output-filter)
802 (setenv envvar with-editor-sleeping-editor))
803
804 (cl-defmethod with-editor--export-editor ;vterm-mode
805 (envvar interactive _ &context (major-mode vterm-mode))
806 (let ((process-environment (copy-sequence process-environment))
807 (with-editor--envvar envvar))
808 (with-editor--setup)
809 (while (accept-process-output vterm--process 1 nil t))
810 (when (or (not with-editor-emacsclient-executable)
811 (file-remote-p default-directory))
812 (add-function :before (process-filter vterm--process)
813 #'with-editor-sleeping-editor-filter))
814 (when$ (getenv envvar)
815 (vterm-send-string (with-editor--format-export envvar $))
816 (vterm-send-return))
817 (when$ (getenv "EMACS_SERVER_FILE")
818 (vterm-send-string (with-editor--format-export "EMACS_SERVER_FILE" $))
819 (vterm-send-return))
820 (unless interactive
821 (vterm-send-string " clear")
822 (vterm-send-return))))
823
824 (cl-defmethod with-editor--export-editor ;eat-mode
825 (envvar interactive process &context (major-mode eat-mode))
826 (let* ((process-environment (copy-sequence process-environment))
827 ;; `eat-exec-hook' calls this function with one argument. If
828 ;; (apply-partially #'with-editor-export-editor "SOMEEDITOR"))
829 ;; was added to that hook, we receive that as the PROCESS
830 ;; argument. However, if this function is called via one of
831 ;; the commands below, then ENVVAR actually is an envvar, and
832 ;; the process has to be determined by other means.
833 (process (cond ((processp envvar)
834 (prog1 envvar (setq envvar "EDITOR")))
835 ((processp process) process)
836 ((eat-term-parameter eat-terminal 'eat--process))))
837 (with-editor--envvar envvar))
838 (with-editor--setup)
839 (while (accept-process-output process 1 nil t))
840 (when (or (not with-editor-emacsclient-executable)
841 (file-remote-p default-directory))
842 (add-function :before (process-filter process)
843 #'with-editor-sleeping-editor-filter))
844 (when$ (getenv envvar)
845 (eat-term-send-string
846 eat-terminal (with-editor--format-export envvar $))
847 (eat-self-input 1 'return))
848 (when$ (getenv "EMACS_SERVER_FILE")
849 (eat-term-send-string
850 eat-terminal (with-editor--format-export "EMACS_SERVER_FILE" $))
851 (eat-self-input 1 'return))
852 (unless interactive
853 (eat-term-send-string eat-terminal " clear")
854 (eat-self-input 1 'return))))
855
856 (cl-defmethod with-editor--export-editor ;ghostel-mode
857 (envvar interactive _ &context (major-mode ghostel-mode))
858 (cond
859 ((file-remote-p default-directory)
860 (message "With-editor cannot be used in ghostel on remote systems"))
861 ((not with-editor-emacsclient-executable)
862 (message "With-editor cannot be used in ghostel with sleeping editor"))
863 (interactive
864 (let ((process-environment (copy-sequence process-environment))
865 (with-editor--envvar envvar))
866 (with-editor--setup)
867 (when$ (getenv envvar)
868 (ghostel-send-string (with-editor--format-export envvar $))
869 (ghostel-send-key "return"))
870 (when$ (getenv "EMACS_SERVER_FILE")
871 (ghostel-send-string (with-editor--format-export "EMACS_SERVER_FILE" $))
872 (ghostel-send-key "return"))))
873 ((let ((with-editor--envvar envvar))
874 (with-editor--setup)))))
875
876 (defun with-editor--format-export (envvar val &optional lf)
877 (format " export %s=%s%s" envvar (shell-quote-argument val) (if lf "\n" "")))
878
879 ;;;###autoload
880 (defun with-editor-export-git-editor (&optional process interactive)
881 "Like `with-editor-export-editor' but always set `$GIT_EDITOR'.
882
883 PROCESS is only intended for use by `eat-exec-hook'. When invoked
884 interactively, INTERACTIVE is non-nil, which suppresses the call to
885 \"clear\"."
886 (interactive (list nil t))
887 (with-editor-export-editor "GIT_EDITOR" process interactive))
888
889 ;;;###autoload
890 (defun with-editor-export-hg-editor (&optional process interactive)
891 "Like `with-editor-export-editor' but always set `$HG_EDITOR'.
892
893 PROCESS is only intended for use by `eat-exec-hook'. When invoked
894 interactively, INTERACTIVE is non-nil, which suppresses the call to
895 \"clear\"."
896 (interactive (list nil t))
897 (with-editor-export-editor "HG_EDITOR" process interactive))
898
899 (defun with-editor-output-filter (string)
900 "Handle edit requests on behalf of `comint-mode' and `eshell-mode'."
901 (with-editor-sleeping-editor-filter nil string))
902
903 (defun with-editor-emulate-terminal (process string)
904 "Like `term-emulate-terminal' but also handle edit requests."
905 (let ((with-editor-sleeping-editor-regexp
906 (substring with-editor-sleeping-editor-regexp 1)))
907 (with-editor-sleeping-editor-filter process string))
908 (term-emulate-terminal process string))
909
910 (defvar with-editor-envvars '("EDITOR" "GIT_EDITOR" "HG_EDITOR"))
911
912 (cl-defun with-editor-read-envvar
913 (&optional (prompt "Set environment variable")
914 (default "EDITOR"))
915 (let ((reply (completing-read (if default
916 (format "%s (%s): " prompt default)
917 (concat prompt ": "))
918 with-editor-envvars nil nil nil nil default)))
919 (if (string= reply "") (user-error "Nothing selected") reply)))
920
921 ;;;###autoload
922 (define-minor-mode shell-command-with-editor-mode
923 "Teach `shell-command' to use current Emacs instance as editor.
924
925 Teach `shell-command', and all commands that ultimately call that
926 command, to use the current Emacs instance as editor by executing
927 \"EDITOR=CLIENT COMMAND&\" instead of just \"COMMAND&\".
928
929 CLIENT is automatically generated; EDITOR=CLIENT instructs
930 COMMAND to use to the current Emacs instance as \"the editor\",
931 assuming no other variable overrides the effect of \"$EDITOR\".
932 CLIENT may be the path to an appropriate emacsclient executable
933 with arguments, or a script which also works over Tramp.
934
935 Alternatively you can use the `with-editor-async-shell-command',
936 which also allows the use of another variable instead of
937 \"EDITOR\"."
938 :global t)
939
940 ;;;###autoload
941 (defun with-editor-async-shell-command
942 (command &optional output-buffer error-buffer envvar)
943 "Like `async-shell-command' but with `$EDITOR' set.
944
945 Execute string \"ENVVAR=CLIENT COMMAND\" in an inferior shell;
946 display output, if any. With a prefix argument prompt for an
947 environment variable, otherwise the default \"EDITOR\" variable
948 is used. With a negative prefix argument additionally insert
949 the COMMAND's output at point.
950
951 CLIENT is automatically generated; ENVVAR=CLIENT instructs
952 COMMAND to use to the current Emacs instance as \"the editor\",
953 assuming it respects ENVVAR as an \"EDITOR\"-like variable.
954 CLIENT may be the path to an appropriate emacsclient executable
955 with arguments, or a script which also works over Tramp.
956
957 Also see `async-shell-command' and `shell-command'."
958 (interactive (with-editor-shell-command-read-args "Async shell command: " t))
959 (let ((with-editor--envvar envvar))
960 (with-editor
961 (async-shell-command command output-buffer error-buffer))))
962
963 ;;;###autoload
964 (defun with-editor-shell-command
965 (command &optional output-buffer error-buffer envvar)
966 "Like `shell-command' or `with-editor-async-shell-command'.
967 If COMMAND ends with \"&\" behave like the latter,
968 else like the former."
969 (interactive (with-editor-shell-command-read-args "Shell command: "))
970 (if (string-match "&[ \t]*\\'" command)
971 (with-editor-async-shell-command
972 command output-buffer error-buffer envvar)
973 (shell-command command output-buffer error-buffer)))
974
975 (defun with-editor-shell-command-read-args (prompt &optional async)
976 (let ((command (read-shell-command
977 prompt nil nil
978 (let ((filename (or buffer-file-name
979 (and (eq major-mode 'dired-mode)
980 (dired-get-filename nil t)))))
981 (and filename (file-relative-name filename))))))
982 (list command
983 (if (or async (setq async (string-match-p "&[ \t]*\\'" command)))
984 (< (prefix-numeric-value current-prefix-arg) 0)
985 current-prefix-arg)
986 shell-command-default-error-buffer
987 (and async current-prefix-arg (with-editor-read-envvar)))))
988
989 (define-advice shell-command
990 (:around (fn command &optional output-buffer error-buffer)
991 shell-command-with-editor-mode)
992 "Set editor envvar, if `shell-command-with-editor-mode' is enabled.
993 Also take care of that for `with-editor-[async-]shell-command'."
994 ;; `shell-mode' and its hook are intended for buffers in which an
995 ;; interactive shell is running, but `shell-command' also turns on
996 ;; that mode, even though it only runs the shell to run a single
997 ;; command. The `with-editor-export-editor' hook function is only
998 ;; intended to be used in buffers in which an interactive shell is
999 ;; running, so it has to be removed here.
1000 (let ((shell-mode-hook (remove 'with-editor-export-editor shell-mode-hook)))
1001 (cond
1002 ;; If `with-editor-async-shell-command' was used, then `with-editor'
1003 ;; was used, and `with-editor--envvar'. `with-editor-shell-command'
1004 ;; only goes down that path if the command ends with "&". We might
1005 ;; still have to use `with-editor' here, for `async-shell-command'
1006 ;; or `shell-command', if the mode is enabled.
1007 ((and (string-suffix-p "&" command)
1008 (or with-editor--envvar
1009 shell-command-with-editor-mode))
1010 (if with-editor--envvar
1011 (funcall fn command output-buffer error-buffer)
1012 (with-editor (funcall fn command output-buffer error-buffer)))
1013 ;; The comint filter was overridden with our filter. Use both.
1014 (and-let ((process (get-buffer-process
1015 (or output-buffer
1016 (get-buffer "*Async Shell Command*")))))
1017 (prog1 process
1018 (add-function :after (process-filter process)
1019 (lambda (proc str)
1020 (with-editor-process-filter proc str t))))))
1021 ((funcall fn command output-buffer error-buffer)))))
1022
1023 ;;; _
1024
1025 (defun with-editor-debug ()
1026 "Debug configuration issues.
1027 See info node `(with-editor)Debugging' for instructions."
1028 (interactive)
1029 (require 'warnings)
1030 (with-current-buffer (get-buffer-create "*with-editor-debug*")
1031 (pop-to-buffer (current-buffer))
1032 (erase-buffer)
1033 (ignore-errors (with-editor))
1034 (insert
1035 (format "with-editor: %s\n" (locate-library "with-editor.el"))
1036 (format "emacs: %s (%s)\n"
1037 (expand-file-name invocation-name invocation-directory)
1038 emacs-version)
1039 "system:\n"
1040 (format " system-type: %s\n" system-type)
1041 (format " system-configuration: %s\n" system-configuration)
1042 (format " system-configuration-options: %s\n" system-configuration-options)
1043 "server:\n"
1044 (format " server-running-p: %s\n" (server-running-p))
1045 (format " server-process: %S\n" server-process)
1046 (format " server-use-tcp: %s\n" server-use-tcp)
1047 (format " server-name: %s\n" server-name)
1048 (format " server-socket-dir: %s\n" server-socket-dir))
1049 (if (and server-socket-dir (file-accessible-directory-p server-socket-dir))
1050 (dolist (file (directory-files server-socket-dir nil "^[^.]"))
1051 (insert (format " %s\n" file)))
1052 (insert (format " %s: not an accessible directory\n"
1053 (if server-use-tcp "WARNING" "ERROR"))))
1054 (insert (format " server-auth-dir: %s\n" server-auth-dir))
1055 (if (file-accessible-directory-p server-auth-dir)
1056 (dolist (file (directory-files server-auth-dir nil "^[^.]"))
1057 (insert (format " %s\n" file)))
1058 (insert (format " %s: not an accessible directory\n"
1059 (if server-use-tcp "ERROR" "WARNING"))))
1060 (let ((val with-editor-emacsclient-executable)
1061 (def (default-value 'with-editor-emacsclient-executable))
1062 (fun (let ((warning-minimum-level :error)
1063 (warning-minimum-log-level :error))
1064 (with-editor-locate-emacsclient))))
1065 (insert "with-editor-emacsclient-executable:\n"
1066 (format " value: %s (%s)\n" val
1067 (and val (with-editor-emacsclient-version val)))
1068 (format " default: %s (%s)\n" def
1069 (and def (with-editor-emacsclient-version def)))
1070 (format " funcall: %s (%s)\n" fun
1071 (and fun (with-editor-emacsclient-version fun)))))
1072 (insert "path:\n"
1073 (format " $PATH: %s\n" (split-string (getenv "PATH") ":"))
1074 (format " exec-path: %s\n" exec-path))
1075 (insert (format " with-editor-emacsclient-path:\n"))
1076 (dolist (dir (with-editor-emacsclient-path))
1077 (insert (format " %s (%s)\n" dir (car (file-attributes dir))))
1078 (when (file-directory-p dir)
1079 ;; Don't match emacsclientw.exe, it makes popup windows.
1080 (dolist (exec (directory-files dir t "emacsclient\\(?:[^w]\\|\\'\\)"))
1081 (insert (format " %s (%s)\n" exec
1082 (with-editor-emacsclient-version exec))))))))
1083
1084 (defconst with-editor-font-lock-keywords
1085 '(("(\\(with-\\(?:git-\\)?editor\\)\\_>" (1 'font-lock-keyword-face))))
1086 (font-lock-add-keywords 'emacs-lisp-mode with-editor-font-lock-keywords)
1087
1088 (provide 'with-editor)
1089 ;; Local Variables:
1090 ;; byte-compile-warnings: (not docstrings-control-chars)
1091 ;; indent-tabs-mode: nil
1092 ;; lisp-indent-local-overrides: ((cond . 0) (interactive . 0))
1093 ;; read-symbol-shorthands: (
1094 ;; ("and$" . "cond-let--and$")
1095 ;; ("thread$" . "cond-let--thread$")
1096 ;; ("when$" . "cond-let--when$")
1097 ;; ("and-let*" . "cond-let--and-let*")
1098 ;; ("and-let" . "cond-let--and-let")
1099 ;; ("if-let*" . "cond-let--if-let*")
1100 ;; ("if-let" . "cond-let--if-let")
1101 ;; ("when-let*" . "cond-let--when-let*")
1102 ;; ("when-let" . "cond-let--when-let")
1103 ;; ("while-let*" . "cond-let--while-let*")
1104 ;; ("while-let" . "cond-let--while-let")
1105 ;; ("match-string" . "match-string")
1106 ;; ("match-str" . "match-string-no-properties"))
1107 ;; End:
1108 ;;; with-editor.el ends here