commit 35aaa6b6aa9a2e7b42465603fb32355a009c510f (HEAD, refs/remotes/origin/master) Author: Christopher Genovese Date: Tue Dec 20 17:41:56 2016 +0900 ibuffer: New filters and commands Add several new filters and improve documentation. See discussion on: https://lists.gnu.org/archive/html/emacs-devel/2016-11/msg00399.html * lisp/ibuf-ext.el: Add paragraph to file commentary. (ibuffer-saved-filters, ibuffer-filtering-qualifiers) (ibuffer-filter-groups): Update doc string. (ibuffer-unary-operand): Add new function that transparently handles 'not' formats for compound filters. (ibuffer-included-in-filter-p): Handle 'not' fully; update doc string. (ibuffer-included-in-filter-p-1): Handle 'and' compound filters. (ibuffer-decompose-filter): Handle 'and' as well, and handle 'not' consistently with other uses. (ibuffer-and-filter): New defun analogous to 'ibuffer-or-filter'. (ibuffer--or-and-filter): New defun. (ibuffer-or-filter, ibuffer-and-filter): Use it. (ibuffer-format-qualifier): Handle 'and' filters as well. (ibuffer-filter-by-basename, ibuffer-filter-by-file-extension) (ibuffer-filter-by-directory, ibuffer-filter-by-starred-name) (ibuffer-filter-by-modified, ibuffer-filter-by-visiting-file): Add new pre-defined filters. (ibuffer-filter-chosen-by-completion): Add new interactive command for easily choosing a filter from the descriptions. * lisp/ibuffer.el (ibuffer-mode-map): Bind ibuffer-filter-by-basename, ibuffer-filter-by-file-extension, ibuffer-filter-by-starred-name, ibuffer-filter-by-modified, ibuffer-filter-by-visiting-file to '/b', '/.', '/*', '/i', '/v' respectively; bind 'ibuffer-or-filter', 'ibuffer-and-filter', 'ibuffer-pop-filter' ,'ibuffer-pop-filter-group' and 'ibuffer-filter-disable' to '/|', '/&', '/', '/S-' and '/ DEL' respectively. * test/lisp/ibuffer-tests.el (ibuffer-autoload): Add appropriate skip specification. Add menu entries for the new filters. (ibuffer-filter-inclusion-1, ibuffer-filter-inclusion-2 ibuffer-filter-inclusion-3, ibuffer-filter-inclusion-4 ibuffer-filter-inclusion-5, ibuffer-filter-inclusion-6 ibuffer-filter-inclusion-7, ibuffer-filter-inclusion-8 ibuffer-decompose-filter, ibuffer-and-filter ibuffer-or-filter): Add new tests; they are skipped unless ibuf-ext is loaded. ; * etc/NEWS: Add entries for new user-facing features. diff --git a/etc/NEWS b/etc/NEWS index e15ab79d50..ee74236a52 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -333,6 +333,27 @@ bound to 'Buffer-menu-unmark-all-buffers'. ** Ibuffer --- +*** New filter commands `ibuffer-filter-by-basename', +`ibuffer-filter-by-file-extension', `ibuffer-filter-by-directory', +`ibuffer-filter-by-starred-name', `ibuffer-filter-by-modified' +and `ibuffer-filter-by-visiting-file'; bound respectively +to '/b', '/.', '//', '/*', '/i' and '/v'. + +--- +*** Two new commands 'ibuffer-filter-chosen-by-completion' +and `ibuffer-and-filter', the second bound to '/&'. + +--- +*** The commands `ibuffer-pop-filter', `ibuffer-pop-filter-group', +`ibuffer-or-filter' and `ibuffer-filter-disable' have the alternative +bindings '/', '/S-', '/|' and '/DEL', respectively. + +--- +*** The data format specifying filters has been extended to allow +explicit logical 'and', and a more flexible form for logical 'not'. +See 'ibuffer-filtering-qualifiers' doc string for full details. + +--- *** A new command 'ibuffer-copy-buffername-as-kill'; bound to 'B'. diff --git a/lisp/ibuf-ext.el b/lisp/ibuf-ext.el index 9ce7b5a484..7ebfecd374 100644 --- a/lisp/ibuf-ext.el +++ b/lisp/ibuf-ext.el @@ -28,6 +28,13 @@ ;; These functions should be automatically loaded when called, but you ;; can explicitly (require 'ibuf-ext) in your ~/.emacs to have them ;; preloaded. +;; +;; For details on the structure of ibuffer filters and filter groups, +;; see the documentation for variables `ibuffer-filtering-qualifiers', +;; `ibuffer-filter-groups', and `ibuffer-saved-filters' in that order. +;; The variable `ibuffer-filtering-alist' contains names and +;; descriptions of the currently defined filters; also see the macro +;; `define-ibuffer-filter'. ;;; Code: @@ -139,19 +146,33 @@ Returns (OLD-FORMAT-DETECTED . UPDATED-SAVED-FILTERS-LIST)." (fixed (mapcar fix-filter filters))) (cons old-format-detected fixed)))) -(defcustom ibuffer-saved-filters '(("gnus" +(defcustom ibuffer-saved-filters '(("programming" + (or (derived-mode . prog-mode) + (mode . ess-mode) + (mode . compilation-mode))) + ("text document" + (and (derived-mode . text-mode) + (not (starred-name)))) + ("TeX" + (or (derived-mode . tex-mode) + (mode . latex-mode) + (mode . context-mode) + (mode . ams-tex-mode) + (mode . bibtex-mode))) + ("web" + (or (derived-mode . sgml-mode) + (derived-mode . css-mode) + (mode . javascript-mode) + (mode . js2-mode) + (mode . scss-mode) + (derived-mode . haml-mode) + (mode . sass-mode))) + ("gnus" (or (mode . message-mode) (mode . mail-mode) (mode . gnus-group-mode) (mode . gnus-summary-mode) - (mode . gnus-article-mode))) - ("programming" - (or (mode . emacs-lisp-mode) - (mode . cperl-mode) - (mode . c-mode) - (mode . java-mode) - (mode . idl-mode) - (mode . lisp-mode)))) + (mode . gnus-article-mode)))) "An alist mapping saved filter names to filter specifications. @@ -214,8 +235,48 @@ Alternative ways to save the repaired value: ")) (defvar ibuffer-filtering-qualifiers nil - "A list like (SYMBOL . QUALIFIER) which filters the current buffer list. -See also `ibuffer-filtering-alist'.") + "A list specifying the filters currently acting on the buffer list. + +If this list is nil, then no filters are currently in +effect. Otherwise, each element of this list specifies a single +filter, and all of the specified filters in the list are applied +successively to the buffer list. + +Each filter specification can be of two types: simple or compound. + +A simple filter specification has the form (SYMBOL . QUALIFIER), +where SYMBOL is a key in the alist `ibuffer-filtering-alist' that +determines the filter function to use and QUALIFIER is the data +passed to that function (along with the buffer being considered). + +A compound filter specification can have one of four forms: + +-- (not FILTER-SPEC) + + Represents the logical complement of FILTER-SPEC, which + is any single filter specification, simple or compound. + The form (not . FILTER-SPEC) is also accepted here. + +-- (and FILTER-SPECS...) + + Represents the logical-and of the filters defined by one or + more filter specifications FILTER-SPECS..., where each + specification can be simple or compound. Note that and is + implicitly applied to the filters in the top-level list. + +-- (or FILTER-SPECS...) + + Represents the logical-or of the filters defined by one or + more filter specifications FILTER-SPECS..., where each + specification can be simple or compound. + +-- (saved . \"NAME\") + + Represents the filter saved under the string NAME + in the alist `ibuffer-saved-filters'. It is an + error to name a filter that has not been saved. + +This variable is local to each ibuffer buffer.") ;; This is now frobbed by `define-ibuffer-filter'. (defvar ibuffer-filtering-alist nil @@ -247,10 +308,18 @@ to this variable." (defvar ibuffer-compiled-filter-formats nil) (defvar ibuffer-filter-groups nil - "A list like ((\"NAME\" ((SYMBOL . QUALIFIER) ...) ...) which groups buffers. -The SYMBOL should be one from `ibuffer-filtering-alist'. -The QUALIFIER should be the same as QUALIFIER in -`ibuffer-filtering-qualifiers'.") + "An alist giving this buffer's active filter groups, or nil if none. + +This alist maps filter group labels to filter specification +lists. Each element has the form (\"LABEL\" FILTER-SPECS...), +where FILTER-SPECS... represents one or more filter +specifications of the same form as allowed as elements of +`ibuffer-filtering-qualifiers'. + +Each filter group is displayed as a separate section in the +ibuffer list, headed by LABEL and displaying only the buffers +that pass through all the filters associated with NAME in this +list.") (defcustom ibuffer-show-empty-filter-groups t "If non-nil, then show the names of filter groups which are empty." @@ -260,20 +329,21 @@ The QUALIFIER should be the same as QUALIFIER in (defcustom ibuffer-saved-filter-groups nil "An alist of filtering groups to switch between. -This variable should look like ((\"STRING\" QUALIFIERS) - (\"STRING\" QUALIFIERS) ...), where -QUALIFIERS is a list of the same form as -`ibuffer-filtering-qualifiers'. +Each element is of the form (\"NAME\" . FILTER-GROUP-LIST), +where NAME is a unique but arbitrary name and FILTER-GROUP-LIST +is a list of filter groups with the same structure as +allowed for `ibuffer-filter-groups'. -See also the variables `ibuffer-filter-groups', -`ibuffer-filtering-qualifiers', `ibuffer-filtering-alist', and the -functions `ibuffer-switch-to-saved-filter-groups', -`ibuffer-save-filter-groups'." +See also the functions `ibuffer-save-filter-groups' and +`ibuffer-switch-to-saved-filter-groups' for saving and switching +between sets of filter groups, and the variable +`ibuffer-save-with-custom' that affects how this information is +saved." :type '(repeat sexp) :group 'ibuffer) (defvar ibuffer-hidden-filter-groups nil - "A list of filtering groups which are currently hidden.") + "The list of filter groups that are currently hidden.") (defvar ibuffer-filter-group-kill-ring nil) @@ -602,18 +672,38 @@ To evaluate a form without viewing the buffer, see `ibuffer-do-eval'." ;;;###autoload (defun ibuffer-included-in-filters-p (buf filters) + "Return non-nil if BUF passes all FILTERS. + +BUF is a lisp buffer object, and FILTERS is a list of filter +specifications with the same structure as +`ibuffer-filtering-qualifiers'." (not (memq nil ;; a filter will return nil if it failed - (mapcar - ;; filter should be like (TYPE . QUALIFIER), or - ;; (or (TYPE . QUALIFIER) (TYPE . QUALIFIER) ...) - #'(lambda (qual) - (ibuffer-included-in-filter-p buf qual)) - filters)))) + (mapcar #'(lambda (filter) + (ibuffer-included-in-filter-p buf filter)) + filters)))) + +(defun ibuffer-unary-operand (filter) + "Extracts operand from a unary compound FILTER specification. + +FILTER should be a cons cell of either form (f . d) or (f d), +where operand d is itself a cons cell, or nil. Returns d." + (let* ((tail (cdr filter)) + (maybe-q (car-safe tail))) + (if (consp maybe-q) maybe-q tail))) (defun ibuffer-included-in-filter-p (buf filter) + "Return non-nil if BUF pass FILTER. + +BUF is a lisp buffer object, and FILTER is a filter +specification, with the same structure as an element of the list +`ibuffer-filtering-qualifiers'." (if (eq (car filter) 'not) - (not (ibuffer-included-in-filter-p-1 buf (cdr filter))) + (let ((inner (ibuffer-unary-operand filter))) + ;; Allows (not (not ...)) etc, which may be overkill + (if (eq (car inner) 'not) + (ibuffer-included-in-filter-p buf (ibuffer-unary-operand inner)) + (not (ibuffer-included-in-filter-p-1 buf inner)))) (ibuffer-included-in-filter-p-1 buf filter))) (defun ibuffer-included-in-filter-p-1 (buf filter) @@ -621,9 +711,19 @@ To evaluate a form without viewing the buffer, see `ibuffer-do-eval'." (not (pcase (car filter) (`or + ;;; ATTN: Short-circuiting alternative with parallel structure w/`and + ;;(catch 'has-match + ;; (dolist (filter-spec (cdr filter) nil) + ;; (when (ibuffer-included-in-filter-p buf filter-spec) + ;; (throw 'has-match t)))) (memq t (mapcar #'(lambda (x) - (ibuffer-included-in-filter-p buf x)) - (cdr filter)))) + (ibuffer-included-in-filter-p buf x)) + (cdr filter)))) + (`and + (catch 'no-match + (dolist (filter-spec (cdr filter) t) + (unless (ibuffer-included-in-filter-p buf filter-spec) + (throw 'no-match nil))))) (`saved (let ((data (assoc (cdr filter) ibuffer-saved-filters))) (unless data @@ -916,17 +1016,17 @@ group definitions by setting `ibuffer-filter-groups' to nil." (when buf (ibuffer-jump-to-buffer (buffer-name buf))))) -(defun ibuffer-push-filter (qualifier) - "Add QUALIFIER to `ibuffer-filtering-qualifiers'." - (push qualifier ibuffer-filtering-qualifiers)) +(defun ibuffer-push-filter (filter-specification) + "Add FILTER-SPECIFICATION to `ibuffer-filtering-qualifiers'." + (push filter-specification ibuffer-filtering-qualifiers)) ;;;###autoload (defun ibuffer-decompose-filter () - "Separate the top compound filter (OR, NOT, or SAVED) in this buffer. + "Separate this buffer's top compound filter (AND, OR, NOT, or SAVED). This means that the topmost filter on the filtering stack, which must be a complex filter like (OR [name: foo] [mode: bar-mode]), will be -turned into two separate filters [name: foo] and [mode: bar-mode]." +turned into separate filters, like [name: foo] and [mode: bar-mode]." (interactive) (unless ibuffer-filtering-qualifiers (error "No filters in effect")) @@ -935,14 +1035,14 @@ turned into two separate filters [name: foo] and [mode: bar-mode]." (tail (cdr filters)) (value (pcase (caar filters) - (`or (nconc head tail)) + ((or `or 'and) (nconc head tail)) (`saved (let ((data (assoc head ibuffer-saved-filters))) (unless data (ibuffer-filter-disable) (error "Unknown saved filter %s" head)) (append (cdr data) tail))) - (`not (cons head tail)) + (`not (cons (ibuffer-unary-operand (car filters)) tail)) (_ (error "Filter type %s is not compound" (caar filters)))))) (setq ibuffer-filtering-qualifiers value)) @@ -971,31 +1071,36 @@ turned into two separate filters [name: foo] and [mode: bar-mode]." ibuffer-filtering-qualifiers)) (ibuffer-update nil t)) +(defun ibuffer--or-and-filter (op decompose) + (if decompose + (if (eq op (caar ibuffer-filtering-qualifiers)) + (ibuffer-decompose-filter) + (error "Top filter is not an %s" (upcase (symbol-name op)))) + (when (< (length ibuffer-filtering-qualifiers) 2) + (error "Need two filters to %s" (upcase (symbol-name op)))) + ;; If either filter is an op, eliminate unnecessary nesting. + (let ((first (pop ibuffer-filtering-qualifiers)) + (second (pop ibuffer-filtering-qualifiers))) + (push (nconc (if (eq op (car first)) first (list op first)) + (if (eq op (car second)) (cdr second) (list second))) + ibuffer-filtering-qualifiers))) + (ibuffer-update nil t)) + ;;;###autoload -(defun ibuffer-or-filter (&optional reverse) +(defun ibuffer-or-filter (&optional decompose) "Replace the top two filters in this buffer with their logical OR. -If optional argument REVERSE is non-nil, instead break the top OR +If optional argument DECOMPOSE is non-nil, instead break the top OR filter into parts." (interactive "P") - (if reverse - (progn - (when (or (null ibuffer-filtering-qualifiers) - (not (eq 'or (caar ibuffer-filtering-qualifiers)))) - (error "Top filter is not an OR")) - (let ((lim (pop ibuffer-filtering-qualifiers))) - (setq ibuffer-filtering-qualifiers - (nconc (cdr lim) ibuffer-filtering-qualifiers)))) - (when (< (length ibuffer-filtering-qualifiers) 2) - (error "Need two filters to OR")) - ;; If the second filter is an OR, just add to it. - (let ((first (pop ibuffer-filtering-qualifiers)) - (second (pop ibuffer-filtering-qualifiers))) - (if (eq 'or (car second)) - (push (nconc (list 'or first) (cdr second)) - ibuffer-filtering-qualifiers) - (push (list 'or first second) - ibuffer-filtering-qualifiers)))) - (ibuffer-update nil t)) + (ibuffer--or-and-filter 'or decompose)) + +;;;###autoload +(defun ibuffer-and-filter (&optional decompose) + "Replace the top two filters in this buffer with their logical AND. +If optional argument DECOMPOSE is non-nil, instead break the top AND +filter into parts." + (interactive "P") + (ibuffer--or-and-filter 'and decompose)) (defun ibuffer-maybe-save-stuff () (when ibuffer-save-with-custom @@ -1069,7 +1174,9 @@ Interactively, prompt for NAME, and use the current filters." (defun ibuffer-format-qualifier (qualifier) (if (eq (car-safe qualifier) 'not) - (concat " [NOT" (ibuffer-format-qualifier-1 (cdr qualifier)) "]") + (concat " [NOT" + (ibuffer-format-qualifier-1 (ibuffer-unary-operand qualifier)) + "]") (ibuffer-format-qualifier-1 qualifier))) (defun ibuffer-format-qualifier-1 (qualifier) @@ -1078,14 +1185,16 @@ Interactively, prompt for NAME, and use the current filters." (concat " [filter: " (cdr qualifier) "]")) (`or (concat " [OR" (mapconcat #'ibuffer-format-qualifier - (cdr qualifier) "") "]")) + (cdr qualifier) "") "]")) + (`and + (concat " [AND" (mapconcat #'ibuffer-format-qualifier + (cdr qualifier) "") "]")) (_ (let ((type (assq (car qualifier) ibuffer-filtering-alist))) (unless qualifier - (error "Ibuffer: bad qualifier %s" qualifier)) + (error "Ibuffer: bad qualifier %s" qualifier)) (concat " [" (cadr type) ": " (format "%s]" (cdr qualifier))))))) - (defun ibuffer-list-buffer-modes (&optional include-parents) "Create a completion table of buffer modes currently in use. If INCLUDE-PARENTS is non-nil then include parent modes." @@ -1103,7 +1212,7 @@ If INCLUDE-PARENTS is non-nil then include parent modes." ;;;###autoload (autoload 'ibuffer-filter-by-mode "ibuf-ext") (define-ibuffer-filter mode - "Toggle current view to buffers with major mode QUALIFIER." + "Limit current view to buffers with major mode QUALIFIER." (:description "major mode" :reader (let* ((buf (ibuffer-current-buffer)) @@ -1123,7 +1232,7 @@ If INCLUDE-PARENTS is non-nil then include parent modes." ;;;###autoload (autoload 'ibuffer-filter-by-used-mode "ibuf-ext") (define-ibuffer-filter used-mode - "Toggle current view to buffers with major mode QUALIFIER. + "Limit current view to buffers with major mode QUALIFIER. Called interactively, this function allows selection of modes currently used by buffers." (:description "major mode in use" @@ -1142,7 +1251,7 @@ currently used by buffers." ;;;###autoload (autoload 'ibuffer-filter-by-derived-mode "ibuf-ext") (define-ibuffer-filter derived-mode - "Toggle current view to buffers whose major mode inherits from QUALIFIER." + "Limit current view to buffers whose major mode inherits from QUALIFIER." (:description "derived mode" :reader (intern @@ -1153,22 +1262,73 @@ currently used by buffers." ;;;###autoload (autoload 'ibuffer-filter-by-name "ibuf-ext") (define-ibuffer-filter name - "Toggle current view to buffers with name matching QUALIFIER." + "Limit current view to buffers with name matching QUALIFIER." (:description "buffer name" :reader (read-from-minibuffer "Filter by name (regexp): ")) (string-match qualifier (buffer-name buf))) +;;;###autoload (autoload 'ibuffer-filter-by-starred-name "ibuf-ext") +(define-ibuffer-filter starred-name + "Limit current view to buffers with name beginning and ending +with *, along with an optional suffix of the form digits or +." + (:description "starred buffer name" + :reader nil) + (string-match "\\`\\*[^*]+\\*\\(?:<[[:digit:]]+>\\)?\\'" (buffer-name buf))) + ;;;###autoload (autoload 'ibuffer-filter-by-filename "ibuf-ext") (define-ibuffer-filter filename - "Toggle current view to buffers with filename matching QUALIFIER." - (:description "filename" - :reader (read-from-minibuffer "Filter by filename (regexp): ")) + "Limit current view to buffers with full file name matching QUALIFIER. + +For example, for a buffer associated with file '/a/b/c.d', this +matches against '/a/b/c.d'." + (:description "full file name" + :reader (read-from-minibuffer "Filter by full file name (regexp): ")) (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name)) (string-match qualifier it))) +;;;###autoload (autoload 'ibuffer-filter-by-basename "ibuf-ext") +(define-ibuffer-filter basename + "Limit current view to buffers with file basename matching QUALIFIER. + +For example, for a buffer associated with file '/a/b/c.d', this +matches against 'c.d'." + (:description "file basename" + :reader (read-from-minibuffer + "Filter by file name, without directory part (regex): ")) + (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name)) + (string-match qualifier (file-name-nondirectory it)))) + +;;;###autoload (autoload 'ibuffer-filter-by-file-extension "ibuf-ext") +(define-ibuffer-filter file-extension + "Limit current view to buffers with filename extension matching QUALIFIER. + +The separator character (typically `.') is not part of the +pattern. For example, for a buffer associated with file +'/a/b/c.d', this matches against 'd'." + (:description "filename extension" + :reader (read-from-minibuffer + "Filter by filename extension without separator (regex): ")) + (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name)) + (string-match qualifier (or (file-name-extension it) "")))) + +;;;###autoload (autoload 'ibuffer-filter-by-directory "ibuf-ext") +(define-ibuffer-filter directory + "Limit current view to buffers with directory matching QUALIFIER. + +For a buffer associated with file '/a/b/c.d', this matches +against '/a/b'. For a buffer not associated with a file, this +matches against the value of `default-directory' in that buffer." + (:description "directory name" + :reader (read-from-minibuffer "Filter by directory name (regex): ")) + (ibuffer-aif (with-current-buffer buf (ibuffer-buffer-file-name)) + (let ((dirname (file-name-directory it))) + (when dirname (string-match qualifier dirname))) + (when default-directory (string-match qualifier default-directory)))) + ;;;###autoload (autoload 'ibuffer-filter-by-size-gt "ibuf-ext") (define-ibuffer-filter size-gt - "Toggle current view to buffers with size greater than QUALIFIER." + "Limit current view to buffers with size greater than QUALIFIER." (:description "size greater than" :reader (string-to-number (read-from-minibuffer "Filter by size greater than: "))) @@ -1177,16 +1337,30 @@ currently used by buffers." ;;;###autoload (autoload 'ibuffer-filter-by-size-lt "ibuf-ext") (define-ibuffer-filter size-lt - "Toggle current view to buffers with size less than QUALIFIER." + "Limit current view to buffers with size less than QUALIFIER." (:description "size less than" :reader (string-to-number (read-from-minibuffer "Filter by size less than: "))) (< (with-current-buffer buf (buffer-size)) qualifier)) +;;;###autoload (autoload 'ibuffer-filter-by-modified "ibuf-ext") +(define-ibuffer-filter modified + "Limit current view to buffers that are marked as modified." + (:description "modified" + :reader nil) + (buffer-modified-p buf)) + +;;;###autoload (autoload 'ibuffer-filter-by-visiting-file "ibuf-ext") +(define-ibuffer-filter visiting-file + "Limit current view to buffers that are visiting a file." + (:description "visiting a file" + :reader nil) + (with-current-buffer buf (buffer-file-name))) + ;;;###autoload (autoload 'ibuffer-filter-by-content "ibuf-ext") (define-ibuffer-filter content - "Toggle current view to buffers whose contents match QUALIFIER." + "Limit current view to buffers whose contents match QUALIFIER." (:description "content" :reader (read-from-minibuffer "Filter by content (regexp): ")) (with-current-buffer buf @@ -1196,12 +1370,33 @@ currently used by buffers." ;;;###autoload (autoload 'ibuffer-filter-by-predicate "ibuf-ext") (define-ibuffer-filter predicate - "Toggle current view to buffers for which QUALIFIER returns non-nil." + "Limit current view to buffers for which QUALIFIER returns non-nil." (:description "predicate" :reader (read-minibuffer "Filter by predicate (form): ")) (with-current-buffer buf (eval qualifier))) +;;;###autoload (autoload 'ibuffer-filter-chosen-by-completion "ibuf-ext") +(defun ibuffer-filter-chosen-by-completion () + "Select and apply filter chosen by completion against available filters. +Indicates corresponding key sequences in echo area after filtering. + +The completion matches against the filter description text of +each filter in `ibuffer-filtering-alist'." + (interactive) + (let* ((filters (mapcar (lambda (x) (cons (cadr x) (car x))) + ibuffer-filtering-alist)) + (match (completing-read "Filter by: " filters nil t)) + (filter (cdr (assoc match filters))) + (command (intern (concat "ibuffer-filter-by-" (symbol-name filter))))) + (call-interactively command) + (message "%s can be run with key sequences: %s" + command + (mapconcat #'key-description + (where-is-internal command ibuffer-mode-map nil t) + "or ")))) + + ;;; Sorting ;;;###autoload diff --git a/lisp/ibuffer.el b/lisp/ibuffer.el index 94cee329d5..5a740845bd 100644 --- a/lisp/ibuffer.el +++ b/lisp/ibuffer.el @@ -518,26 +518,37 @@ directory, like `default-directory'." (define-key map (kbd "s f") 'ibuffer-do-sort-by-filename/process) (define-key map (kbd "s m") 'ibuffer-do-sort-by-major-mode) + (define-key map (kbd "/ RET") 'ibuffer-filter-by-mode) (define-key map (kbd "/ m") 'ibuffer-filter-by-used-mode) (define-key map (kbd "/ M") 'ibuffer-filter-by-derived-mode) (define-key map (kbd "/ n") 'ibuffer-filter-by-name) - (define-key map (kbd "/ c") 'ibuffer-filter-by-content) - (define-key map (kbd "/ e") 'ibuffer-filter-by-predicate) + (define-key map (kbd "/ *") 'ibuffer-filter-by-starred-name) (define-key map (kbd "/ f") 'ibuffer-filter-by-filename) - (define-key map (kbd "/ >") 'ibuffer-filter-by-size-gt) + (define-key map (kbd "/ b") 'ibuffer-filter-by-basename) + (define-key map (kbd "/ .") 'ibuffer-filter-by-file-extension) (define-key map (kbd "/ <") 'ibuffer-filter-by-size-lt) + (define-key map (kbd "/ >") 'ibuffer-filter-by-size-gt) + (define-key map (kbd "/ i") 'ibuffer-filter-by-modified) + (define-key map (kbd "/ v") 'ibuffer-filter-by-visiting-file) + (define-key map (kbd "/ c") 'ibuffer-filter-by-content) + (define-key map (kbd "/ e") 'ibuffer-filter-by-predicate) + (define-key map (kbd "/ r") 'ibuffer-switch-to-saved-filters) (define-key map (kbd "/ a") 'ibuffer-add-saved-filters) (define-key map (kbd "/ x") 'ibuffer-delete-saved-filters) (define-key map (kbd "/ d") 'ibuffer-decompose-filter) (define-key map (kbd "/ s") 'ibuffer-save-filters) (define-key map (kbd "/ p") 'ibuffer-pop-filter) + (define-key map (kbd "/ ") 'ibuffer-pop-filter) (define-key map (kbd "/ !") 'ibuffer-negate-filter) (define-key map (kbd "/ t") 'ibuffer-exchange-filters) (define-key map (kbd "/ TAB") 'ibuffer-exchange-filters) (define-key map (kbd "/ o") 'ibuffer-or-filter) + (define-key map (kbd "/ |") 'ibuffer-or-filter) + (define-key map (kbd "/ &") 'ibuffer-and-filter) (define-key map (kbd "/ g") 'ibuffer-filters-to-filter-group) (define-key map (kbd "/ P") 'ibuffer-pop-filter-group) + (define-key map (kbd "/ S-") 'ibuffer-pop-filter-group) (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group) (define-key map (kbd "/ /") 'ibuffer-filter-disable) @@ -657,13 +668,43 @@ directory, like `default-directory'." ibuffer-filter-by-derived-mode)) (define-key-after map [menu-bar view filter filter-by-name] '(menu-item "Add filter by buffer name..." ibuffer-filter-by-name)) + (define-key-after map [menu-bar view filter filter-by-starred-name] + '(menu-item "Add filter by starred buffer name..." + ibuffer-filter-by-starred-name + :help "List buffers whose names begin with a star")) (define-key-after map [menu-bar view filter filter-by-filename] - '(menu-item "Add filter by filename..." ibuffer-filter-by-filename)) + '(menu-item "Add filter by full filename..." ibuffer-filter-by-filename + :help + (concat "For a buffer associated with file '/a/b/c.d', " + "list buffer if a given pattern matches '/a/b/c.d'"))) + (define-key-after map [menu-bar view filter filter-by-basename] + '(menu-item "Add filter by file basename..." + ibuffer-filter-by-basename + :help (concat "For a buffer associated with file '/a/b/c.d', " + "list buffer if a given pattern matches 'c.d'"))) + (define-key-after map [menu-bar view filter filter-by-file-extension] + '(menu-item "Add filter by file name extension..." + ibuffer-filter-by-file-extension + :help (concat "For a buffer associated with file '/a/b/c.d', " + "list buffer if a given pattern matches 'd'"))) + (define-key-after map [menu-bar view filter filter-by-directory] + '(menu-item "Add filter by filename's directory..." + ibuffer-filter-by-directory + :help + (concat "For a buffer associated with file '/a/b/c.d', " + "list buffer if a given pattern matches '/a/b'"))) (define-key-after map [menu-bar view filter filter-by-size-lt] '(menu-item "Add filter by size less than..." ibuffer-filter-by-size-lt)) (define-key-after map [menu-bar view filter filter-by-size-gt] '(menu-item "Add filter by size greater than..." ibuffer-filter-by-size-gt)) + (define-key-after map [menu-bar view filter filter-by-modified] + '(menu-item "Add filter by modified buffer" ibuffer-filter-by-modified + :help "List buffers that are marked as modified")) + (define-key-after map [menu-bar view filter filter-by-visiting-file] + '(menu-item "Add filter by buffer visiting a file" + ibuffer-filter-by-visiting-file + :help "List buffers that are visiting files")) (define-key-after map [menu-bar view filter filter-by-content] '(menu-item "Add filter by content (regexp)..." ibuffer-filter-by-content)) @@ -673,6 +714,12 @@ directory, like `default-directory'." (define-key-after map [menu-bar view filter pop-filter] '(menu-item "Remove top filter" ibuffer-pop-filter :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers))) + (define-key-after map [menu-bar view filter and-filter] + '(menu-item "AND top two filters" ibuffer-and-filter + :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers + (cdr ibuffer-filtering-qualifiers)) + :help + "Create a new filter which is the logical AND of the top two filters")) (define-key-after map [menu-bar view filter or-filter] '(menu-item "OR top two filters" ibuffer-or-filter :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers diff --git a/test/lisp/ibuffer-tests.el b/test/lisp/ibuffer-tests.el index 92ed101e6b..40760abd96 100644 --- a/test/lisp/ibuffer-tests.el +++ b/test/lisp/ibuffer-tests.el @@ -24,7 +24,8 @@ (require 'ibuf-macs)) (ert-deftest ibuffer-autoload () - "Tests to see whether reftex-auc has been autoloaded" + "Tests to see whether ibuffer has been autoloaded" + (skip-unless (not (featurep 'ibuf-ext))) (should (fboundp 'ibuffer-mark-unsaved-buffers)) (should @@ -138,5 +139,669 @@ (should-not ibuffer-filtering-qualifiers)) (setq ibuffer-filtering-qualifiers filters)))) +;; Test Filter Inclusion +(let* (test-buffer-list ; accumulated buffers to clean up + ;; Utility functions without polluting the environment + (set-buffer-mode + (lambda (buffer mode) + "Set BUFFER's major mode to MODE, a mode function, or fundamental." + (with-current-buffer buffer + (funcall (or mode #'fundamental-mode))))) + (set-buffer-contents + (lambda (buffer size include-content) + "Add exactly SIZE bytes to BUFFER, including INCLUDE-CONTENT." + (when (or size include-content) + (let* ((unit "\n") + (chunk "ccccccccccccccccccccccccccccccc\n") + (chunk-size (length chunk)) + (size (if (and size include-content (stringp include-content)) + (- size (length include-content)) + size))) + (unless (or (null size) (> size 0)) + (error "size argument must be nil or positive")) + (with-current-buffer buffer + (when include-content + (insert include-content)) + (when size + (dotimes (_ (floor size chunk-size)) + (insert chunk)) + (dotimes (_ (mod size chunk-size)) + (insert unit))) + ;; prevent query on cleanup + (set-buffer-modified-p nil)))))) + (create-file-buffer + (lambda (prefix &rest args-plist) + "Create a file and buffer with designated properties. + PREFIX is a string giving the beginning of the name, and ARGS-PLIST + is a series of keyword-value pairs, with allowed keywords + :suffix STRING, :size NUMBER, :mode MODE-FUNC, :include-content STRING. + Returns the created buffer." + (let* ((suffix (plist-get args-plist :suffix)) + (size (plist-get args-plist :size)) + (include (plist-get args-plist :include-content)) + (mode (plist-get args-plist :mode)) + (file (make-temp-file prefix nil suffix)) + (buf (find-file-noselect file t))) + (push buf test-buffer-list) ; record for cleanup + (funcall set-buffer-mode buf mode) + (funcall set-buffer-contents buf size include) + buf))) + (create-non-file-buffer + (lambda (prefix &rest args-plist) + "Create a non-file and buffer with designated properties. + PREFIX is a string giving the beginning of the name, and ARGS-PLIST + is a series of keyword-value pairs, with allowed keywords + :size NUMBER, :mode MODE-FUNC, :include-content STRING. + Returns the created buffer." + (let* ((size (plist-get args-plist :size)) + (include (plist-get args-plist :include-content)) + (mode (plist-get args-plist :mode)) + (buf (generate-new-buffer prefix))) + (push buf test-buffer-list) ; record for cleanup + (funcall set-buffer-mode buf mode) + (funcall set-buffer-contents buf size include) + buf))) + (clean-up + (lambda () + "Restore all emacs state modified during the tests" + (while test-buffer-list ; created temporary buffers + (let ((buf (pop test-buffer-list))) + (with-current-buffer buf (bury-buffer)) ; ensure not selected + (kill-buffer buf)))))) + ;; Tests + (ert-deftest ibuffer-filter-inclusion-1 () + "Tests inclusion using basic filter combinators with a single buffer." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((buf + (funcall create-file-buffer "ibuf-test-1" :size 100 + :include-content "One ring to rule them all\n"))) + (should (ibuffer-included-in-filters-p buf '((size-gt . 99)))) + (should (ibuffer-included-in-filters-p buf '((size-lt . 101)))) + (should (ibuffer-included-in-filters-p + buf '((mode . fundamental-mode)))) + (should (ibuffer-included-in-filters-p + buf '((content . "ring to rule them all")))) + (should (ibuffer-included-in-filters-p + buf '((and (content . "ring to rule them all"))))) + (should (ibuffer-included-in-filters-p + buf '((and (and (content . "ring to rule them all")))))) + (should (ibuffer-included-in-filters-p + buf '((and (and (and (content . "ring to rule them all"))))))) + (should (ibuffer-included-in-filters-p + buf '((or (content . "ring to rule them all"))))) + (should (ibuffer-included-in-filters-p + buf '((not (not (content . "ring to rule them all")))))) + (should (ibuffer-included-in-filters-p + buf '((and (size-gt . 99) + (content . "ring to rule them all") + (mode . fundamental-mode) + (basename . "\\`ibuf-test-1"))))) + (should (ibuffer-included-in-filters-p + buf '((not (or (not (size-gt . 99)) + (not (content . "ring to rule them all")) + (not (mode . fundamental-mode)) + (not (basename . "\\`ibuf-test-1"))))))) + (should (ibuffer-included-in-filters-p + buf '((and (or (size-gt . 99) (size-lt . 10)) + (and (content . "ring.*all") + (content . "rule") + (content . "them all") + (content . "One")) + (not (mode . text-mode)) + (basename . "\\`ibuf-test-1")))))) + (funcall clean-up))) + + (ert-deftest ibuffer-filter-inclusion-2 () + "Tests inclusion of basic filters in combination on a single buffer." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((buf + (funcall create-file-buffer "ibuf-test-2" :size 200 + :mode #'text-mode + :include-content "and in the darkness find them\n"))) + (should (ibuffer-included-in-filters-p buf '((size-gt . 199)))) + (should (ibuffer-included-in-filters-p buf '((size-lt . 201)))) + (should (ibuffer-included-in-filters-p buf '((not size-gt . 200)))) + (should (ibuffer-included-in-filters-p buf '((not (size-gt . 200))))) + (should (ibuffer-included-in-filters-p + buf '((and (size-gt . 199) (size-lt . 201))))) + (should (ibuffer-included-in-filters-p + buf '((or (size-gt . 199) (size-gt . 201))))) + (should (ibuffer-included-in-filters-p + buf '((or (size-gt . 201) (size-gt . 199))))) + (should (ibuffer-included-in-filters-p + buf '((size-gt . 199) (mode . text-mode) + (content . "darkness find them")))) + (should (ibuffer-included-in-filters-p + buf '((and (size-gt . 199) (mode . text-mode) + (content . "darkness find them"))))) + (should (ibuffer-included-in-filters-p + buf '((not (or (not (size-gt . 199)) (not (mode . text-mode)) + (not (content . "darkness find them"))))))) + (should (ibuffer-included-in-filters-p + buf '((or (size-gt . 200) (content . "darkness find them") + (derived-mode . emacs-lisp-mode))))) + (should-not (ibuffer-included-in-filters-p + buf '((or (size-gt . 200) (content . "rule them all") + (derived-mode . emacs-lisp-mode)))))) + (funcall clean-up))) + + (ert-deftest ibuffer-filter-inclusion-3 () + "Tests inclusion with filename filters on specified buffers." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let* ((bufA + (funcall create-file-buffer "ibuf-test-3.a" :size 50 + :mode #'text-mode + :include-content "...but a multitude of drops?\n")) + (bufB + (funcall create-non-file-buffer "ibuf-test-3.b" :size 50 + :mode #'text-mode + :include-content "...but a multitude of drops?\n")) + (dirA (with-current-buffer bufA default-directory)) + (dirB (with-current-buffer bufB default-directory))) + (should (ibuffer-included-in-filters-p + bufA '((basename . "ibuf-test-3")))) + (should (ibuffer-included-in-filters-p + bufA '((basename . "test-3\\.a")))) + (should (ibuffer-included-in-filters-p + bufA '((file-extension . "a")))) + (should (ibuffer-included-in-filters-p + bufA (list (cons 'directory dirA)))) + (should-not (ibuffer-included-in-filters-p + bufB '((basename . "ibuf-test-3")))) + (should-not (ibuffer-included-in-filters-p + bufB '((file-extension . "b")))) + (should (ibuffer-included-in-filters-p + bufB (list (cons 'directory dirB)))) + (should (ibuffer-included-in-filters-p + bufA '((name . "ibuf-test-3")))) + (should (ibuffer-included-in-filters-p + bufB '((name . "ibuf-test-3"))))) + (funcall clean-up))) + + (ert-deftest ibuffer-filter-inclusion-4 () + "Tests inclusion with various filters on a single buffer." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((buf + (funcall create-file-buffer "ibuf-test-4" + :mode #'emacs-lisp-mode :suffix ".el" + :include-content "(message \"--%s--\" 'emacs-rocks)\n"))) + (should (ibuffer-included-in-filters-p + buf '((file-extension . "el")))) + (should (ibuffer-included-in-filters-p + buf '((derived-mode . prog-mode)))) + (should (ibuffer-included-in-filters-p + buf '((used-mode . emacs-lisp-mode)))) + (should (ibuffer-included-in-filters-p + buf '((mode . emacs-lisp-mode)))) + (with-current-buffer buf (set-buffer-modified-p t)) + (should (ibuffer-included-in-filters-p buf '((modified)))) + (with-current-buffer buf (set-buffer-modified-p nil)) + (should (ibuffer-included-in-filters-p buf '((not modified)))) + (should (ibuffer-included-in-filters-p + buf '((and (file-extension . "el") + (derived-mode . prog-mode) + (not modified))))) + (should (ibuffer-included-in-filters-p + buf '((or (file-extension . "tex") + (derived-mode . prog-mode) + (modified))))) + (should (ibuffer-included-in-filters-p + buf '((file-extension . "el") + (derived-mode . prog-mode) + (not modified))))) + (funcall clean-up))) + + (ert-deftest ibuffer-filter-inclusion-5 () + "Tests inclusion with various filters on a single buffer." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((buf + (funcall create-non-file-buffer "ibuf-test-5.el" + :mode #'emacs-lisp-mode + :include-content + "(message \"--%s--\" \"It really does!\")\n"))) + (should-not (ibuffer-included-in-filters-p + buf '((file-extension . "el")))) + (should (ibuffer-included-in-filters-p + buf '((size-gt . 18)))) + (should (ibuffer-included-in-filters-p + buf '((predicate . (lambda () + (> (- (point-max) (point-min)) 18)))))) + (should (ibuffer-included-in-filters-p + buf '((and (mode . emacs-lisp-mode) + (or (starred-name) + (size-gt . 18)) + (and (not (size-gt . 100)) + (content . "[Ii]t *really does!") + (or (name . "test-5") + (not (filename . "test-5"))))))))) + (funcall clean-up))) + + (ert-deftest ibuffer-filter-inclusion-6 () + "Tests inclusion using saved filters and DeMorgan's laws." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((buf + (funcall create-non-file-buffer "*ibuf-test-6*" :size 65 + :mode #'text-mode)) + (buf2 + (funcall create-file-buffer "ibuf-test-6a" :suffix ".html" + :mode #'html-mode + :include-content + "

Hello, World!

"))) + (should (ibuffer-included-in-filters-p buf '((starred-name)))) + (should-not (ibuffer-included-in-filters-p + buf '((saved . "text document")))) + (should (ibuffer-included-in-filters-p buf2 '((saved . "web")))) + (should (ibuffer-included-in-filters-p + buf2 '((not (and (not (derived-mode . sgml-mode)) + (not (derived-mode . css-mode)) + (not (mode . javascript-mode)) + (not (mode . js2-mode)) + (not (mode . scss-mode)) + (not (derived-mode . haml-mode)) + (not (mode . sass-mode))))))) + (should (ibuffer-included-in-filters-p + buf '((and (starred-name) + (or (size-gt . 50) (filename . "foo")))))) + (should (ibuffer-included-in-filters-p + buf '((not (or (not starred-name) + (and (size-lt . 51) + (not (filename . "foo"))))))))) + (funcall clean-up))) + + (ert-deftest ibuffer-filter-inclusion-7 () + "Tests inclusion with various filters on a single buffer." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((buf + (funcall create-non-file-buffer "ibuf-test-7" + :mode #'artist-mode))) + (should (ibuffer-included-in-filters-p + buf '((not (starred-name))))) + (should (ibuffer-included-in-filters-p + buf '((not starred-name)))) + (should (ibuffer-included-in-filters-p + buf '((not (not (not starred-name)))))) + (should (ibuffer-included-in-filters-p + buf '((not (modified))))) + (should (ibuffer-included-in-filters-p + buf '((not modified)))) + (should (ibuffer-included-in-filters-p + buf '((not (not (not modified))))))) + (funcall clean-up))) + + (ert-deftest ibuffer-filter-inclusion-8 () + "Tests inclusion with various filters." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((bufA + (funcall create-non-file-buffer "ibuf-test-8a" + :mode #'artist-mode)) + (bufB (funcall create-non-file-buffer "*ibuf-test-8b*" :size 32)) + (bufC (funcall create-file-buffer "ibuf-test8c" :suffix "*" + :size 64)) + (bufD (funcall create-file-buffer "*ibuf-test8d" :size 128)) + (bufE (funcall create-file-buffer "*ibuf-test8e" :suffix "*<2>" + :size 16)) + (bufF (and (funcall create-non-file-buffer "*ibuf-test8f*") + (funcall create-non-file-buffer "*ibuf-test8f*" + :size 8)))) + (with-current-buffer bufA (set-buffer-modified-p t)) + (should (ibuffer-included-in-filters-p + bufA '((and (not starred-name) + (modified) + (name . "test-8") + (not (size-gt . 100)) + (mode . picture-mode))))) + (with-current-buffer bufA (set-buffer-modified-p nil)) + (should-not (ibuffer-included-in-filters-p + bufA '((or (starred-name) (visiting-file) (modified))))) + (should (ibuffer-included-in-filters-p + bufB '((and (starred-name) + (name . "test.*8b") + (size-gt . 31) + (not visiting-file))))) + (should (ibuffer-included-in-filters-p + bufC '((and (not (starred-name)) + (visiting-file) + (name . "8c[^*]*\\*") + (size-lt . 65))))) + (should (ibuffer-included-in-filters-p + bufD '((and (not (starred-name)) + (visiting-file) + (name . "\\`\\*.*test8d") + (size-lt . 129) + (size-gt . 127))))) + (should (ibuffer-included-in-filters-p + bufE '((and (starred-name) + (visiting-file) + (name . "8e.*?\\*<[[:digit:]]+>") + (size-gt . 10))))) + (should (ibuffer-included-in-filters-p + bufF '((and (starred-name) + (not (visiting-file)) + (name . "8f\\*<[[:digit:]]>") + (size-lt . 10)))))) + (funcall clean-up)))) + +;; Test Filter Combination and Decomposition +(let* (ibuffer-to-kill ; if non-nil, kill this buffer at cleanup + (ibuffer-already 'check) ; existing ibuffer buffer to use but not kill + ;; Utility functions without polluting the environment + (get-test-ibuffer + (lambda () + "Returns a test ibuffer-mode buffer, creating one if necessary. + If a new buffer is created, it is named \"*Test-Ibuffer*\" and is + saved to `ibuffer-to-kill' for later cleanup." + (when (eq ibuffer-already 'check) + (setq ibuffer-already + (catch 'found-buf + (dolist (buf (buffer-list) nil) + (when (with-current-buffer buf + (derived-mode-p 'ibuffer-mode)) + (throw 'found-buf buf)))))) + (or ibuffer-already + ibuffer-to-kill + (let ((test-ibuf-name "*Test-Ibuffer*")) + (ibuffer nil test-ibuf-name nil t) + (setq ibuffer-to-kill (get-buffer test-ibuf-name)))))) + (clean-up + (lambda () + "Restore all emacs state modified during the tests" + (when ibuffer-to-kill ; created ibuffer + (with-current-buffer ibuffer-to-kill + (set-buffer-modified-p nil) + (bury-buffer)) + (kill-buffer ibuffer-to-kill) + (setq ibuffer-to-kill nil)) + (when (and ibuffer-already (not (eq ibuffer-already 'check))) + ;; restore existing ibuffer state + (ibuffer-update nil t))))) + ;; Tests + (ert-deftest ibuffer-decompose-filter () + "Tests `ibuffer-decompose-filter' for and, or, not, and saved." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((ibuf (funcall get-test-ibuffer))) + (with-current-buffer ibuf + (let ((ibuffer-filtering-qualifiers nil) + (ibuffer-filter-groups nil) + (filters '((size-gt . 100) (not (starred-name)) + (name . "foo")))) + (progn + (push (cons 'or filters) ibuffer-filtering-qualifiers) + (ibuffer-decompose-filter) + (should (equal filters ibuffer-filtering-qualifiers)) + (setq ibuffer-filtering-qualifiers nil)) + (progn + (push (cons 'and filters) ibuffer-filtering-qualifiers) + (ibuffer-decompose-filter) + (should (equal filters ibuffer-filtering-qualifiers)) + (setq ibuffer-filtering-qualifiers nil)) + (progn + (push (list 'not (car filters)) ibuffer-filtering-qualifiers) + (ibuffer-decompose-filter) + (should (equal (list (car filters)) + ibuffer-filtering-qualifiers)) + (setq ibuffer-filtering-qualifiers nil)) + (progn + (push (cons 'not (car filters)) ibuffer-filtering-qualifiers) + (ibuffer-decompose-filter) + (should (equal (list (car filters)) + ibuffer-filtering-qualifiers)) + (setq ibuffer-filtering-qualifiers nil)) + (let ((gnus (assoc "gnus" ibuffer-saved-filters))) + (push '(saved . "gnus") ibuffer-filtering-qualifiers) + (ibuffer-decompose-filter) + (should (equal (cdr gnus) ibuffer-filtering-qualifiers)) + (ibuffer-decompose-filter) + (should (equal (cdr (cadr gnus)) ibuffer-filtering-qualifiers)) + (setq ibuffer-filtering-qualifiers nil)) + (when (not (assoc "__unknown__" ibuffer-saved-filters)) + (push '(saved . "__uknown__") ibuffer-filtering-qualifiers) + (should-error (ibuffer-decompose-filter) :type 'error) + (setq ibuffer-filtering-qualifiers nil)) + (progn + (push (car filters) ibuffer-filtering-qualifiers) + (should-error (ibuffer-decompose-filter) :type 'error) + (setq ibuffer-filtering-qualifiers nil))))) + (funcall clean-up))) + + (ert-deftest ibuffer-and-filter () + "Tests `ibuffer-and-filter' in an Ibuffer buffer." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((ibuf (funcall get-test-ibuffer))) + (with-current-buffer ibuf + (let ((ibuffer-filtering-qualifiers nil) + (ibuffer-filter-groups nil) + (filters [(size-gt . 100) (not (starred-name)) + (filename . "A") (mode . text-mode)])) + (should-error (ibuffer-and-filter) :type 'error) + (progn + (push (aref filters 1) ibuffer-filtering-qualifiers) + (should-error (ibuffer-and-filter) :type 'error)) + (should (progn + (push (aref filters 0) ibuffer-filtering-qualifiers) + (ibuffer-and-filter) + (and (equal (list 'and (aref filters 0) (aref filters 1)) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers))))) + (should (progn + (ibuffer-and-filter 'decompose) + (and (equal (aref filters 0) + (pop ibuffer-filtering-qualifiers)) + (equal (aref filters 1) + (pop ibuffer-filtering-qualifiers)) + (null ibuffer-filtering-qualifiers)))) + (should (progn + (push (list 'and (aref filters 2) (aref filters 3)) + ibuffer-filtering-qualifiers) + (push (list 'and (aref filters 0) (aref filters 1)) + ibuffer-filtering-qualifiers) + (ibuffer-and-filter) + (and (equal (list 'and (aref filters 0) (aref filters 1) + (aref filters 2) (aref filters 3)) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers))))) + (pop ibuffer-filtering-qualifiers) + (should (progn + (push (list 'or (aref filters 2) (aref filters 3)) + ibuffer-filtering-qualifiers) + (push (list 'and (aref filters 0) (aref filters 1)) + ibuffer-filtering-qualifiers) + (ibuffer-and-filter) + (and (equal (list 'and (aref filters 0) (aref filters 1) + (list 'or (aref filters 2) + (aref filters 3))) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers))))) + (pop ibuffer-filtering-qualifiers) + (should (progn + (push (list 'and (aref filters 2) (aref filters 3)) + ibuffer-filtering-qualifiers) + (push (list 'or (aref filters 0) (aref filters 1)) + ibuffer-filtering-qualifiers) + (ibuffer-and-filter) + (and (equal (list 'and (list 'or (aref filters 0) + (aref filters 1)) + (aref filters 2) (aref filters 3)) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers))))) + (pop ibuffer-filtering-qualifiers) + (should (progn + (push (list 'or (aref filters 2) (aref filters 3)) + ibuffer-filtering-qualifiers) + (push (list 'or (aref filters 0) (aref filters 1)) + ibuffer-filtering-qualifiers) + (ibuffer-and-filter) + (and (equal (list 'and + (list 'or (aref filters 0) + (aref filters 1)) + (list 'or (aref filters 2) + (aref filters 3))) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers)))))))) + (funcall clean-up))) + + (ert-deftest ibuffer-or-filter () + "Tests `ibuffer-or-filter' in an Ibuffer buffer." + (skip-unless (featurep 'ibuf-ext)) + (unwind-protect + (let ((ibuf (funcall get-test-ibuffer))) + (with-current-buffer ibuf + (let ((ibuffer-filtering-qualifiers nil) + (ibuffer-filter-groups nil) + (filters [(size-gt . 100) (not (starred-name)) + (filename . "A") (mode . text-mode)])) + (should-error (ibuffer-or-filter) :type 'error) + (progn + (push (aref filters 1) ibuffer-filtering-qualifiers) + (should-error (ibuffer-or-filter) :type 'error)) + (should (progn + (push (aref filters 0) ibuffer-filtering-qualifiers) + (ibuffer-or-filter) + (and (equal (list 'or (aref filters 0) (aref filters 1)) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers))))) + (should (progn + (ibuffer-or-filter 'decompose) + (and (equal (aref filters 0) + (pop ibuffer-filtering-qualifiers)) + (equal (aref filters 1) + (pop ibuffer-filtering-qualifiers)) + (null ibuffer-filtering-qualifiers)))) + (should (progn + (push (list 'or (aref filters 2) (aref filters 3)) + ibuffer-filtering-qualifiers) + (push (list 'or (aref filters 0) (aref filters 1)) + ibuffer-filtering-qualifiers) + (ibuffer-or-filter) + (and (equal (list 'or (aref filters 0) (aref filters 1) + (aref filters 2) (aref filters 3)) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers))))) + (pop ibuffer-filtering-qualifiers) + (should (progn + (push (list 'and (aref filters 2) (aref filters 3)) + ibuffer-filtering-qualifiers) + (push (list 'or (aref filters 0) (aref filters 1)) + ibuffer-filtering-qualifiers) + (ibuffer-or-filter) + (and (equal (list 'or (aref filters 0) (aref filters 1) + (list 'and (aref filters 2) + (aref filters 3))) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers))))) + (pop ibuffer-filtering-qualifiers) + (should (progn + (push (list 'or (aref filters 2) (aref filters 3)) + ibuffer-filtering-qualifiers) + (push (list 'and (aref filters 0) (aref filters 1)) + ibuffer-filtering-qualifiers) + (ibuffer-or-filter) + (and (equal (list 'or (list 'and (aref filters 0) + (aref filters 1)) + (aref filters 2) (aref filters 3)) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers))))) + (pop ibuffer-filtering-qualifiers) + (should (progn + (push (list 'and (aref filters 2) (aref filters 3)) + ibuffer-filtering-qualifiers) + (push (list 'and (aref filters 0) (aref filters 1)) + ibuffer-filtering-qualifiers) + (ibuffer-or-filter) + (and (equal (list 'or + (list 'and (aref filters 0) + (aref filters 1)) + (list 'and (aref filters 2) + (aref filters 3))) + (car ibuffer-filtering-qualifiers)) + (null (cdr ibuffer-filtering-qualifiers)))))))) + (funcall clean-up)))) + +(ert-deftest ibuffer-format-qualifier () + "Tests string recommendation of filter from `ibuffer-format-qualifier'." + (skip-unless (featurep 'ibuf-ext)) + (let ((test1 '(mode . org-mode)) + (test2 '(size-lt . 100)) + (test3 '(derived-mode . prog-mode)) + (test4 '(or (size-gt . 10000) + (and (not (starred-name)) + (directory . "\\")))) + (test5 '(or (filename . "scratch") + (filename . "bonz") + (filename . "temp"))) + (test6 '(or (mode . emacs-lisp-mode) (file-extension . "elc?") + (and (starred-name) (name . "elisp")) + (mode . lisp-interaction-mode))) + (description (lambda (q) + (cadr (assq q ibuffer-filtering-alist)))) + (tag (lambda (&rest args ) + (concat " [" (apply #'concat args) "]")))) + (should (equal (ibuffer-format-qualifier test1) + (funcall tag (funcall description 'mode) + ": " "org-mode"))) + (should (equal (ibuffer-format-qualifier test2) + (funcall tag (funcall description 'size-lt) + ": " "100"))) + (should (equal (ibuffer-format-qualifier test3) + (funcall tag (funcall description 'derived-mode) + ": " "prog-mode"))) + (should (equal (ibuffer-format-qualifier test4) + (funcall tag "OR" + (funcall tag (funcall description 'size-gt) + ": " (format "%s" 10000)) + (funcall tag "AND" + (funcall tag "NOT" + (funcall tag + (funcall description + 'starred-name) + ": " "nil")) + (funcall tag + (funcall description 'directory) + ": " "\\"))))) + (should (equal (ibuffer-format-qualifier test5) + (funcall tag "OR" + (funcall tag (funcall description 'filename) + ": " "scratch") + (funcall tag (funcall description 'filename) + ": " "bonz") + (funcall tag (funcall description 'filename) + ": " "temp")))) + (should (equal (ibuffer-format-qualifier test6) + (funcall tag "OR" + (funcall tag (funcall description 'mode) + ": " "emacs-lisp-mode") + (funcall tag (funcall description 'file-extension) + ": " "elc?") + (funcall tag "AND" + (funcall tag + (funcall description 'starred-name) + ": " "nil") + (funcall tag + (funcall description 'name) + ": " "elisp")) + (funcall tag (funcall description 'mode) + ": " "lisp-interaction-mode")))))) + +(ert-deftest ibuffer-unary-operand () + "Tests `ibuffer-unary-operand': (not cell) or (not . cell) -> cell." + (skip-unless (featurep 'ibuf-ext)) + (should (equal (ibuffer-unary-operand '(not . (mode "foo"))) + '(mode "foo"))) + (should (equal (ibuffer-unary-operand '(not (mode "foo"))) + '(mode "foo"))) + (should (equal (ibuffer-unary-operand '(not "cdr")) + '("cdr"))) + (should (equal (ibuffer-unary-operand '(not)) nil)) + (should (equal (ibuffer-unary-operand '(not . a)) 'a))) + (provide 'ibuffer-tests) ;; ibuffer-tests.el ends here commit f8072cd5c16f855505f6a0ce6a6b30309735705d Author: Mark Oteiza Date: Mon Dec 19 20:37:46 2016 -0500 Update NEWS * etc/NEWS (Image-Dired): New section. diff --git a/etc/NEWS b/etc/NEWS index e69ba87484..e15ab79d50 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -462,6 +462,39 @@ for 'image-mode' that complement 'image-scroll-up' and 'image-scroll-down': they have the same prefix arg behavior and stop at image boundaries. +** Image-Dired + +*** Now provides a minor mode 'image-dired-minor-mode' which replaces +the function 'image-dired-setup-dired-keybindings'. + +*** Thumbnail generation is now asynchronous +The number of concurrent processes is limited by the variable +'image-dired-thumb-job-limit'. + +*** 'image-dired-thumbnail-storage' has a new option 'standard-large' +for generating 256x256 thumbnails according to the Thumbnail Managing +Standard. + +*** Inherits movement keys from 'image-mode' for viewing full images. +This includes the usual char, line, and page movement commands. + +*** All the -options types have been changed to argument lists +instead of shell command strings. This change affects +'image-dired-cmd-create-thumbnail-options', +'image-dired-cmd-create-temp-image-options', +'image-dired-cmd-rotate-thumbnail-options', +'image-dired-cmd-rotate-original-options', +'image-dired-cmd-write-exif-data-options', +'image-dired-cmd-read-exif-data-options', and introduces +'image-dired-cmd-pngnq-options', 'image-dired-cmd-pngcrush-options', +'image-dired-cmd-create-standard-thumbnail-options' + +*** Recognizes more tools by default, including pngnq-s9, optipng, and gm + +*** 'find-file' and related commands now work on thumbnails and +displayed images, providing a default argument of the original file name +via an addition to 'file-name-at-point-functions'. + --- ** The default 'Info-default-directory-list' no longer checks some obsolete directory suffixes (gnu, gnu/lib, gnu/lib/emacs, emacs, lib, lib/emacs) commit 5c266405f559823038dfa900aaad66605f0d5287 Author: Mark Oteiza Date: Mon Dec 19 20:35:02 2016 -0500 Recognize graphicsmagick in image-dired * lisp/image-dired.el (image-dired-cmd-create-thumbnail-program): (image-dired-cmd-create-thumbnail-options): (image-dired-cmd-create-temp-image-program): (image-dired-cmd-create-temp-image-options): (image-dired-cmd-create-standard-thumbnail-options): (image-dired-cmd-rotate-thumbnail-program): (image-dired-cmd-rotate-thumbnail-options): Account for existence of gm(1) executable. diff --git a/lisp/image-dired.el b/lisp/image-dired.el index 560cadbe75..eed4280c03 100644 --- a/lisp/image-dired.el +++ b/lisp/image-dired.el @@ -221,14 +221,19 @@ expects to find pictures in this directory." :group 'image-dired) (defcustom image-dired-cmd-create-thumbnail-program - "convert" + (cond ((executable-find "gm") "gm") + ((executable-find "convert") "convert") + (t "convert")) "Executable used to create thumbnail. Used together with `image-dired-cmd-create-thumbnail-options'." + :version "26.1" :type 'file :group 'image-dired) (defcustom image-dired-cmd-create-thumbnail-options - '("-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t") + `(,@(when (string-match "gm\\'" image-dired-cmd-create-thumbnail-program) + '("convert")) + "-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t") "Options of command used to create thumbnail image. Used with `image-dired-cmd-create-thumbnail-program'. Available format specifiers are: %w which is replaced by @@ -239,14 +244,20 @@ which is replaced by the file name of the thumbnail file." :type '(repeat (string :tag "Argument")) :group 'image-dired) -(defcustom image-dired-cmd-create-temp-image-program "convert" +(defcustom image-dired-cmd-create-temp-image-program + (cond ((executable-find "gm") "gm") + ((executable-find "convert") "convert") + (t "convert")) "Executable used to create temporary image. Used together with `image-dired-cmd-create-temp-image-options'." + :version "26.1" :type 'file :group 'image-dired) (defcustom image-dired-cmd-create-temp-image-options - '("-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t") + `(,@(when (string-match "gm\\'" image-dired-cmd-create-temp-image-program) + '("convert")) + "-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t") "Options of command used to create temporary image for display window. Used together with `image-dired-cmd-create-temp-image-program', Available format specifiers are: %w and %h which are replaced by @@ -316,15 +327,17 @@ Available format specifiers are described in :group 'image-dired) (defcustom image-dired-cmd-create-standard-thumbnail-options - (append '("-size" "%wx%h" "%f") - (unless (or image-dired-cmd-pngcrush-program - image-dired-cmd-pngnq-program) - (list - "-set" "Thumb::MTime" "%m" - "-set" "Thumb::URI" "file://%f" - "-set" "Description" "Thumbnail of file://%f" - "-set" "Software" (emacs-version))) - '("-thumbnail" "%wx%h>" "png:%t")) + `(,@(when (string-match "gm\\'" image-dired-cmd-create-thumbnail-program) + '("convert")) + "-size" "%wx%h" "%f" + ,@(unless (or image-dired-cmd-pngcrush-program + image-dired-cmd-pngnq-program) + (list + "-set" "Thumb::MTime" "%m" + "-set" "Thumb::URI" "file://%f" + "-set" "Description" "Thumbnail of file://%f" + "-set" "Software" (emacs-version))) + "-thumbnail" "%wx%h>" "png:%t") "Options for creating thumbnails according to the Thumbnail Managing Standard. Available format specifiers are the same as in `image-dired-cmd-create-thumbnail-options', with %m for file modification time." @@ -333,14 +346,19 @@ Available format specifiers are the same as in :group 'image-dired) (defcustom image-dired-cmd-rotate-thumbnail-program - "mogrify" + (cond ((executable-find "gm") "gm") + ((executable-find "mogrify") "mogrify") + (t "mogrify")) "Executable used to rotate thumbnail. Used together with `image-dired-cmd-rotate-thumbnail-options'." + :version "26.1" :type 'file :group 'image-dired) (defcustom image-dired-cmd-rotate-thumbnail-options - '("-rotate" "%d" "%t") + `(,@(when (string-match "gm\\'" image-dired-cmd-rotate-thumbnail-program) + '("mogrify")) + "-rotate" "%d" "%t") "Arguments of command used to rotate thumbnail image. Used with `image-dired-cmd-rotate-thumbnail-program'. Available format specifiers are: %d which is replaced by the commit 36b9973dec65c8daf57d1cb73f0de5a3c59279fc Author: Mark Oteiza Date: Mon Dec 19 19:47:06 2016 -0500 Implement asynchronous thumbnail generation in image-dired Additionally, all FOO-options defcustoms that were in fact shell command strings have been converted to argument lists. Another method for shrinking PNG thumbs with optipng(1) has been added. * lisp/image-dired.el: Remove TODO item in commentary. (image-dired-cmd-create-thumbnail-options): (image-dired-cmd-create-temp-image-options): (image-dired-cmd-rotate-thumbnail-options): (image-dired-cmd-rotate-original-options): (image-dired-cmd-write-exif-data-options): (image-dired-cmd-read-exif-data-options): Convert to argument lists. (image-dired-cmd-pngnq-program, image-dired-cmd-pngcrush-program): Change string type to file. (image-dired-cmd-create-standard-thumbnail-command): Remove. (image-dired-cmd-pngnq-options): (image-dired-cmd-create-standard-thumbnail-options): (image-dired-cmd-optipng-program, image-dired-cmd-optipng-options): New defcustoms. (image-dired-queue, image-dired-queue-active-jobs): (image-dired-queue-active-limit): New variables. (image-dired-pngnq-thumb, image-dired-pngcrush-thumb): (image-dired-optipng-thumb): New functions. (image-dired-create-thumb-1): Renamed from image-dired-create-thumb. Use start-process instead of call-process. Set file modes. Trigger PNG file optimization in process sentinel. (image-dired-thumb-queue-run, image-dired-create-thumb): New functions. (image-dired-display-thumbs): (image-dired-create-thumbs): Don't expect call-process return value. (image-dired-display-image, image-dired-rotate-thumbnail): Use start-process instead of call-process. (image-dired-rotate-original, image-dired-set-exif-data): (image-dired-get-exif-data): Adapt to arguments being an arg list. diff --git a/lisp/image-dired.el b/lisp/image-dired.el index 6902d742db..560cadbe75 100644 --- a/lisp/image-dired.el +++ b/lisp/image-dired.el @@ -118,8 +118,6 @@ ;; * From thumbs.el: Add the "modify" commands (emboss, negate, ;; monochrome etc). ;; -;; * Asynchronous creation of thumbnails. -;; ;; * Add `image-dired-display-thumbs-ring' and functions to cycle that. Find ;; out which is best, saving old batch just before inserting new, or ;; saving the current batch in the ring when inserting it. Adding it @@ -230,14 +228,15 @@ Used together with `image-dired-cmd-create-thumbnail-options'." :group 'image-dired) (defcustom image-dired-cmd-create-thumbnail-options - "%p -size %wx%h \"%f\" -resize \"%wx%h>\" -strip jpeg:\"%t\"" - "Format of command used to create thumbnail image. -Available options are %p which is replaced by -`image-dired-cmd-create-thumbnail-program', %w which is replaced by + '("-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t") + "Options of command used to create thumbnail image. +Used with `image-dired-cmd-create-thumbnail-program'. +Available format specifiers are: %w which is replaced by `image-dired-thumb-width', %h which is replaced by `image-dired-thumb-height', %f which is replaced by the file name of the original image and %t which is replaced by the file name of the thumbnail file." - :type 'string + :version "26.1" + :type '(repeat (string :tag "Argument")) :group 'image-dired) (defcustom image-dired-cmd-create-temp-image-program "convert" @@ -247,14 +246,15 @@ Used together with `image-dired-cmd-create-temp-image-options'." :group 'image-dired) (defcustom image-dired-cmd-create-temp-image-options - "%p -size %wx%h \"%f\" -resize \"%wx%h>\" -strip jpeg:\"%t\"" - "Format of command used to create temporary image for display window. -Available options are %p which is replaced by -`image-dired-cmd-create-temp-image-program', %w and %h which is replaced by + '("-size" "%wx%h" "%f" "-resize" "%wx%h>" "-strip" "jpeg:%t") + "Options of command used to create temporary image for display window. +Used together with `image-dired-cmd-create-temp-image-program', +Available format specifiers are: %w and %h which are replaced by the calculated max size for width and height in the image display window, %f which is replaced by the file name of the original image and %t which is replaced by the file name of the temporary file." - :type 'string + :version "26.1" + :type '(repeat (string :tag "Argument")) :group 'image-dired) (defcustom image-dired-cmd-pngnq-program @@ -264,49 +264,72 @@ is replaced by the file name of the temporary file." It quantizes colors of PNG images down to 256 colors or fewer using the Neuquant procedure." :version "26.1" - :type '(choice (const :tag "Not Set" nil) string) + :type '(choice (const :tag "Not Set" nil) file) + :group 'image-dired) + +(defcustom image-dired-cmd-pngnq-options + '("-f" "%t") + "Arguments to pass `image-dired-cmd-pngnq-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options'." + :version "26.1" + :type '(repeat (string :tag "Argument")) :group 'image-dired) (defcustom image-dired-cmd-pngcrush-program (executable-find "pngcrush") "The file name of the `pngcrush' program. It optimizes the compression of PNG images. Also it adds PNG textual chunks with the information required by the Thumbnail Managing Standard." - :type '(choice (const :tag "Not Set" nil) string) + :type '(choice (const :tag "Not Set" nil) file) :group 'image-dired) -(defcustom image-dired-cmd-create-standard-thumbnail-command - (concat - "%p -size %wx%h \"%f\" " - (unless (or image-dired-cmd-pngcrush-program image-dired-cmd-pngnq-program) - (concat - "-set \"Thumb::MTime\" \"%m\" " - "-set \"Thumb::URI\" \"file://%f\" " - "-set \"Description\" \"Thumbnail of file://%f\" " - "-set \"Software\" \"" (emacs-version) "\" ")) - "-thumbnail \"%wx%h>\" png:\"%t\"" - (if image-dired-cmd-pngnq-program - (concat - " ; " image-dired-cmd-pngnq-program " -f \"%t\"" - (unless image-dired-cmd-pngcrush-program - " ; mv %q %t"))) - (if image-dired-cmd-pngcrush-program - (concat - (unless image-dired-cmd-pngcrush-program - " ; cp %t %q") - " ; " image-dired-cmd-pngcrush-program " -q " - "-text b \"Description\" \"Thumbnail of file://%f\" " - "-text b \"Software\" \"" (emacs-version) "\" " - ;; "-text b \"Thumb::Image::Height\" \"%oh\" " - ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " - ;; "-text b \"Thumb::Image::Width\" \"%ow\" " - "-text b \"Thumb::MTime\" \"%m\" " - ;; "-text b \"Thumb::Size\" \"%b\" " - "-text b \"Thumb::URI\" \"file://%f\" " - "%q %t" - " ; rm %q"))) - "Command to create thumbnails according to the Thumbnail Managing Standard." +(defcustom image-dired-cmd-pngcrush-options + `("-q" + "-text" "b" "Description" "Thumbnail of file://%f" + "-text" "b" "Software" ,(emacs-version) + ;; "-text b \"Thumb::Image::Height\" \"%oh\" " + ;; "-text b \"Thumb::Image::Mimetype\" \"%mime\" " + ;; "-text b \"Thumb::Image::Width\" \"%ow\" " + "-text" "b" "Thumb::MTime" "%m" + ;; "-text b \"Thumb::Size\" \"%b\" " + "-text" "b" "Thumb::URI" "file://%f" + "%q" "%t") + "Arguments for `image-dired-cmd-pngcrush-program'. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %q for a +temporary file name (typically generated by pnqnq)" :version "26.1" - :type 'string + :type '(repeat (string :tag "Argument")) + :group 'image-dired) + +(defcustom image-dired-cmd-optipng-program (executable-find "optipng") + "The file name of the `optipng' program." + :type '(choice (const :tag "Not Set" nil) file) + :group 'image-dired) + +(defcustom image-dired-cmd-optipng-options '("-o5" "%t") + "Arguments passed to `image-dired-optipng-program'. +Available format specifiers are described in +`image-dired-cmd-create-thumbnail-options'." + :type '(repeat (string :tag "Argument")) + :link '(url-link "man:optipng(1)") + :group 'image-dired) + +(defcustom image-dired-cmd-create-standard-thumbnail-options + (append '("-size" "%wx%h" "%f") + (unless (or image-dired-cmd-pngcrush-program + image-dired-cmd-pngnq-program) + (list + "-set" "Thumb::MTime" "%m" + "-set" "Thumb::URI" "file://%f" + "-set" "Description" "Thumbnail of file://%f" + "-set" "Software" (emacs-version))) + '("-thumbnail" "%wx%h>" "png:%t")) + "Options for creating thumbnails according to the Thumbnail Managing Standard. +Available format specifiers are the same as in +`image-dired-cmd-create-thumbnail-options', with %m for file modification time." + :version "26.1" + :type '(repeat (string :tag "Argument")) :group 'image-dired) (defcustom image-dired-cmd-rotate-thumbnail-program @@ -317,14 +340,15 @@ Used together with `image-dired-cmd-rotate-thumbnail-options'." :group 'image-dired) (defcustom image-dired-cmd-rotate-thumbnail-options - "%p -rotate %d \"%t\"" - "Format of command used to rotate thumbnail image. -Available options are %p which is replaced by -`image-dired-cmd-rotate-thumbnail-program', %d which is replaced by the + '("-rotate" "%d" "%t") + "Arguments of command used to rotate thumbnail image. +Used with `image-dired-cmd-rotate-thumbnail-program'. +Available format specifiers are: %d which is replaced by the number of (positive) degrees to rotate the image, normally 90 or 270 \(for 90 degrees right and left), %t which is replaced by the file name of the thumbnail file." - :type 'string + :version "26.1" + :type '(repeat (string :tag "Argument")) :group 'image-dired) (defcustom image-dired-cmd-rotate-original-program @@ -335,15 +359,16 @@ Used together with `image-dired-cmd-rotate-original-options'." :group 'image-dired) (defcustom image-dired-cmd-rotate-original-options - "%p -rotate %d -copy all -outfile %t \"%o\"" - "Format of command used to rotate original image. -Available options are %p which is replaced by -`image-dired-cmd-rotate-original-program', %d which is replaced by the + '("-rotate" "%d" "-copy" "all" "-outfile" "%t" "%o") + "Arguments of command used to rotate original image. +Used with `image-dired-cmd-rotate-original-program'. +Available format specifiers are: %d which is replaced by the number of (positive) degrees to rotate the image, normally 90 or 270 \(for 90 degrees right and left), %o which is replaced by the original image file name and %t which is replaced by `image-dired-temp-image-file'." - :type 'string + :version "26.1" + :type '(repeat (string :tag "Argument")) :group 'image-dired) (defcustom image-dired-temp-rotate-image-file @@ -367,13 +392,14 @@ Used together with `image-dired-cmd-write-exif-data-options'." :group 'image-dired) (defcustom image-dired-cmd-write-exif-data-options - "%p -%t=\"%v\" \"%f\"" - "Format of command used to write EXIF data. -Available options are %p which is replaced by -`image-dired-cmd-write-exif-data-program', %f which is replaced by + '("-%t=%v" "%f") + "Arguments of command used to write EXIF data. +Used with `image-dired-cmd-write-exif-data-program'. +Available format specifiers are: %f which is replaced by the image file name, %t which is replaced by the tag name and %v which is replaced by the tag value." - :type 'string + :version "26.1" + :type '(repeat (string :tag "Argument")) :group 'image-dired) (defcustom image-dired-cmd-read-exif-data-program @@ -384,12 +410,13 @@ Used together with `image-dired-cmd-read-exif-data-options'." :group 'image-dired) (defcustom image-dired-cmd-read-exif-data-options - "%p -s -s -s -%t \"%f\"" - "Format of command used to read EXIF data. -Available options are %p which is replaced by -`image-dired-cmd-write-exif-data-program', %f which is replaced + '("-s" "-s" "-s" "-%t" "%f") + "Arguments of command used to read EXIF data. +Used with `image-dired-cmd-read-exif-data-program'. +Available format specifiers are: %f which is replaced by the image file name and %t which is replaced by the tag name." - :type 'string + :version "26.1" + :type '(repeat (string :tag "Argument")) :group 'image-dired) (defcustom image-dired-gallery-hidden-tags @@ -640,7 +667,81 @@ DIMENSION should be either the symbol 'width or 'height." (width image-dired-thumb-width) (height image-dired-thumb-height))))) -(defun image-dired-create-thumb (original-file thumbnail-file) +(defvar image-dired-queue nil + "List of items in the queue. +Each item has the form (ORIGINAL-FILE TARGET-FILE).") + +(defvar image-dired-queue-active-jobs 0 + "Number of active jobs in `image-dired-queue'.") + +(defvar image-dired-queue-active-limit 2 + "Maximum number of concurrent jobs permitted for generating images. +Increase at own risk.") + +(defun image-dired-pngnq-thumb (spec) + "Quantize thumbnail described by format SPEC with pngnq(1)." + (let ((process + (apply #'start-process "image-dired-pngnq" nil + image-dired-cmd-pngnq-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngnq-options)))) + (setf (process-sentinel process) + (lambda (process status) + (if (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + ;; Pass off to pngcrush, or just rename the + ;; THUMB-nq8.png file back to THUMB.png + (if (and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec) + (let ((nq8 (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (rename-file nq8 thumb t))) + (message "command %S %s" (process-command process) + (replace-regexp-in-string "\n" "" status))))) + process)) + +(defun image-dired-pngcrush-thumb (spec) + "Optimize thumbnail decsribed by format SPEC with pngcrush(1)." + ;; If pngnq wasn't run, then the THUMB-nq8.png file does not exist. + ;; pngcrush needs an infile and outfile, so we just copy THUMB to + ;; THUMB-nq8.png and use the latter as a temp file. + (when (not image-dired-cmd-pngnq-program) + (let ((temp (cdr (assq ?q spec))) + (thumb (cdr (assq ?t spec)))) + (copy-file thumb temp))) + (let ((process + (apply #'start-process "image-dired-pngcrush" nil + image-dired-cmd-pngcrush-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-pngcrush-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (replace-regexp-in-string "\n" "" status))) + (when (memq (process-status process) '(exit signal)) + (let ((temp (cdr (assq ?q spec)))) + (delete-file temp))))) + process)) + +(defun image-dired-optipng-thumb (spec) + "Optimize thumbnail decsribed by format SPEC with optipng(1)." + (let ((process + (apply #'start-process "image-dired-optipng" nil + image-dired-cmd-optipng-program + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-optipng-options)))) + (setf (process-sentinel process) + (lambda (process status) + (unless (and (eq (process-status process) 'exit) + (zerop (process-exit-status process))) + (message "command %S %s" (process-command process) + (replace-regexp-in-string "\n" "" status))))) + process)) + +(defun image-dired-create-thumb-1 (original-file thumbnail-file) "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE." (image-dired--check-executable-exists 'image-dired-cmd-create-thumbnail-program) @@ -649,25 +750,76 @@ DIMENSION should be either the symbol 'width or 'height." (modif-time (floor (float-time (nth 5 (file-attributes original-file))))) (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png" thumbnail-file)) - (command - (format-spec - (if (memq image-dired-thumbnail-storage '(standard standard-large)) - image-dired-cmd-create-standard-thumbnail-command - image-dired-cmd-create-thumbnail-options) - (list - (cons ?p image-dired-cmd-create-thumbnail-program) - (cons ?w width) - (cons ?h height) - (cons ?m modif-time) - (cons ?f original-file) - (cons ?q thumbnail-nq8-file) - (cons ?t thumbnail-file)))) - thumbnail-dir) - (when (not (file-exists-p - (setq thumbnail-dir (file-name-directory thumbnail-file)))) - (message "Creating thumbnail directory.") - (make-directory thumbnail-dir t)) - (call-process shell-file-name nil nil nil shell-command-switch command))) + (spec + (list + (cons ?w width) + (cons ?h height) + (cons ?m modif-time) + (cons ?f original-file) + (cons ?q thumbnail-nq8-file) + (cons ?t thumbnail-file))) + (thumbnail-dir (file-name-directory thumbnail-file)) + process) + (when (not (file-exists-p thumbnail-dir)) + (message "Creating thumbnail directory") + (make-directory thumbnail-dir t) + (set-file-modes thumbnail-dir #o700)) + + ;; Thumbnail file creation processes begin here and are marshalled + ;; in a queue by `image-dired-create-thumb'. + (setq process + (apply #'start-process "image-dired-create-thumbnail" nil + image-dired-cmd-create-thumbnail-program + (mapcar + (lambda (arg) (format-spec arg spec)) + (if (memq image-dired-thumbnail-storage + '(standard standard-large)) + image-dired-cmd-create-standard-thumbnail-options + image-dired-cmd-create-thumbnail-options)))) + + (setf (process-sentinel process) + (lambda (process status) + ;; Trigger next in queue once a thumbnail has been created + (cl-decf image-dired-queue-active-jobs) + (image-dired-thumb-queue-run) + (if (not (and (eq (process-status process) 'exit) + (zerop (process-exit-status process)))) + (message "Thumb could not be created for %s: %s" + (abbreviate-file-name original-file) + (replace-regexp-in-string "\n" "" status)) + (set-file-modes thumbnail-file #o600) + (clear-image-cache thumbnail-file) + ;; PNG thumbnail has been created since we are + ;; following the XDG thumbnail spec, so try to optimize + (when (memq image-dired-thumbnail-storage + '(standard standard-large)) + (cond + ((and image-dired-cmd-pngnq-program + (executable-find image-dired-cmd-pngnq-program)) + (image-dired-pngnq-thumb spec)) + ((and image-dired-cmd-pngcrush-program + (executable-find image-dired-cmd-pngcrush-program)) + (image-dired-pngcrush-thumb spec)) + ((and image-dired-cmd-optipng-program + (executable-find image-dired-cmd-optipng-program)) + (image-dired-optipng-thumb spec))))))) + process)) + +(defun image-dired-thumb-queue-run () + "Run a queued job if one exists and not too many jobs are running. +Queued items live in `image-dired-queue'." + (while (and image-dired-queue + (< image-dired-queue-active-jobs + image-dired-queue-active-limit)) + (cl-incf image-dired-queue-active-jobs) + (apply #'image-dired-create-thumb-1 (pop image-dired-queue)))) + +(defun image-dired-create-thumb (original-file thumbnail-file) + "Add a job for generating thumbnail to `image-dired-queue'." + (setq image-dired-queue + (nconc image-dired-queue + (list (list original-file thumbnail-file)))) + (run-at-time 0 nil #'image-dired-thumb-queue-run)) ;;;###autoload (defun image-dired-dired-toggle-marked-thumbs (&optional arg) @@ -867,10 +1019,9 @@ thumbnail buffer to be selected." (goto-char (point-max))) (dolist (curr-file files) (setq thumb-name (image-dired-thumb-name curr-file)) - (if (and (not (file-exists-p thumb-name)) - (not (= 0 (image-dired-create-thumb curr-file thumb-name)))) - (message "Thumb could not be created for file %s" curr-file) - (image-dired-insert-thumbnail thumb-name curr-file dired-buf)))) + (when (not (file-exists-p thumb-name)) + (image-dired-create-thumb curr-file thumb-name)) + (image-dired-insert-thumbnail thumb-name curr-file dired-buf))) (if do-not-pop (display-buffer buf) (pop-to-buffer buf)) @@ -1554,8 +1705,7 @@ With prefix argument ARG, create thumbnails even if they already exist (clear-image-cache (expand-file-name thumb-name))) (when (or (not (file-exists-p thumb-name)) arg) - (when (not (= 0 (image-dired-create-thumb curr-file thumb-name))) - (error "Thumb could not be created")))))) + (image-dired-create-thumb curr-file thumb-name))))) (defvar image-dired-slideshow-timer nil "Slideshow timer.") @@ -1747,17 +1897,19 @@ original size." (image-type 'jpeg)) (setq file (expand-file-name file)) (if (not original-size) - (let* ((command - (format-spec - image-dired-cmd-create-temp-image-options - (list - (cons ?p image-dired-cmd-create-temp-image-program) - (cons ?w (image-dired-display-window-width window)) - (cons ?h (image-dired-display-window-height window)) - (cons ?f file) - (cons ?t new-file)))) - (ret (call-process shell-file-name nil nil nil - shell-command-switch command))) + (let* ((spec + (list + (cons ?p image-dired-cmd-create-temp-image-program) + (cons ?w (image-dired-display-window-width window)) + (cons ?h (image-dired-display-window-height window)) + (cons ?f file) + (cons ?t new-file))) + (ret + (apply #'call-process + image-dired-cmd-create-temp-image-program nil nil nil + (mapcar + (lambda (arg) (format-spec arg spec)) + image-dired-cmd-create-temp-image-options)))) (when (not (zerop ret)) (error "Could not resize image"))) (setq image-type (image-type-from-file-name file)) @@ -1811,14 +1963,10 @@ With prefix argument ARG, display image in its original size." (message "No thumbnail at point") (let* ((file (image-dired-thumb-name (image-dired-original-file-name))) (thumb (expand-file-name file)) - command) - (setq command (format-spec - image-dired-cmd-rotate-thumbnail-options - (list - (cons ?p image-dired-cmd-rotate-thumbnail-program) - (cons ?d degrees) - (cons ?t thumb)))) - (call-process shell-file-name nil nil nil shell-command-switch command) + (spec (list (cons ?d degrees) (cons ?t thumb)))) + (apply #'call-process image-dired-cmd-rotate-thumbnail-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-thumbnail-options)) (clear-image-cache thumb)))) (defun image-dired-rotate-thumbnail-left () @@ -1853,19 +2001,18 @@ overwritten. This confirmation can be turned off using 'image-dired-cmd-rotate-original-program) (if (not (image-dired-image-at-point-p)) (message "No image at point") - (let ((file (image-dired-original-file-name)) - command) + (let* ((file (image-dired-original-file-name)) + (spec + (list + (cons ?d degrees) + (cons ?o (expand-file-name file)) + (cons ?t image-dired-temp-rotate-image-file)))) (unless (eq 'jpeg (image-type file)) (error "Only JPEG images can be rotated!")) - (setq command (format-spec - image-dired-cmd-rotate-original-options - (list - (cons ?p image-dired-cmd-rotate-original-program) - (cons ?d degrees) - (cons ?o (expand-file-name file)) - (cons ?t image-dired-temp-rotate-image-file)))) - (if (not (= 0 (call-process shell-file-name nil nil nil - shell-command-switch command))) + (if (not (= 0 (apply #'call-process image-dired-cmd-rotate-original-program + nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-rotate-original-options)))) (error "Could not rotate image") (image-dired-display-image image-dired-temp-rotate-image-file) (if (or (and image-dired-rotate-original-ask-before-overwrite @@ -1931,32 +2078,30 @@ default value at the prompt." "In FILE, set EXIF tag TAG-NAME to value TAG-VALUE." (image-dired--check-executable-exists 'image-dired-cmd-write-exif-data-program) - (let (command) - (setq command (format-spec - image-dired-cmd-write-exif-data-options - (list - (cons ?p image-dired-cmd-write-exif-data-program) - (cons ?f (expand-file-name file)) - (cons ?t tag-name) - (cons ?v tag-value)))) - (call-process shell-file-name nil nil nil shell-command-switch command))) + (let ((spec + (list + (cons ?f (expand-file-name file)) + (cons ?t tag-name) + (cons ?v tag-value)))) + (apply #'call-process image-dired-cmd-write-exif-data-program nil nil nil + (mapcar (lambda (arg) (format-spec arg spec)) + image-dired-cmd-write-exif-data-options)))) (defun image-dired-get-exif-data (file tag-name) "From FILE, return EXIF tag TAG-NAME." (image-dired--check-executable-exists 'image-dired-cmd-read-exif-data-program) (let ((buf (get-buffer-create "*image-dired-get-exif-data*")) - command tag-value) - (setq command (format-spec - image-dired-cmd-read-exif-data-options - (list - (cons ?p image-dired-cmd-read-exif-data-program) - (cons ?f file) - (cons ?t tag-name)))) + (spec (list (cons ?f file) (cons ?t tag-name))) + tag-value) (with-current-buffer buf (delete-region (point-min) (point-max)) - (if (not (eq (call-process shell-file-name nil t nil - shell-command-switch command) 0)) + (if (not (eq (apply #'call-process image-dired-cmd-read-exif-data-program + nil t nil + (mapcar + (lambda (arg) (format-spec arg spec)) + image-dired-cmd-read-exif-data-options)) + 0)) (error "Could not get EXIF tag") (goto-char (point-min)) ;; Clean buffer from newlines and carriage returns before commit 85aebc12de28667cdccde5b080972453544d015e Author: Andreas Schwab Date: Mon Dec 19 23:07:42 2016 +0100 Protect change of window's buffer in vertical-motion against unwinds (bug#25209) * indent.c (restore_window_buffer): New function. (Fvertical_motion): Use it to restore window's buffer. diff --git a/src/indent.c b/src/indent.c index b68b60297f..29c9ffd90c 100644 --- a/src/indent.c +++ b/src/indent.c @@ -1958,6 +1958,20 @@ window_column_x (struct window *w, Lisp_Object window, return x; } +/* Restore window's buffer and point. */ + +static void +restore_window_buffer (Lisp_Object list) +{ + struct window *w = decode_live_window (XCAR (list)); + list = XCDR (list); + wset_buffer (w, XCAR (list)); + list = XCDR (list); + set_marker_both (w->pointm, w->contents, + XFASTINT (XCAR (list)), + XFASTINT (XCAR (XCDR (list)))); +} + DEFUN ("vertical-motion", Fvertical_motion, Svertical_motion, 1, 3, 0, doc: /* Move point to start of the screen line LINES lines down. If LINES is negative, this means moving up. @@ -1997,10 +2011,9 @@ whether or not it is currently displayed in some window. */) struct it it; struct text_pos pt; struct window *w; - Lisp_Object old_buffer; - EMACS_INT old_charpos UNINIT, old_bytepos UNINIT; Lisp_Object lcols; void *itdata = NULL; + ptrdiff_t count = SPECPDL_INDEX (); /* Allow LINES to be of the form (HPOS . VPOS) aka (COLUMNS . LINES). */ bool lcols_given = CONSP (lines); @@ -2013,13 +2026,13 @@ whether or not it is currently displayed in some window. */) CHECK_NUMBER (lines); w = decode_live_window (window); - old_buffer = Qnil; if (XBUFFER (w->contents) != current_buffer) { /* Set the window's buffer temporarily to the current buffer. */ - old_buffer = w->contents; - old_charpos = marker_position (w->pointm); - old_bytepos = marker_byte_position (w->pointm); + Lisp_Object old = list4 (window, w->contents, + make_number (marker_position (w->pointm)), + make_number (marker_byte_position (w->pointm))); + record_unwind_protect (restore_window_buffer, old); wset_buffer (w, Fcurrent_buffer ()); set_marker_both (w->pointm, w->contents, BUF_PT (current_buffer), BUF_PT_BYTE (current_buffer)); @@ -2255,12 +2268,7 @@ whether or not it is currently displayed in some window. */) bidi_unshelve_cache (itdata, 0); } - if (BUFFERP (old_buffer)) - { - wset_buffer (w, old_buffer); - set_marker_both (w->pointm, w->contents, - old_charpos, old_bytepos); - } + unbind_to (count, Qnil); return make_number (it.vpos); } commit 504e3846041e4fcd1707a9ad6176ddaf3fec3d02 Author: Glenn Morris Date: Mon Dec 19 13:32:55 2016 -0500 Improve default load-path for uninstalled CANNOT_DUMP builds * src/lread.c (load_path_default) [CANNOT_DUMP]: Use build load-path if we seem to be running uninstalled. (Bug#24974) I think this became an issue several years ago when we stopped using EMACSLOADPATH in the Makefiles; however this change should improve the CANNOT_DUMP uninstalled case in general. diff --git a/src/lread.c b/src/lread.c index 157a392a15..fdbf032911 100644 --- a/src/lread.c +++ b/src/lread.c @@ -4271,7 +4271,9 @@ load_path_check (Lisp_Object lpath) are running uninstalled. Uses the following logic: - If CANNOT_DUMP: Use PATH_LOADSEARCH. + If CANNOT_DUMP: + If Vinstallation_directory is not nil (ie, running uninstalled), + use PATH_DUMPLOADSEARCH (ie, build path). Else use PATH_LOADSEARCH. The remainder is what happens when dumping works: If purify-flag (ie dumping) just use PATH_DUMPLOADSEARCH. Otherwise use PATH_LOADSEARCH. @@ -4305,6 +4307,8 @@ load_path_default (void) #endif normal = PATH_LOADSEARCH; + if (!NILP (Vinstallation_directory)) normal = PATH_DUMPLOADSEARCH; + #ifdef HAVE_NS lpath = decode_env_path (0, loadpath ? loadpath : normal, 0); #else commit fe3188b1cecc7ac5534616c8edf14a84b1b3bbb0 Author: Eli Zaretskii Date: Mon Dec 19 19:11:16 2016 +0200 Fix crashes upon C-g on Posix TTY frames * src/thread.h (struct thread_state): New member not_holding_lock. (maybe_reacquire_global_lock): Add prototype. * src/thread.c: Include syssignal.h. (maybe_reacquire_global_lock): New function. (really_call_select): Set the not_holding_lock member of the thread state before releasing the lock, and rest it after re-acquiring the lock when the select function returns. Block SIGINT while doing this to make sure we are not interrupted on TTY frames. * src/sysdep.c (block_interrupt_signal, restore_signal_mask): New functions. * src/syssignal.h (block_interrupt_signal, restore_signal_mask): Add prototypes. * src/keyboard.c (read_char) [THREADS_ENABLED]: Call maybe_reacquire_global_lock. (Bug#25178) diff --git a/src/keyboard.c b/src/keyboard.c index 1fb1d492ce..f2ee313b8c 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -2571,6 +2571,9 @@ read_char (int commandflag, Lisp_Object map, so restore it now. */ restore_getcjmp (save_jump); pthread_sigmask (SIG_SETMASK, &empty_mask, 0); +#if THREADS_ENABLED + maybe_reacquire_global_lock (); +#endif unbind_to (jmpcount, Qnil); XSETINT (c, quit_char); internal_last_event_frame = selected_frame; diff --git a/src/sysdep.c b/src/sysdep.c index 3d2b9bdeee..96c9e53840 100644 --- a/src/sysdep.c +++ b/src/sysdep.c @@ -765,6 +765,23 @@ unblock_child_signal (sigset_t const *oldset) pthread_sigmask (SIG_SETMASK, oldset, 0); } +/* Block SIGINT. */ +void +block_interrupt_signal (sigset_t *oldset) +{ + sigset_t blocked; + sigemptyset (&blocked); + sigaddset (&blocked, SIGINT); + pthread_sigmask (SIG_BLOCK, &blocked, oldset); +} + +/* Restore previously saved signal mask. */ +void +restore_signal_mask (sigset_t const *oldset) +{ + pthread_sigmask (SIG_SETMASK, oldset, 0); +} + #endif /* !MSDOS */ /* Saving and restoring the process group of Emacs's terminal. */ diff --git a/src/syssignal.h b/src/syssignal.h index 3de83c7175..62704fc351 100644 --- a/src/syssignal.h +++ b/src/syssignal.h @@ -25,6 +25,8 @@ along with GNU Emacs. If not, see . */ extern void init_signals (bool); extern void block_child_signal (sigset_t *); extern void unblock_child_signal (sigset_t const *); +extern void block_interrupt_signal (sigset_t *); +extern void restore_signal_mask (sigset_t const *); extern void block_tty_out_signal (sigset_t *); extern void unblock_tty_out_signal (sigset_t const *); diff --git a/src/thread.c b/src/thread.c index e8cb430119..bf2cf1b06c 100644 --- a/src/thread.c +++ b/src/thread.c @@ -24,6 +24,7 @@ along with GNU Emacs. If not, see . */ #include "buffer.h" #include "process.h" #include "coding.h" +#include "syssignal.h" static struct thread_state primary_thread; @@ -100,6 +101,23 @@ acquire_global_lock (struct thread_state *self) post_acquire_global_lock (self); } +/* This is called from keyboard.c when it detects that SIGINT + interrupted thread_select before the current thread could acquire + the lock. We must acquire the lock to prevent a thread from + running without holding the global lock, and to avoid repeated + calls to sys_mutex_unlock, which invokes undefined behavior. */ +void +maybe_reacquire_global_lock (void) +{ + if (current_thread->not_holding_lock) + { + struct thread_state *self = current_thread; + + acquire_global_lock (self); + current_thread->not_holding_lock = 0; + } +} + static void @@ -493,11 +511,20 @@ really_call_select (void *arg) { struct select_args *sa = arg; struct thread_state *self = current_thread; + sigset_t oldset; + block_interrupt_signal (&oldset); + self->not_holding_lock = 1; release_global_lock (); + restore_signal_mask (&oldset); + sa->result = (sa->func) (sa->max_fds, sa->rfds, sa->wfds, sa->efds, sa->timeout, sa->sigmask); + + block_interrupt_signal (&oldset); acquire_global_lock (self); + self->not_holding_lock = 0; + restore_signal_mask (&oldset); } int diff --git a/src/thread.h b/src/thread.h index e6084b13c2..7dee67d659 100644 --- a/src/thread.h +++ b/src/thread.h @@ -171,6 +171,13 @@ struct thread_state interrupter should broadcast to this condition. */ sys_cond_t *wait_condvar; + /* This thread might have released the global lock. If so, this is + non-zero. When a thread runs outside thread_select with this + flag non-zero, it means it has been interrupted by SIGINT while + in thread_select, and didn't have a chance of acquiring the lock. + It must do so ASAP. */ + int not_holding_lock; + /* Threads are kept on a linked list. */ struct thread_state *next_thread; }; @@ -224,6 +231,7 @@ extern void unmark_threads (void); extern void finalize_one_thread (struct thread_state *state); extern void finalize_one_mutex (struct Lisp_Mutex *); extern void finalize_one_condvar (struct Lisp_CondVar *); +extern void maybe_reacquire_global_lock (void); extern void init_threads_once (void); extern void init_threads (void); commit 657bcaf5ac30449915e070c3fa80a2eaaf1ee7e1 Author: Sam Steingold Date: Mon Dec 19 11:44:18 2016 -0500 avoid Eager macro-expansion failure: (void-function string-to-list) * loadup.el [ns]: "ucs-normalize" uses `string-to-list' which is defined in "mule-util", so we have to load "mule-util" before "ucs-normalize", otherwise I get "Eager macro-expansion failure" on "make bootstrap" diff --git a/lisp/loadup.el b/lisp/loadup.el index e9dd683b28..5350024031 100644 --- a/lisp/loadup.el +++ b/lisp/loadup.el @@ -301,6 +301,7 @@ ;; already produced, because it needs uni-*.el files that might ;; not be built early enough during bootstrap. (when (load-history-filename-element "charprop\\.el") + (load "international/mule-util") (load "international/ucs-normalize") (load "term/ns-win")))) (if (fboundp 'x-create-frame)