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