commit 8098ad9679c7f5ea19493bdae18227f7a81b3eb4 (HEAD, refs/remotes/origin/master) Author: Eli Zaretskii Date: Sun May 5 08:20:39 2024 +0300 ; Improve documentation of a recent change * lisp/progmodes/project.el (project-files-relative-names): * etc/NEWS: Improve documentation of 'project-files-relative-names'. diff --git a/etc/NEWS b/etc/NEWS index 014184f1fa6..0a1c217d897 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -696,9 +696,11 @@ you can add this to your init script: (setopt project-switch-commands #'project-prefix-or-any-command) +--- *** New variable 'project-files-relative-names'. -Project backends can support it to improve the performance of their -'project-files' implementation when this variable is non-nil. +If it's non-nil, 'project-files' can return file names relative to the +project root. Project backends can use this to improve the performance +of their 'project-files' implementation. ** VC diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index b716d442aed..a3181340411 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -324,10 +324,11 @@ end it with `/'. DIR must be either `project-root' or one of (cdr project)) (defvar project-files-relative-names nil - "When non-nil, `project-files' is allowed to return relative names. -The names will be relative to the project root. And this can only -happen when all returned files are in the same directory. Meaning, the -DIRS argument has to be nil or have only one element.") + "If non-nil, `project-files' is allowed to return relative file names. +The file names should be relative to the project root. And this can +only happen when all returned files are in the same directory. +In other words, the DIRS argument of `project-files' has to be nil or a +list of only one element.") (cl-defgeneric project-files (project &optional dirs) "Return a list of files in directories DIRS in PROJECT. commit 370b216f08699bdd85b910868642df441c06306c Author: Dmitry Gutov Date: Sun May 5 06:27:39 2024 +0300 New variable 'project-files-relative-names' * lisp/progmodes/project.el (project-files-relative-names): New variable (bug#69233). (project--files-in-directory): Honor it. (project--vc-list-files): Here too. (project-find-regexp): Use it to improve performance. (project-or-external-find-regexp): Add a TODO. (project-find-file): Use it here too. (project--read-file-cpd-relative, project--read-file-absolute): Try to handle file lists with absolute and relative files names. (project-find-file-in): Set default-directory, so relative names are interpreted correctly. * lisp/progmodes/xref.el (xref-matches-in-files): Consider that the first in FILES can be a relative file name. * test/lisp/progmodes/project-tests.el (project-find-regexp): New test. * etc/NEWS: Mention it. diff --git a/etc/NEWS b/etc/NEWS index 9b264a23d5c..014184f1fa6 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -696,6 +696,10 @@ you can add this to your init script: (setopt project-switch-commands #'project-prefix-or-any-command) +*** New variable 'project-files-relative-names'. +Project backends can support it to improve the performance of their +'project-files' implementation when this variable is non-nil. + ** VC --- diff --git a/lisp/progmodes/project.el b/lisp/progmodes/project.el index 000a05804a8..b716d442aed 100644 --- a/lisp/progmodes/project.el +++ b/lisp/progmodes/project.el @@ -323,6 +323,12 @@ end it with `/'. DIR must be either `project-root' or one of (cl-defmethod project-root ((project (head transient))) (cdr project)) +(defvar project-files-relative-names nil + "When non-nil, `project-files' is allowed to return relative names. +The names will be relative to the project root. And this can only +happen when all returned files are in the same directory. Meaning, the +DIRS argument has to be nil or have only one element.") + (cl-defgeneric project-files (project &optional dirs) "Return a list of files in directories DIRS in PROJECT. DIRS is a list of absolute directories; it should be some @@ -345,7 +351,6 @@ to find the list of ignores for each directory." ;; expanded and not left for the shell command ;; to interpret. (localdir (file-name-unquote (file-local-name (expand-file-name dir)))) - (dfn (directory-file-name localdir)) (command (format "%s -H . %s -type f %s -print0" find-program (xref--find-ignores-arguments ignores "./") @@ -376,12 +381,14 @@ to find the list of ignores for each directory." (error "File listing failed: %s" (buffer-string)))) (goto-char pt) (while (search-forward "\0" nil t) - (push (buffer-substring-no-properties (1+ pt) (1- (point))) + (push (buffer-substring-no-properties (+ pt 2) (1- (point))) res) (setq pt (point))))) - (project--remote-file-names - (mapcar (lambda (s) (concat dfn s)) - (sort res #'string<))))) + (if project-files-relative-names + (sort res #'string<) + (project--remote-file-names + (mapcar (lambda (s) (concat localdir s)) + (sort res #'string<)))))) (defun project--remote-file-names (local-files) "Return LOCAL-FILES as if they were on the system of `default-directory'. @@ -689,7 +696,9 @@ See `project-vc-extra-root-markers' for the marker value format.") (mapcar (lambda (file) (unless (member file submodules) - (concat default-directory file))) + (if project-files-relative-names + file + (concat default-directory file)))) (split-string (apply #'vc-git--run-command-string nil "ls-files" args) "\0" t)))) @@ -716,7 +725,8 @@ See `project-vc-extra-root-markers' for the marker value format.") dir)) (args (list (concat "-mcard" (and include-untracked "u")) "--no-status" - "-0"))) + "-0")) + files) (when extra-ignores (setq args (nconc args (mapcan @@ -725,9 +735,12 @@ See `project-vc-extra-root-markers' for the marker value format.") extra-ignores)))) (with-temp-buffer (apply #'vc-hg-command t 0 "." "status" args) - (mapcar - (lambda (s) (concat default-directory s)) - (split-string (buffer-string) "\0" t))))))) + (setq files (split-string (buffer-string) "\0" t)) + (unless project-files-relative-names + (setq files (mapcar + (lambda (s) (concat default-directory s)) + files))) + files))))) (defun project--vc-merge-submodules-p (dir) (project--value-in-dir @@ -970,6 +983,7 @@ requires quoting, e.g. `\\[quoted-insert]'." (let* ((caller-dir default-directory) (pr (project-current t)) (default-directory (project-root pr)) + (project-files-relative-names t) (files (if (not current-prefix-arg) (project-files pr) @@ -1000,6 +1014,8 @@ requires quoting, e.g. `\\[quoted-insert]'." (require 'xref) (let* ((pr (project-current t)) (default-directory (project-root pr)) + ;; TODO: Make use of `project-files-relative-names' by + ;; searching each root separately (maybe in parallel, too). (files (project-files pr (cons (project-root pr) @@ -1054,7 +1070,8 @@ for VCS directories listed in `vc-directory-exclusion-list'." (interactive "P") (let* ((pr (project-current t)) (root (project-root pr)) - (dirs (list root))) + (dirs (list root)) + (project-files-relative-names t)) (project-find-file-in (or (thing-at-point 'filename) (and buffer-file-name (project--find-default-from buffer-file-name pr))) @@ -1130,7 +1147,12 @@ by the user at will." (if (> (length common-prefix) 0) (file-name-directory common-prefix)))) (cpd-length (length common-parent-directory)) - (prompt (if (zerop cpd-length) + (common-parent-directory (if (file-name-absolute-p (car all-files)) + common-parent-directory + (concat default-directory common-parent-directory))) + (prompt (if (and (zerop cpd-length) + all-files + (file-name-absolute-p (car all-files))) prompt (concat prompt (format " in %s" common-parent-directory)))) (included-cpd (when (member common-parent-directory all-files) @@ -1167,10 +1189,19 @@ by the user at will." (defun project--read-file-absolute (prompt all-files &optional predicate hist mb-default) - (project--completing-read-strict prompt - (project--file-completion-table all-files) - predicate - hist mb-default)) + (let* ((new-prompt (if (file-name-absolute-p (car all-files)) + prompt + (concat prompt " in " default-directory))) + ;; FIXME: Map relative names to absolute? + (ct (project--file-completion-table all-files)) + (file + (project--completing-read-strict new-prompt + ct + predicate + hist mb-default))) + (unless (file-name-absolute-p file) + (setq file (expand-file-name file))) + file)) (defun project--read-file-name ( project prompt all-files &optional predicate @@ -1215,6 +1246,7 @@ directories listed in `vc-directory-exclusion-list'." dirs) (project-files project dirs))) (completion-ignore-case read-file-name-completion-ignore-case) + (default-directory (project-root project)) (file (project--read-file-name project "Find file" all-files nil 'file-name-history diff --git a/lisp/progmodes/xref.el b/lisp/progmodes/xref.el index 755c3db04fd..29fc6cd560f 100644 --- a/lisp/progmodes/xref.el +++ b/lisp/progmodes/xref.el @@ -1922,7 +1922,8 @@ to control which program to use when looking for matches." (hits nil) ;; Support for remote files. The assumption is that, if the ;; first file is remote, they all are, and on the same host. - (dir (file-name-directory (car files))) + (dir (or (file-name-directory (car files)) + default-directory)) (remote-id (file-remote-p dir)) ;; The 'auto' default would be fine too, but ripgrep can't handle ;; the options we pass in that case. diff --git a/test/lisp/progmodes/project-tests.el b/test/lisp/progmodes/project-tests.el index 04cdf1dea29..84a5d55f136 100644 --- a/test/lisp/progmodes/project-tests.el +++ b/test/lisp/progmodes/project-tests.el @@ -163,4 +163,28 @@ When `project-ignores' includes a name matching project dir." (should-not (null project)) (should (string-match-p "/test/lisp/progmodes/project-resources/\\'" (project-root project))))) +(ert-deftest project-find-regexp () + "Check the happy path." + (skip-unless (executable-find find-program)) + (skip-unless (executable-find "xargs")) + (skip-unless (executable-find "grep")) + (let* ((directory (ert-resource-directory)) + (project-find-functions nil) + (project (cons 'transient directory))) + (add-hook 'project-find-functions (lambda (_dir) project)) + (should (eq (project-current) project)) + (let* ((matches nil) + (xref-search-program 'grep) + (xref-show-xrefs-function + (lambda (fetcher _display) + (setq matches (funcall fetcher))))) + (project-find-regexp "etc") + (should (equal (mapcar (lambda (item) + (file-name-base + (xref-location-group (xref-item-location item)))) + matches) + '(".dir-locals" "etc"))) + (should (equal (sort (mapcar #'xref-item-summary matches) #'string<) + '("((nil . ((project-vc-ignores . (\"etc\")))))" "etc")))))) + ;;; project-tests.el ends here commit e0993f5169ebf761d520b2e23630a3de7d13ccb3 Author: Po Lu Date: Sun May 5 09:49:09 2024 +0800 Don't permit C-x 8 RET &c in isearch.el to contaminate search string * lisp/isearch.el (isearch-char-by-name, isearch-emoji-by-name): Concatenate new character to the query string saved by with-isearch-suspended, not the current string, which might have been modified by recursive I-search sessions within completing-read. diff --git a/lisp/isearch.el b/lisp/isearch.el index a139a6fb84e..e8fb33ef6ea 100644 --- a/lisp/isearch.el +++ b/lisp/isearch.el @@ -2800,8 +2800,8 @@ With argument, add COUNT copies of the character." (let ((string (if (and (integerp count) (> count 1)) (make-string count char) (char-to-string char)))) - (setq isearch-new-string (concat isearch-string string) - isearch-new-message (concat isearch-message + (setq isearch-new-string (concat isearch-new-string string) + isearch-new-message (concat isearch-new-message (mapconcat 'isearch-text-char-description string "")))))))) @@ -2822,8 +2822,8 @@ The command accepts Unicode names like \"smiling face\" or (when (and (integerp count) (> count 1)) (setq emoji (apply 'concat (make-list count emoji)))) (when emoji - (setq isearch-new-string (concat isearch-string emoji) - isearch-new-message (concat isearch-message + (setq isearch-new-string (concat isearch-new-string emoji) + isearch-new-message (concat isearch-new-message (mapconcat 'isearch-text-char-description emoji ""))))))) commit 129312aa12c2f9c2cc44fcbdecb6b6a45b7588f4 Author: Po Lu Date: Sun May 5 09:38:27 2024 +0800 Fix cursor misalignment on truncated RTL rows * src/xdisp.c (set_cursor_from_row): Begin computing the position of the rightmost glyph from row->x, not 0. diff --git a/src/xdisp.c b/src/xdisp.c index 85802ec5083..8829c617f29 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -18182,7 +18182,7 @@ set_cursor_from_row (struct window *w, struct glyph_row *row, --glyph; /* By default, in reversed rows we put the cursor on the rightmost (first in the reading order) glyph. */ - for (x = 0, g = end + 1; g < glyph; g++) + for (x = row->x, g = end + 1; g < glyph; g++) x += g->pixel_width; while (end < glyph && NILP ((end + 1)->object) commit 51791fd1781729baeb38551e833861d9c24c302f Author: Po Lu Date: Sun May 5 09:03:57 2024 +0800 Fix inadvertent removal in EmacsWindow * java/org/gnu/emacs/EmacsWindow.java (onKeyUp): Send KeyPress events upon deferred KEYCODE_BACK. diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 16ff00070c4..9acdc9502cf 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -788,6 +788,10 @@ private static class Coordinate if ((event.getFlags () & KeyEvent.FLAG_CANCELED) != 0) return; + + /* Dispatch the key press event that was deferred till now. */ + EmacsNative.sendKeyPress (this.handle, event.getEventTime (), + state, keyCode, unicode_char); } EmacsNative.sendKeyRelease (this.handle, event.getEventTime (), commit 043bb36312039f60a464b918daa1dd214cd369f1 Author: Stefan Monnier Date: Sat May 4 17:14:46 2024 -0400 (eglot--track-changes-signal): Improve last fix (bug#70541) * lisp/progmodes/eglot.el (eglot--add-one-shot-hook): New function. (eglot--track-changes-signal): Use it. diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index 8ba47fe268d..47d45a100f2 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2679,6 +2679,15 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." ,(buffer-substring-no-properties beg end)) eglot--recent-changes)))))) +(defun eglot--add-one-shot-hook (hook function &optional append local) + "Like `add-hook' but calls FUNCTION only once." + (let* ((fname (make-symbol (format "eglot--%s-once" function))) + (fun (lambda (&rest args) + (remove-hook hook fname local) + (apply function args)))) + (fset fname fun) + (add-hook hook fname append local))) + (defun eglot--track-changes-signal (id &optional distance) (cond (distance @@ -2697,15 +2706,13 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (when eglot--managed-mode (if (and (fboundp 'track-changes-inconsistent-state-p) (track-changes-inconsistent-state-p)) - ;; Not a good time (e.g. in the middle of Quail - ;; thingy, bug#70541), let's reschedule. - ;; Ideally, we'd `run-with-idle-timer' to call - ;; ourselves again but it's kind of a pain to do that - ;; right (because we first have to wait for the - ;; current idle period to end), so we just do - ;; nothing and wait for the next buffer change to - ;; reschedule us. - nil + ;; Not a good time (e.g. in the middle of Quail thingy, + ;; bug#70541): reschedule for the next idle period. + (eglot--add-one-shot-hook + 'post-command-hook + (lambda () + (eglot--when-live-buffer buf + (eglot--track-changes-signal id)))) (run-hooks 'eglot--document-changed-hook) (setq eglot--change-idle-timer nil))))) (current-buffer)))) commit 672ca232db0a30e45b7f3f5e06b8fc6f12e23faa Author: Andreas Schwab Date: Fri Jul 28 11:53:03 2023 +0200 textsec: handle email address without domain part * lisp/international/textsec.el (textsec-email-address-suspicious-p): Handle missing domain part. (textsec-email-address-header-suspicious-p): Likewise. diff --git a/lisp/international/textsec.el b/lisp/international/textsec.el index 86429f15f7c..ce1f6c1d592 100644 --- a/lisp/international/textsec.el +++ b/lisp/international/textsec.el @@ -395,7 +395,7 @@ suspicious by, respectively, `textsec-local-address-suspicious-p' and `textsec-domain-suspicious-p'." (pcase-let ((`(,local ,domain) (split-string address "@"))) (or - (textsec-domain-suspicious-p domain) + (if domain (textsec-domain-suspicious-p domain)) (textsec-local-address-suspicious-p local)))) (defun textsec-email-address-header-suspicious-p (email) @@ -417,7 +417,7 @@ and `textsec-name-suspicious-p'." (mail-header-parse-address email t) (error (throw 'end "Email address can't be parsed."))))) (or - (textsec-email-address-suspicious-p address) + (and address (textsec-email-address-suspicious-p address)) (and name (textsec-name-suspicious-p name)))))) (defun textsec-url-suspicious-p (url) commit ea752667cd8a146e79e5646bbedc76be08c50141 Author: Stefan Monnier Date: Sat May 4 12:06:16 2024 -0400 (ruler-mode-ruler): Minor optimization * lisp/ruler-mode.el (ruler-mode-ruler): Don't compute `line-number-display-width` redundantly. Use `add-text-properties`. Remove redundant `local-map` property. diff --git a/lisp/ruler-mode.el b/lisp/ruler-mode.el index b9c32a40a07..c7e85b04cfd 100644 --- a/lisp/ruler-mode.el +++ b/lisp/ruler-mode.el @@ -350,7 +350,7 @@ nothing is dragged.") (defun ruler-mode-text-scaled-width (width) "Compute scaled text width according to current font scaling. -Convert a width of char units into a text-scaled char width units, +Convert a WIDTH of char units into a text-scaled char width units, for example `window-hscroll'." (/ (* width (frame-char-width)) (default-font-width))) @@ -528,7 +528,7 @@ START-EVENT is the mouse click event." (defvar ruler-mode-header-line-format-old nil "Hold previous value of `header-line-format'.") -(defvar ruler-mode-ruler-function 'ruler-mode-ruler +(defvar ruler-mode-ruler-function #'ruler-mode-ruler "Function to call to return ruler header line format. This variable is expected to be made buffer-local by modes.") @@ -563,7 +563,7 @@ format first." (ruler--save-header-line-format)) (setq ruler-mode enable))) (if ruler-mode - (add-hook 'post-command-hook 'force-mode-line-update nil t) + (add-hook 'post-command-hook #'force-mode-line-update nil t) ;; When `ruler-mode' is off restore previous header line format if ;; the current one is the ruler header line format. (when (eq header-line-format ruler-mode-header-line-format) @@ -571,7 +571,7 @@ format first." (when (local-variable-p 'ruler-mode-header-line-format-old) (setq header-line-format ruler-mode-header-line-format-old) (kill-local-variable 'ruler-mode-header-line-format-old))) - (remove-hook 'post-command-hook 'force-mode-line-update t))) + (remove-hook 'post-command-hook #'force-mode-line-update t))) ;; Add ruler-mode to the minor mode menu in the mode line (define-key mode-line-mode-menu [ruler-mode] @@ -625,7 +625,7 @@ mouse-2: unset goal column" (defsubst ruler-mode-space (width &rest props) "Return a single space string of WIDTH times the normal character width. Optional argument PROPS specifies other text properties to apply." - (apply 'propertize " " 'display (list 'space :width width) props)) + (apply #'propertize " " 'display (list 'space :width width) props)) (defun ruler-mode-ruler () "Compute and return a header line ruler." @@ -665,25 +665,25 @@ Optional argument PROPS specifies other text properties to apply." 'face 'ruler-mode-pad)) ;; Remember the scrollbar vertical type. (sbvt (car (window-current-scroll-bars))) - ;; Create an "clean" ruler. + ;; Create a "clean" ruler. (ruler ;; Make the part of header-line corresponding to the ;; line-number display be blank, not filled with ;; ruler-mode-basic-graduation-char. - (if display-line-numbers - (let ((lndw (round (line-number-display-width 'columns)))) - (vconcat (make-vector lndw ?\s) - (make-vector (- w lndw) - ruler-mode-basic-graduation-char))) + (if (> i 0) + (vconcat (make-vector i ?\s) + (make-vector (- w i) + ruler-mode-basic-graduation-char)) (make-vector w ruler-mode-basic-graduation-char))) (ruler-wide-props - `((face . ruler-mode-default) - (local-map . ruler-mode-map) - (help-echo . ,(cond (ruler-mode-show-tab-stops - ruler-mode-ruler-help-echo-when-tab-stops) - (goal-column - ruler-mode-ruler-help-echo-when-goal-column) - (ruler-mode-ruler-help-echo))))) + `( face ruler-mode-default + ;; This is redundant with the minor mode map. + ;;local-map ruler-mode-map + help-echo ,(cond (ruler-mode-show-tab-stops + ruler-mode-ruler-help-echo-when-tab-stops) + (goal-column + ruler-mode-ruler-help-echo-when-goal-column) + (ruler-mode-ruler-help-echo)))) (props nil) k c) ;; Setup the active area. @@ -695,7 +695,7 @@ Optional argument PROPS specifies other text properties to apply." (setq c (number-to-string (/ j 10)) m (length c) k i) - (push (list i (1+ i) 'face 'ruler-mode-column-number) props) + (push `(,i ,(1+ i) face ruler-mode-column-number) props) (while (and (> m 0) (>= k 0)) (aset ruler k (aref c (setq m (1- m)))) (setq k (1- k)))) @@ -707,43 +707,43 @@ Optional argument PROPS specifies other text properties to apply." ;; Show the `current-column' marker. ((= j (current-column)) (aset ruler i ruler-mode-current-column-char) - (push (list i (1+ i) 'face 'ruler-mode-current-column) props)) + (push `(,i ,(1+ i) face ruler-mode-current-column) props)) ;; Show the `goal-column' marker. ((and goal-column (= j goal-column)) (aset ruler i ruler-mode-goal-column-char) - (push (list i (1+ i) 'face 'ruler-mode-goal-column) props) - (push (list i (1+ i) 'mouse-face 'mode-line-highlight) props) - (push (list i (1+ i) 'help-echo ruler-mode-goal-column-help-echo) + (push `(,i ,(1+ i) + help-echo ,ruler-mode-goal-column-help-echo + face ruler-mode-goal-column + mouse-face mode-line-highlight) props)) ;; Show the `comment-column' marker. ((= j comment-column) (aset ruler i ruler-mode-comment-column-char) - (push (list i (1+ i) 'face 'ruler-mode-comment-column) - props) - (push (list i (1+ i) 'mouse-face 'mode-line-highlight) - props) - (push (list i (1+ i) 'help-echo ruler-mode-comment-column-help-echo) + (push `(,i ,(1+ i) + help-echo ,ruler-mode-comment-column-help-echo + face ruler-mode-comment-column + mouse-face mode-line-highlight) props)) ;; Show the `fill-column' marker. ((= j fill-column) (aset ruler i ruler-mode-fill-column-char) - (push (list i (1+ i) 'face 'ruler-mode-fill-column) props) - (push (list i (1+ i) 'mouse-face 'mode-line-highlight) props) - (push (list i (1+ i) 'help-echo ruler-mode-fill-column-help-echo) + (push `(,i ,(1+ i) + help-echo ,ruler-mode-fill-column-help-echo + face ruler-mode-fill-column + mouse-face mode-line-highlight) props)) ;; Show the `tab-stop-list' markers. ((and ruler-mode-show-tab-stops (= j (indent-next-tab-stop (1- j)))) (aset ruler i ruler-mode-tab-stop-char) - (push (list i (1+ i) 'face 'ruler-mode-tab-stop) props))) + (push `(,i ,(1+ i) face ruler-mode-tab-stop) props))) (setq i (1+ i) j (1+ j))) (let ((ruler-str (concat ruler)) (len (length ruler))) - (dolist (c ruler-wide-props) - (put-text-property 0 len (car c) (cdr c) ruler-str)) + (add-text-properties 0 len ruler-wide-props ruler-str) (dolist (p (nreverse props)) - (put-text-property (nth 0 p) (nth 1 p) (nth 2 p) (nth 3 p) ruler-str)) + (add-text-properties (nth 0 p) (nth 1 p) (nthcdr 2 p) ruler-str)) ;; Return the ruler propertized string. Using list here, ;; instead of concat visually separate the different areas. commit fd859fbea2e9d13e76db1c5295d9ddd1c5955d83 Author: Mattias EngdegÄrd Date: Sat May 4 14:09:23 2024 +0200 Allow `letrec` binding without init expression For example, (letrec (... (x) ...) ...) is now allowed. * lisp/subr.el (letrec): Allow omitted init expression. * test/lisp/subr-tests.el (subr--tests-letrec): Add test case. diff --git a/lisp/subr.el b/lisp/subr.el index 92d1e50ab2c..0ac71560c59 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -2281,7 +2281,9 @@ all symbols are bound before any of the VALUEFORMs are evalled." (let ((nbody (if (null binders) (macroexp-progn body) `(let ,(mapcar #'car binders) - ,@(mapcar (lambda (binder) `(setq ,@binder)) binders) + ,@(mapcan (lambda (binder) + (and (cdr binder) (list `(setq ,@binder)))) + binders) ,@body)))) (cond ;; All bindings are recursive. diff --git a/test/lisp/subr-tests.el b/test/lisp/subr-tests.el index 4e3f743cc93..119c124f3a5 100644 --- a/test/lisp/subr-tests.el +++ b/test/lisp/subr-tests.el @@ -744,7 +744,14 @@ See https://debbugs.gnu.org/cgi/bugreport.cgi?bug=19350." (+ subr-tests-var1 subr-tests-var2))) '(let* ((subr-tests-var1 1) (subr-tests-var2 subr-tests-var1)) - (+ subr-tests-var1 subr-tests-var2))))) + (+ subr-tests-var1 subr-tests-var2)))) + ;; Check that the init expression can be omitted, as in `let'/`let*'. + (should (equal (letrec ((a (lambda () (funcall c))) + (b) + (c (lambda () b))) + (setq b 'ok) + (funcall a)) + 'ok))) (defvar subr-tests--hook nil) commit 1e4cb12a8189b16e523bfcb4bd88a9de8f78a181 Merge: fdfadd6f467 b392169e541 Author: Eli Zaretskii Date: Sat May 4 08:10:54 2024 -0400 Merge from origin/emacs-29 b392169e541 ; * doc/lispref/commands.texi (Event Mod): Add motivation... e272fd3da92 ; Update description of 'mail-mode' commit fdfadd6f467063dc83032b4e0dbe4e7b96690670 Author: Eli Zaretskii Date: Sat May 4 14:51:05 2024 +0300 ; Fix last change * lisp/progmodes/python.el (python-shell-compilation-regexp-alist): Add :version. (Bug#70653) diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index e332bd8f61e..7f28f583543 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -2871,7 +2871,8 @@ virtualenv." "(" (group (1+ digit)) ")" (1+ (not (any "("))) "()") 1 2)) "`compilation-error-regexp-alist' for inferior Python." - :type '(alist regexp)) + :type '(alist regexp) + :version "30.1") (defcustom python-shell-dedicated nil "Whether to make Python shells dedicated by default. commit bfb31a06acadd5e819a3cb8dc6886122e53646f3 Author: shynur Date: Fri May 3 19:40:23 2024 +0800 Locate error source of ExceptionGroup in Python shell The Python shell recognizes the line containing a file path and a line number when an exception is raised up to the top-level, in order to locate the source of error. It's supposed to recognize the built-in ExceptionGroup as well. (Bug#70653) * lisp/progmodes/python.el (python-shell-compilation-regexp-alist): Take the single leading vertical line into account. * etc/NEWS: Announce this change. diff --git a/etc/NEWS b/etc/NEWS index 518477fbb72..9b264a23d5c 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1351,6 +1351,14 @@ interactive Python interpreter specified by 'python-interpreter'. It sends the python block delimited by 'python-nav-beginning-of-block' and 'python-nav-end-of-block' to the inferior Python process. +** Inferior Python mode + +--- +*** Default value of 'python-shell-compilation-regexp-alist' is changed. +Support for Python's ExceptionGroup has been added, so in the Python +shell, the line indicating the source of error in the error messages +from ExceptionGroup will be recognized as well. + ** Scheme mode Scheme mode now handles regular expression literal '#/regexp/' that is available in some Scheme implementations. diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 5ba185bc60c..e332bd8f61e 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -2860,7 +2860,7 @@ virtualenv." :type '(repeat symbol)) (defcustom python-shell-compilation-regexp-alist - `((,(rx line-start (1+ (any " \t")) "File \"" + `((,(rx line-start (1+ (any " \t")) (? ?| (1+ (any " \t"))) "File \"" (group (1+ (not (any "\"<")))) ; avoid `' &c "\", line " (group (1+ digit))) 1 2) commit fa0f65aa342e181e0e98f55cbf5d9a9be5ed3be6 Author: Eli Zaretskii Date: Sat May 4 13:12:21 2024 +0300 Fix implementation of the --terminal command-line switch It sounds like this has been broken ever since multi-tty was added to Emacs. * src/keyboard.c (dev_tty): New global variable. * src/keyboard.h: Declare 'dev_tty'. * src/emacs.c (main): Initialize 'dev_tty'. * src/term.c (Fcontrolling_tty_p, Fresume_tty, init_tty): * src/process.c (dissociate_controlling_tty): * src/keyboard.c (handle_interrupt_signal, handle_interrupt) (Fset_quit_char): Use 'dev_tty' instead of 'DEV_TTY'. (Bug#70519) diff --git a/src/emacs.c b/src/emacs.c index 7431cef274d..77e6c41e822 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -1653,6 +1653,7 @@ main (int argc, char **argv) inhibit_window_system = 0; /* Handle the -t switch, which specifies filename to use as terminal. */ + dev_tty = xstrdup (DEV_TTY); /* the default terminal */ while (!only_version) { char *term; @@ -1675,6 +1676,8 @@ main (int argc, char **argv) exit (EXIT_FAILURE); } fprintf (stderr, "Using %s\n", term); + xfree (dev_tty); + dev_tty = xstrdup (term); #ifdef HAVE_WINDOW_SYSTEM inhibit_window_system = true; /* -t => -nw */ #endif diff --git a/src/keyboard.c b/src/keyboard.c index a06c9116d24..69d29ededc0 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -98,6 +98,7 @@ char const DEV_TTY[] = "CONOUT$"; #else char const DEV_TTY[] = "/dev/tty"; #endif +char *dev_tty; /* set by init_keyboard */ /* Variables for blockinput.h: */ @@ -12003,7 +12004,7 @@ static void handle_interrupt_signal (int sig) { /* See if we have an active terminal on our controlling tty. */ - struct terminal *terminal = get_named_terminal (DEV_TTY); + struct terminal *terminal = get_named_terminal (dev_tty); if (!terminal) { /* If there are no frames there, let's pretend that we are a @@ -12072,7 +12073,7 @@ handle_interrupt (bool in_signal_handler) cancel_echoing (); /* XXX This code needs to be revised for multi-tty support. */ - if (!NILP (Vquit_flag) && get_named_terminal (DEV_TTY)) + if (!NILP (Vquit_flag) && get_named_terminal (dev_tty)) { if (! in_signal_handler) { @@ -12365,7 +12366,7 @@ process. See also `current-input-mode'. */) (Lisp_Object quit) { - struct terminal *t = get_named_terminal (DEV_TTY); + struct terminal *t = get_named_terminal (dev_tty); struct tty_display_info *tty; if (!t) diff --git a/src/keyboard.h b/src/keyboard.h index 2ce003fd444..42637ca1cf7 100644 --- a/src/keyboard.h +++ b/src/keyboard.h @@ -521,6 +521,9 @@ extern void mark_kboards (void); extern const char *const lispy_function_keys[]; #endif +/* Terminal device used by Emacs for terminal I/O. */ +extern char *dev_tty; +/* Initial value for dev_tty. */ extern char const DEV_TTY[]; INLINE_HEADER_END diff --git a/src/process.c b/src/process.c index 6b8b483cdf7..50d1968200d 100644 --- a/src/process.c +++ b/src/process.c @@ -2114,7 +2114,7 @@ dissociate_controlling_tty (void) child that has not execed. I wonder: would just ioctl (fd, TIOCNOTTY, 0) work here, for some fd that the caller already has? */ - int ttyfd = emacs_open (DEV_TTY, O_RDWR, 0); + int ttyfd = emacs_open (dev_tty, O_RDWR, 0); if (0 <= ttyfd) { ioctl (ttyfd, TIOCNOTTY, 0); diff --git a/src/term.c b/src/term.c index 6cb57592643..903444ef69f 100644 --- a/src/term.c +++ b/src/term.c @@ -2312,7 +2312,7 @@ TERMINAL is not on a tty device. */) { struct terminal *t = decode_tty_terminal (terminal); - return (t && !strcmp (t->display_info.tty->name, DEV_TTY) ? Qt : Qnil); + return (t && !strcmp (t->display_info.tty->name, dev_tty) ? Qt : Qnil); } DEFUN ("tty-no-underline", Ftty_no_underline, Stty_no_underline, 0, 1, 0, @@ -2467,7 +2467,7 @@ frame's terminal). */) open_errno); } - if (!O_IGNORE_CTTY && strcmp (t->display_info.tty->name, DEV_TTY) != 0) + if (!O_IGNORE_CTTY && strcmp (t->display_info.tty->name, dev_tty) != 0) dissociate_if_controlling_tty (fd); #endif /* MSDOS */ @@ -4075,7 +4075,7 @@ dissociate_if_controlling_tty (int fd) /* Create a termcap display on the tty device with the given name and type. - If NAME is NULL, then use the controlling tty, i.e., DEV_TTY. + If NAME is NULL, then use the controlling tty, i.e., dev_tty. Otherwise NAME should be a path to the tty device file, e.g. "/dev/pts/7". @@ -4114,9 +4114,9 @@ init_tty (const char *name, const char *terminal_type, bool must_succeed) "Unknown terminal type"); if (name == NULL) - name = DEV_TTY; + name = dev_tty; #ifndef DOS_NT - if (!strcmp (name, DEV_TTY)) + if (!strcmp (name, dev_tty)) ctty = 1; #endif commit 1121f17d7c4bc3b71edcd0799b894f50aa3a715e Author: Mattias EngdegÄrd Date: Sat May 4 10:08:19 2024 +0200 Only issue lexical cookie warning for elisp files * src/lread.c (string_suffix_p): New. (warn_missing_cookie): Suppress warning for files not ending in ".el", except ".emacs". * etc/NEWS: Update accordingly, and mention how the warning can be suppressed. diff --git a/etc/NEWS b/etc/NEWS index 718498cd836..518477fbb72 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2427,8 +2427,9 @@ Its warning name is 'docstrings-wide'. Emacs now emits a run-time warning if an Elisp source file being loaded lacks the '-*- lexical-binding: ... -*-' cookie on the first line. See the lexical-binding compiler warning described above for how to make -the warning go away. The user's init file (whose name is the value of -'user-init-file') and early-init file are exempt from this warning. +the warning go away by adding a declaration to the file. You can also +suppress the warning by adding an entry for the warning type +'lexical-warning' to 'warning-suppress-types'. --- ** New user option 'native-comp-async-warnings-errors-kind'. diff --git a/src/lread.c b/src/lread.c index a8ea52a888d..ba890cb673d 100644 --- a/src/lread.c +++ b/src/lread.c @@ -1342,20 +1342,33 @@ close_file_unwind_android_fd (void *ptr) #endif +static bool +string_suffix_p (const char *string, ptrdiff_t string_len, + const char *suffix, ptrdiff_t suffix_len) +{ + return string_len >= suffix_len && memcmp (string + string_len - suffix_len, + suffix, suffix_len) == 0; +} + static void warn_missing_cookie (Lisp_Object file) { - Lisp_Object msg; - - /* The user init file should not be subject to these warnings, as - Emacs doesn't insert cookies into generated init files. */ - if (!NILP (Fequal (file, Vuser_init_file))) + /* Only warn for files whose name end in .el, to suppress loading of + data-as-code. ".emacs" is an exception, since it does tend to contain + actual hand-written code. */ + if (!STRINGP (file)) + return; + const char *name = SSDATA (file); + ptrdiff_t nb = SBYTES (file); + if (!(string_suffix_p (name, nb, ".el", 3) + || (string_suffix_p (name, nb, ".emacs", 6) + && (nb == 6 || SREF (file, nb - 7) == '/')))) return; - msg = CALLN (Fformat, - build_string ("File %s lacks `lexical-binding'" - " directive on its first line"), - file); + Lisp_Object msg = CALLN (Fformat, + build_string ("File %s lacks `lexical-binding'" + " directive on its first line"), + file); Vdelayed_warnings_list = Fcons (list2 (Qlexical_binding, msg), Vdelayed_warnings_list); } commit 41dd78cd362a80f1becc006a37f163119b93df10 Author: Po Lu Date: Sat May 4 16:06:00 2024 +0800 Simplify handling of command-line arguments on Android * java/org/gnu/emacs/EmacsActivity.java (EXTRA_STARTUP_ARGUMENTS): New constant. (onCreate): Read a string array, not a string extra from the intent with this key. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) : Delete field. (onCreate): Provide file name as a command line argument when starting the Emacs service. * java/org/gnu/emacs/EmacsPreferencesActivity.java (startEmacsQ) (startEmacsDebugInit): In like manner, replace ad-hoc command-line argument extra with a proper array. * java/org/gnu/emacs/EmacsService.java (EmacsService): Rename extraStartupArgument to extraStartupArguments, and change its type to a string array. (onCreate): Adjust to match. * java/org/gnu/emacs/EmacsThread.java (EmacsThread) : Ditto. : Delete field. (run): Adjust correspondingly. diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 90be9a385cf..118c3375ad5 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -55,6 +55,9 @@ public class EmacsActivity extends Activity { public static final String TAG = "EmacsActivity"; + /* Key of intent value providing extra startup argument. */ + public static final String EXTRA_STARTUP_ARGUMENTS; + /* ID for URIs from a granted document tree. */ public static final int ACCEPT_DOCUMENT_TREE = 1; @@ -88,6 +91,7 @@ public class EmacsActivity extends Activity static { focusedActivities = new ArrayList (); + EXTRA_STARTUP_ARGUMENTS = "org.gnu.emacs.STARTUP_ARGUMENTS"; }; public static void @@ -242,8 +246,8 @@ children and RESETWHENCHILDLESS is set (implying it is a /* See if Emacs should be started with any extra arguments, such as `--quick'. */ intent = getIntent (); - EmacsService.extraStartupArgument - = intent.getStringExtra ("org.gnu.emacs.STARTUP_ARGUMENT"); + EmacsService.extraStartupArguments + = intent.getStringArrayExtra (EXTRA_STARTUP_ARGUMENTS); matchParent = FrameLayout.LayoutParams.MATCH_PARENT; params diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index cdc68aea2bf..28e1e261821 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -70,11 +70,6 @@ public final class EmacsOpenActivity extends Activity { private static final String TAG = "EmacsOpenActivity"; - /* The name of any file that should be opened as EmacsThread starts - Emacs. This is never cleared, even if EmacsOpenActivity is - started a second time, as EmacsThread only starts once. */ - public static String fileToOpen; - /* Any currently focused EmacsOpenActivity. Used to show pop ups while the activity is active and Emacs doesn't have permission to display over other programs. */ @@ -697,9 +692,10 @@ else if (scheme.equals ("org-protocol")) if (EmacsService.SERVICE == null) { - fileToOpen = fileName; intent = new Intent (EmacsOpenActivity.this, EmacsActivity.class); + intent.putExtra (EmacsActivity.EXTRA_STARTUP_ARGUMENTS, + new String [] { fileName, }); finish (); startActivity (intent); return; diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 766e2e11d46..a3edd6388b4 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -57,7 +57,8 @@ public class EmacsPreferencesActivity extends PreferenceActivity intent = new Intent (this, EmacsActivity.class); intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--quick"); + intent.putExtra (EmacsActivity.EXTRA_STARTUP_ARGUMENTS, + new String[] {"--quick", }); startActivity (intent); System.exit (0); } @@ -74,7 +75,8 @@ public class EmacsPreferencesActivity extends PreferenceActivity intent = new Intent (this, EmacsActivity.class); intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--debug-init"); + intent.putExtra (EmacsActivity.EXTRA_STARTUP_ARGUMENTS, + new String[] {"--debug-init", }); startActivity (intent); System.exit (0); } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index ced9f599960..5548748ddfa 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,6 +25,7 @@ import java.io.UnsupportedEncodingException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -102,9 +103,9 @@ public final class EmacsService extends Service /* The started Emacs service object. */ public static EmacsService SERVICE; - /* If non-NULL, an extra argument to pass to + /* If non-NULL, an array of extra arguments to pass to `android_emacs_init'. */ - public static String extraStartupArgument; + public static String[] extraStartupArguments; /* The thread running Emacs C code. */ private EmacsThread thread; @@ -289,7 +290,9 @@ invocation of app_process (through android-emacs) can Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir + ", libDir = " + libDir + ", and classPath = " + classPath - + "; fileToOpen = " + EmacsOpenActivity.fileToOpen + + "; args = " + (extraStartupArguments != null + ? Arrays.toString (extraStartupArguments) + : "(none)") + "; display density: " + pixelDensityX + " by " + pixelDensityY + " scaled to " + scaledDensity); @@ -306,9 +309,7 @@ invocation of app_process (through android-emacs) can classPath, EmacsService.this, Build.VERSION.SDK_INT); } - }, extraStartupArgument, - /* If any file needs to be opened, open it now. */ - EmacsOpenActivity.fileToOpen); + }, extraStartupArguments); thread.start (); } catch (IOException exception) diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 4adcb98b2f7..a90eb73b1ef 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -28,24 +28,20 @@ public final class EmacsThread extends Thread { private static final String TAG = "EmacsThread"; - /* Whether or not Emacs should be started with an additional - argument, and that additional argument if non-NULL. */ - private String extraStartupArgument; + /* Whether or not Emacs should be started with additional arguments, + and those additional arguments if non-NULL. */ + private final String[] extraStartupArguments; /* Runnable run to initialize Emacs. */ - private Runnable paramsClosure; - - /* Whether or not to open a file after starting Emacs. */ - private String fileToOpen; + private final Runnable paramsClosure; public EmacsThread (EmacsService service, Runnable paramsClosure, - String extraStartupArgument, String fileToOpen) + String[] extraStartupArguments) { super ("Emacs main thread"); - this.extraStartupArgument = extraStartupArgument; + this.extraStartupArguments = extraStartupArguments; this.paramsClosure = paramsClosure; - this.fileToOpen = fileToOpen; } @Override @@ -54,23 +50,15 @@ public final class EmacsThread extends Thread { String args[]; - if (fileToOpen == null) - { - if (extraStartupArgument == null) - args = new String[] { "libandroid-emacs.so", }; - else - args = new String[] { "libandroid-emacs.so", - extraStartupArgument, }; - } + if (extraStartupArguments == null) + args = new String[] { "libandroid-emacs.so", }; else { - if (extraStartupArgument == null) - args = new String[] { "libandroid-emacs.so", - fileToOpen, }; - else - args = new String[] { "libandroid-emacs.so", - extraStartupArgument, - fileToOpen, }; + /* Prepend "libandroid-emacs.so" to the list of arguments. */ + args = new String[extraStartupArguments.length + 1]; + args[0] = "libandroid-emacs.so"; + System.arraycopy (extraStartupArguments, 0, args, + 1, extraStartupArguments.length); } paramsClosure.run (); commit ecfbd0ff992adcb5b1b4b37884db8dbfda2fca6b Author: Po Lu Date: Sat May 4 14:54:12 2024 +0800 Delete redundant backquotes in android-win.el * lisp/term/android-win.el (android-encode-jni) (android-decode-jni): Replace redundant backquotes with ordinary quotes. diff --git a/lisp/term/android-win.el b/lisp/term/android-win.el index 6512ef81ff7..3538f41aa84 100644 --- a/lisp/term/android-win.el +++ b/lisp/term/android-win.el @@ -532,7 +532,7 @@ accessible to other programs." ;; Coding systems used by androidvfs.c. (define-ccl-program android-encode-jni - `(2 ((loop + '(2 ((loop (read r0) (if (r0 < #x1) ; 0x0 is encoded specially in JNI environments. ((write #xc0) @@ -564,7 +564,7 @@ accessible to other programs." "Encode characters from the input buffer for Java virtual machines.") (define-ccl-program android-decode-jni - `(1 ((loop + '(1 ((loop ((read-if (r0 >= #x80) ; More than a one-byte sequence? ((if (r0 < #xe0) ;; Two-byte sequence; potentially a NULL commit b392169e541a29178d7ae20f329d48b3d2bd78cf (refs/remotes/origin/emacs-29) Author: Eli Zaretskii Date: Thu May 2 12:20:09 2024 +0300 ; * doc/lispref/commands.texi (Event Mod): Add motivation (bug#70596). diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index dfb20cd807b..82bc6834142 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -3420,7 +3420,10 @@ character as far as keyboard translation is concerned, but it has the same usual meaning. @xref{Translation Keymaps}, for mechanisms that translate event sequences -at the level of @code{read-key-sequence}. +at the level of @code{read-key-sequence}. If you need to translate +input events that are not characters (i.e., @code{characterp} returns +@code{nil} for them), you must use the event translation mechanism +described there. @node Invoking the Input Method @subsection Invoking the Input Method commit e272fd3da9286d4b87e687b291bdf1fd5489af96 Author: Eli Zaretskii Date: Tue Apr 30 16:04:26 2024 +0300 ; Update description of 'mail-mode' * doc/emacs/sending.texi (Mail Methods): More accurate description of mail-mode's deficiencies. diff --git a/doc/emacs/sending.texi b/doc/emacs/sending.texi index 7d9f4917929..937ee568a3a 100644 --- a/doc/emacs/sending.texi +++ b/doc/emacs/sending.texi @@ -676,9 +676,11 @@ using this. In this chapter we have described the usual Emacs mode for editing and sending mail---Message mode. This is only one of several available modes. Prior to Emacs 23.2, the default mode was Mail mode, -which is similar to Message mode in many respects but lacks features -such as MIME support. Another available mode is MH-E -(@pxref{Top,,MH-E,mh-e, The Emacs Interface to MH}). +which is similar to Message mode in many respects but is less +feature-rich; for example, it supports only basic MIME: it allows you +to add attachments, but lacks more sophisticated MIME features. +Another available mode is MH-E (@pxref{Top,,MH-E,mh-e, The Emacs +Interface to MH}). @vindex mail-user-agent @findex define-mail-user-agent