| 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 |