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