all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
* Ibuffer improvements: filtering, documentation, bug fix, tests
@ 2016-11-17 15:53 Christopher Genovese
  2016-11-18 13:08 ` Richard Stallman
  2016-11-19 11:17 ` Tino Calancha
  0 siblings, 2 replies; 10+ messages in thread
From: Christopher Genovese @ 2016-11-17 15:53 UTC (permalink / raw)
  To: emacs-devel


[-- Attachment #1.1: Type: text/plain, Size: 2581 bytes --]

I'd like to submit some mild changes in Ibuffer (ibuffer.el, ibuf-ext.el,
and ibuffer-tests.el) which are available in this branch

      https://github.com/genovese/emacs/tree/ibuffer-and-filters

which is up to date relative to the HEAD as of this morning. All these
changes have been tested successfully in a new and a pre-existing build
of emacs. (And my copyright paperwork has already been processed.)
A brief description is below, with more detail in the committ message.
I've also attached a patch file in case anyone prefers that.

Brief Description: As a heavy Ibuffer user, I make steady use of
filters and filter groups, with some quite specific rules. While
the filter lists offer an implicit logical 'and', it would be
much more convenient if one could use an 'and' *within* these rules.
(Although DeMorgan's laws will work with 'or' and 'not' and
saved filters can help to simulate this, neither is particularly
convenient, readable, nor aestheticcally pleasing.) The proposed
changes, summarize below, were motivated by adding this simple
and negligble-cost feature to the filtering.

The proposed changes are as follows:

  + Compound filters

    Add support for 'and' and normalize handling of 'not' to allow the
    original "spliced" format as well as a more lispy "sexp" format.

    Original documentation for the structure of compound filters was
    almost completely lacking. The updated code documents compound
    filter structure and clarifies the language used throughout,
    providing a single authoritative source for documentation on each
    concept.

    Fixed bug in 'saved' filter handling. There was an inconsistency in
    how the data was accessed at different points that would cause
    failure. (I do wonder if anyone ever uses saved filters based on
    this.) There are two choices in how to fix this; I made one but am
    open to both.

  + New pre-defined filters and an interactive filtering command

    Several new filters are defined by default to handle some very
    common filtering tasks (e.g., matching filename components since
    the 'filename' filter matches on the absolute pathname). A new
    command is offered to select a filter by completion on the
    descriptions, which is very easy to use without remembering key
    bindings.

  + Documentation fixes throughout ibuf-ext.el

  + Many new tests and fixed bug in original test.

    All the tests pass in emacs -Q in both new and
    old builds of emacs 25 (on Mac OS X).


I look forward to any questions or comments. Thanks for your
consideration.

-- Chris

[-- Attachment #1.2: Type: text/html, Size: 3029 bytes --]

[-- Attachment #2: ibuffer-and-filters.patch --]
[-- Type: text/x-diff, Size: 73286 bytes --]

From a331e8c0820b1c1e5c45c51e26d89cd9ffad6b9d Mon Sep 17 00:00:00 2001
From: "Christopher R. Genovese" <genovese@cmu.edu>
Date: Thu, 17 Nov 2016 00:44:27 -0500
Subject: [PATCH] Ibuffer improvements: filters, documentation, bug fixes,
 tests

+ Provides compound filter to support explicit logical 'and'

  While current and saved ibuffer filter lists offer an implicit
  logical 'and', it can be useful for defining complex filters
  and filter groups to be able to use 'and' explicitly within a
  filter. Although this could be achieved with DeMorgan's laws
  using 'or' and 'not', or saved filters, both options are
  unnecessarily onerous. Providing an 'and' conveniently
  increases filtering power at negligible cost.

+ Accepts 'not' compound in (not . qualifier), (not qualifier) forms

  The original 'not' compound filter expects the form
  (not . qualifier), e.g., (not size-gt . 100). This adds
  support, at negligible cost, for the alternative, more lispy,
  form like (not (size-gt . 100)) or (not (or ...)). The
  original looks nice with nullary filters like (not modified),
  and the new form is pleasantly consistent with sexp structure
  of 'and' and 'or'.

+ Significant documentation improvements for filtering

  The structure of compound filters had not been documented. The
  new documentation gives an authoritative source for each
  concept and makes the language used throughout more clear and
  consistent (e.g., distinguishing qualifier data from general
  filter specifications).

+ Defines several commonly needed filters

  The existing 'filename' matches against the full pathname of
  the buffer's file. This can be inconvenient for precisely
  filtering files, so several new filters are pre-defined to
  match particular pathname components. In addition, convenient
  nullary filters for starred and modified buffers are provided.

+ Fixes bug in `ibuffer-save-filters'

  The structure of `ibuffer-saved-filters' and
  `ibuffer-save-filter' were inconsistent, with the former
  having an extra list level in each alist element. This version
  fixes the inconsistency by simplifying `ibuffer-saved-filters'
  to remove the extra list level and automatically checks to
  repair existing formats. (Alternatively, the access code could
  be special-cased leaving the variable format intact. This
  alternative would arguably be lower impact, but the change
  made seemed asthetically nicer.)

+ Defines completion-based interactive filtering command

  New command to select a filter by completion on filter
  descriptions. Easy to use and bound to /-TAB mnemonically.

+ Fixes small bug in original test

  The one original test failed unexpectedly if ibuf-ext were
  loaded.

+ Adds a substantial number of additional tests with feature ibuf-ext

  Many new tests in ert, leaving the environment untouched,
  cover most aspects of filtering, old and new.

+ Makes a few mnemonic changes to default filtering part of keymap

These changs are mostly quite small but distributed across
several functions and docstrings. See the change log below.

Change Log: 2016-11-16	Christopher R. Genovese	 <genovese@cmu.edu>

* lisp/ibuf-ext.el: added paragraph to file commentary
* lisp/ibuf-ext.el (ibuffer-saved-filters): clarified documentation,
  specified customization type, and simplified data format to be
  consistent with `ibuffer-save-filters'
* lisp/ibuf-ext.el (ibuffer-update-saved-filters-format): new function
  that transforms `ibuffer-saved-filters'-style alist format
* lisp/ibuf-ext.el (ibuffer-repair-saved-filters): new function that
  transforms `ibuffer-saved-filters' to new format if needed
* lisp/ibuf-ext.el (ibuffer-filtering-qualifiers): new documentation
  is the authoritative source for filter specification format
* lisp/ibuf-ext.el (ibuffer-filter-groups): new documentation
  clarifies filter group structure and role
* lisp/ibuf-ext.el (ibuffer-unary-operand): new function transparently
  handles not formats for compound filters
* lisp/ibuf-ext.el (ibuffer-included-in-filter-p): new docstring
  and now handles 'not' fully
* lisp/ibuf-ext.el (ibuffer-included-in-filter-p-1): handles 'and'
  compound filters and consistent handling of 'saved' filter data
* lisp/ibuf-ext.el (ibuffer-decompose-filter): handles 'and' as well,
  made handling of 'saved' filter data and 'not' consistent with
  other uses
* lisp/ibuf-ext.el (ibuffer-and-filter): new function analogous to
  `ibuffer-or-filter' for completeness
* lisp/ibuf-ext.el (ibuffer-maybe-save-stuff): handle 'saved' filter
  data consistently with other uses
* lisp/ibuf-ext.el (ibuffer-format-qualifier): handle 'and' filters
* lisp/ibuf-ext.el (ibuffer-filter-by-*): new pre-defined filters
  filename-base, filename-extension, filename-directory,
  filename-root, starred-name, and modified
* lisp/ibuf-ext.el (ibuffer-filter-chosen-by-completion): new
  interactive command for easily choosing a filter
* lisp/ibuf-ext.el: many small improvements throughout to docstrings,
  variable naming, and spacing
* lisp/ibuffer.el: keymap and menu additions/changes for filtering
* test/lisp/ibuffer-tests.el (ibuffer-autoload): added appropriate
  skip specification
* test/lisp/ibuffer-tests.el (ibuffer-*): many additional tests
  that are skipped unless ibuf-ext is loaded.
---
 lisp/ibuf-ext.el           | 492 +++++++++++++++++++++++++++++++--------
 lisp/ibuffer.el            |  67 +++++-
 test/lisp/ibuffer-tests.el | 565 ++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 1021 insertions(+), 103 deletions(-)

diff --git a/lisp/ibuf-ext.el b/lisp/ibuf-ext.el
index b3d1452..a9b337b 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:
 
@@ -37,7 +44,9 @@
   (require 'ibuf-macs)
   (require 'cl-lib))
 
+
 ;;; Utility functions
+
 (defun ibuffer-delete-alist (key alist)
   "Delete all entries in ALIST that have a key equal to KEY."
   (let (entry)
@@ -119,35 +128,177 @@ Buffers whose major mode is in this list, are not searched."
 
 (defvar ibuffer-auto-buffers-changed nil)
 
-(defcustom ibuffer-saved-filters '(("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)))))
-
-  "An alist of filter qualifiers 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'.
-See also the variables `ibuffer-filtering-qualifiers',
-`ibuffer-filtering-alist', and the functions
-`ibuffer-switch-to-saved-filters', `ibuffer-save-filters'."
-  :type '(repeat sexp)
+(defun ibuffer-update-saved-filters-format (filters)
+  "Transforms alist from old to new `ibuffer-saved-filters' format.
+
+Specifically, converts old-format alist with values of the
+form (STRING (FILTER-SPECS...)) to alist with values of the
+form (STRING FILTER-SPECS...), where each filter spec should be a
+cons cell with a symbol in the car. Any elements in the latter
+form are kept as is.
+
+Returns (OLD-FORMAT-DETECTED? . UPDATED-SAVED-FILTERS-LIST)."
+  (when filters
+    (let* ((old-format-detected nil)
+           (fix-filter (lambda (filter-spec)
+                         (if (symbolp (car (cadr filter-spec)))
+                             filter-spec
+                           (setq old-format-detected t) ; side-effect
+                           (cons (car filter-spec) (cadr filter-spec)))))
+           (fixed (mapcar fix-filter filters)))
+      (cons old-format-detected fixed))))
+
+(defcustom ibuffer-saved-filters '(("programming"
+                                    (or (derived-mode . prog-mode)
+                                        (mode         . ess-mode)
+                                        (mode         . compilation-mode)))
+                                   ("text document"
+                                    (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))))
+
+  "An alist mapping saved filter names to filter specifications.
+
+Each element should look like (\"NAME\" . FILTER-LIST), where
+FILTER-LIST has the same structure as the variable
+`ibuffer-filtering-qualifiers', which see. The filters defined
+here are joined with an implicit logical `and' and associated
+with NAME. The combined specification can be used by name in
+other filter specifications via the `saved' qualifier (again, see
+`ibuffer-filtering-qualifiers'). They can also be switched to by
+name (see the functions `ibuffer-switch-to-saved-filters' and
+`ibuffer-save-filters'). The variable `ibuffer-save-with-custom'
+affects how this information is saved for future sessions. This
+variable can be set directly from lisp code."
+  :type '(alist :key-type (string :tag "Filter name")
+                :value-type (repeat :tag "Filter specification" sexp))
+  :set (lambda (symbol value)
+         ;; Just set-default but update legacy old-style format
+         (set-default symbol (cdr (ibuffer-update-saved-filters-format value))))
   :group 'ibuffer)
 
+(defvar ibuffer-old-saved-filters-warning
+  (concat "Deprecated format detected for variable `ibuffer-saved-filters'.
+
+The format has been repaired and the variable modified accordingly. 
+You can save the current value through the customize system by
+either clicking or hitting return "
+            (make-text-button
+             "here" nil
+             'face '(:weight bold :inherit button)
+             'mouse-face '(:weight normal :background "gray50" :inherit button)
+             'follow-link t
+             'help-echo "Click or RET: save new value in customize"
+             'action (lambda (b)
+                       (if (not (fboundp 'customize-save-variable))
+                           (message "Customize not available; value not saved")
+                         (customize-save-variable 'ibuffer-saved-filters
+                                                  ibuffer-saved-filters)
+                         (message "Saved updated ibuffer-saved-filters."))))
+            ". See below for
+an explanation and alternative ways to save the repaired value.
+
+Explanation: For the list variable `ibuffer-saved-filters',               
+elements of the form (STRING (FILTER-SPECS...)) are deprecated
+and should instead have the form (STRING FILTER-SPECS...), where
+each filter spec is a cons cell with a symbol in the car. See
+`ibuffer-saved-filters' for details. The repaired value fixes 
+this format without changing the meaning of the saved filters. 
+
+Alternative ways to save the repaired value:
+
+  1. Do M-x customize-variable and entering `ibuffer-saved-filters' 
+     when prompted. 
+
+  2. Set the updated value manually by copying the 
+     following emacs-lisp form to your emacs init file.
+
+%s
+"))
+
+(defun ibuffer-repair-saved-filters ()
+  "Updates `ibuffer-saved-filters' to its new-style format, if needed.
+
+If this list has any elements of the old-style format, a
+deprecation warning is raised, with a button allowing persistent
+update. Any updated filters retain their meaning in the new
+format. See `ibuffer-update-saved-filters-format' and
+`ibuffer-saved-filters' for details of the old and new formats."
+  (when (and (boundp 'ibuffer-saved-filters) ibuffer-saved-filters)
+    (let ((fixed (ibuffer-update-saved-filters-format ibuffer-saved-filters)))
+      (prog1
+          (setq ibuffer-saved-filters (cdr fixed))
+        (when-let (old-format-detected? (car fixed))
+          (let ((warning-series t)
+                (updated-form
+                 (with-output-to-string
+                   (pp `(setq ibuffer-saved-filters ',ibuffer-saved-filters)))))
+            (display-warning
+             'ibuffer
+             (format ibuffer-old-saved-filters-warning updated-form))))))))
+
 (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
@@ -179,10 +330,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."
@@ -192,20 +351,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)
 
@@ -512,18 +672,38 @@ To evaluate a form without viewing the buffer, see `ibuffer-do-eval'."
 
 ;;;###autoload
 (defun ibuffer-included-in-filters-p (buf filters)
+  "Does the buffer BUF successfully pass all of the given 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)
+  "Does the buffer BUF successfully 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)))
+        ;; ATTN: Allows (not (not ...)) etc. Is fixing this worthwhile?
+        (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)
@@ -531,17 +711,25 @@ 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
-	   (ibuffer-filter-disable t)
-	   (error "Unknown saved filter %s" (cdr filter)))
-	 (ibuffer-included-in-filters-p buf (cadr data))))
+       (let ((data (assoc (cdr filter) ibuffer-saved-filters)))
+         (unless data
+           (ibuffer-filter-disable t)
+           (error "Unknown saved filter %s" (cdr filter)))
+         (ibuffer-included-in-filters-p buf (cdr data))))
       (_
        (pcase-let ((`(,_type ,_desc ,func)
                     (assq (car filter) ibuffer-filtering-alist)))
@@ -828,39 +1016,36 @@ 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)
   (when (null ibuffer-filtering-qualifiers)
     (error "No filters in effect"))
   (let ((lim (pop ibuffer-filtering-qualifiers)))
     (pcase (car lim)
-      (`or
+      ((or 'or 'and)
        (setq ibuffer-filtering-qualifiers (append
-					  (cdr lim)
-					  ibuffer-filtering-qualifiers)))
+                                           (cdr lim)
+                                           ibuffer-filtering-qualifiers)))
       (`saved
-       (let ((data
-	      (assoc (cdr lim)
-		     ibuffer-saved-filters)))
-	 (unless data
-	   (ibuffer-filter-disable)
-	   (error "Unknown saved filter %s" (cdr lim)))
-	 (setq ibuffer-filtering-qualifiers (append
-					    (cadr data)
-					    ibuffer-filtering-qualifiers))))
+       (let ((data (assoc (cdr lim) ibuffer-saved-filters)))
+         (unless data
+           (ibuffer-filter-disable)
+           (error "Unknown saved filter %s" (cdr lim)))
+         (setq ibuffer-filtering-qualifiers (append
+                                             (cdr data)
+                                             ibuffer-filtering-qualifiers))))
       (`not
-       (push (cdr lim)
-	     ibuffer-filtering-qualifiers))
+       (push (ibuffer-unary-operand lim) ibuffer-filtering-qualifiers))
       (_
        (error "Filter type %s is not compound" (car lim)))))
   (ibuffer-update nil t))
@@ -892,12 +1077,12 @@ turned into two separate filters [name: foo] and [mode: bar-mode]."
   (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
+  (if decompose
       (progn
 	(when (or (null ibuffer-filtering-qualifiers)
 		  (not (eq 'or (caar ibuffer-filtering-qualifiers))))
@@ -917,6 +1102,32 @@ filter into parts."
 	      ibuffer-filtering-qualifiers))))
   (ibuffer-update nil t))
 
+;;;###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")
+  (if decompose
+      (progn
+        (when (or (null ibuffer-filtering-qualifiers)
+                  (not (eq 'and (caar ibuffer-filtering-qualifiers))))
+          (error "Top filter is not an AND"))
+        (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 AND"))
+    ;; If the second filter is an AND, just add to it.
+    (let ((first (pop ibuffer-filtering-qualifiers))
+          (second (pop ibuffer-filtering-qualifiers)))
+      (if (eq 'and (car second))
+          (push (nconc (list 'and first) (cdr second))
+                ibuffer-filtering-qualifiers)
+        (push (list 'and first second)
+              ibuffer-filtering-qualifiers))))
+  (ibuffer-update nil t))
+
 (defun ibuffer-maybe-save-stuff ()
   (when ibuffer-save-with-custom
     (if (fboundp 'customize-save-variable)
@@ -939,7 +1150,7 @@ Interactively, prompt for NAME, and use the current filters."
       ibuffer-filtering-qualifiers)))
   (ibuffer-aif (assoc name ibuffer-saved-filters)
       (setcdr it filters)
-    (push (list name filters) ibuffer-saved-filters))
+    (push (cons name filters) ibuffer-saved-filters))
   (ibuffer-maybe-save-stuff))
 
 ;;;###autoload
@@ -989,7 +1200,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)
@@ -998,14 +1211,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."
@@ -1023,7 +1238,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))
@@ -1043,7 +1258,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"
@@ -1062,7 +1277,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
@@ -1073,22 +1288,83 @@ 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 with *."
+  (:description "starred buffer name"
+   :reader nil)
+  (string-match "\\`*" (buffer-name buf)))
+
+;; This should probably be called pathname but kept for backward compatibility
 ;;;###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): "))
+(define-ibuffer-filter filename  
+  "Limit current view to buffers with full file pathname matching QUALIFIER.
+
+For example, for a buffer associated with file '/a/b/c.d', this
+matches against '/a/b/c.d'."
+  (:description "file pathname"
+   :reader (read-from-minibuffer "Filter by file pathname (regexp): "))
   (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
     (string-match qualifier it)))
 
+;; If filename above were renamed to pathname, this could be called filename.
+;;;###autoload (autoload 'ibuffer-filter-by-filename-base "ibuf-ext")
+(define-ibuffer-filter filename-base 
+    "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-filename-extension "ibuf-ext")
+(define-ibuffer-filter filename-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-filename-root "ibuf-ext")
+(define-ibuffer-filter filename-root
+    "Limit current view to buffers with file basename matching QUALIFIER.
+
+The filename root is the part of the full pathname of the file without
+the directory or extension/suffix components. For example, for a buffer
+associated with file '/a/b/c.d', this matches against 'c'."
+  (:description "filename root"
+   :reader (read-from-minibuffer "Filter by filename root (regex): "))
+  (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
+    (string-match qualifier (file-name-base it))))
+
+;;;###autoload (autoload 'ibuffer-filter-by-filename-directory "ibuf-ext")
+(define-ibuffer-filter filename-directory
+    "Limit current view to buffers with filename directory matching QUALIFIER.
+
+For example, for a buffer associated with file '/a/b/c.d', this
+matches against '/a/b'."
+  (:description "directory name"
+   :reader (read-from-minibuffer "Filter by directory name (regex): "))
+  (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
+    (let ((dirname (file-name-directory it)))
+      (when dirname (string-match qualifier dirname)))))
+
 ;;;###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: ")))
@@ -1097,16 +1373,23 @@ 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-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
@@ -1116,12 +1399,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 b33c2e3..181a01c 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -518,28 +518,40 @@ 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 "/ F") 'ibuffer-filter-by-filename-base)
+    (define-key map (kbd "/ .") 'ibuffer-filter-by-filename-extension)
+    (define-key map (kbd "/ r") 'ibuffer-filter-by-filename-root)
+    (define-key map (kbd "/ /") 'ibuffer-filter-by-filename-directory)
     (define-key map (kbd "/ <") 'ibuffer-filter-by-size-lt)
-    (define-key map (kbd "/ r") 'ibuffer-switch-to-saved-filters)
+    (define-key map (kbd "/ >") 'ibuffer-filter-by-size-gt)
+    (define-key map (kbd "/ i") 'ibuffer-filter-by-modified)
+    (define-key map (kbd "/ c") 'ibuffer-filter-by-content)
+    (define-key map (kbd "/ e") 'ibuffer-filter-by-predicate)
+    (define-key map (kbd "/ TAB") 'ibuffer-filter-chosen-by-completion)
+
+    (define-key map (kbd "/ w") '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 "/ <up>") '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-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
-    (define-key map (kbd "/ /") 'ibuffer-filter-disable)
+    (define-key map (kbd "/ DEL") 'ibuffer-filter-disable)
 
     (define-key map (kbd "M-n") 'ibuffer-forward-filter-group)
     (define-key map "\t" 'ibuffer-forward-filter-group)
@@ -647,29 +659,62 @@ directory, like `default-directory'."
     (define-key-after map [menu-bar view filter filter-disable]
       '(menu-item "Disable all filtering" ibuffer-filter-disable
         :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers)))
+
     (define-key-after map [menu-bar view filter filter-by-mode]
       '(menu-item "Add filter by any major mode..." ibuffer-filter-by-mode))
     (define-key-after map [menu-bar view filter filter-by-used-mode]
       '(menu-item "Add filter by a major mode in use..."
         ibuffer-filter-by-used-mode))
     (define-key-after map [menu-bar view filter filter-by-derived-mode]
-      '(menu-item "Add filter by derived mode..."
+      '(menu-item "Add filter by derived mode..." 
                   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 pathname..." 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-filename-base]
+      '(menu-item "Add filter by file basename..."
+                  ibuffer-filter-by-filename-base
+                  :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-filename-extension]
+      '(menu-item "Add filter by filename extension..."
+                  ibuffer-filter-by-filename-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-filename-root]
+      '(menu-item "Add filter by filename root..."
+                  ibuffer-filter-by-filename-root
+                  :help (concat "For a buffer associated with file '/a/b/c.d', "
+                                "list buffer if a given pattern matches 'c'")))
+    (define-key-after map [menu-bar view filter filter-by-filename-directory]
+      '(menu-item "Add filter by filename's directory..."
+                  ibuffer-filter-by-filename-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-content]
       '(menu-item "Add filter by content (regexp)..."
         ibuffer-filter-by-content))
     (define-key-after map [menu-bar view filter filter-by-predicate]
       '(menu-item "Add filter by Lisp predicate..."
         ibuffer-filter-by-predicate))
+
     (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)))
@@ -682,6 +727,12 @@ directory, like `default-directory'."
     (define-key-after map [menu-bar view filter negate-filter]
       '(menu-item "Negate top filter" ibuffer-negate-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 decompose-filter]
       '(menu-item "Decompose top filter" ibuffer-decompose-filter
         :enable (and (featurep 'ibuf-ext)
diff --git a/test/lisp/ibuffer-tests.el b/test/lisp/ibuffer-tests.el
index de281c0..aa06994 100644
--- a/test/lisp/ibuffer-tests.el
+++ b/test/lisp/ibuffer-tests.el
@@ -22,7 +22,8 @@
 (require 'ibuffer)
 
 (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
@@ -30,5 +31,567 @@
     (symbol-function
      'ibuffer-mark-unsaved-buffers))))
 
+;; 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 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)
+                              (filename-base . "\\`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 (filename-base . "\\`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))
+                              (filename-base . "\\`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")))
+          (message "--> %s" buf)
+          (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)))))
+          (message "--> %s" buf))
+      (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 '((filename-base . "ibuf-test-3"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((filename-root . "ibuf-test-3"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((filename-base . "test-3\\.a"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((filename-extension . "a"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA (list (cons 'filename-directory dirA))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB '((filename-base . "ibuf-test-3"))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB '((filename-root . "ibuf-test-3"))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB '((filename-extension . "b"))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB (list (cons 'filename-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 '((filename-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 (filename-extension . "el")
+                              (derived-mode . prog-mode)
+                              (not modified)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((or (filename-extension . "tex")
+                             (derived-mode . prog-mode)
+                             (modified)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((filename-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 '((filename-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
+                        "<HTML><BODY><H1>Hello, World!</H1></BODY></HTML>")))
+          (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 on a single buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((buf
+               (funcall create-non-file-buffer "ibuf-test-8"
+                        :mode #'artist-mode)))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (not (starred-name))
+                              (name . "test-8")
+                              (not (size-gt . 100))
+                              (mode . picture-mode))))))
+      (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))]))
+              (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)))))))
+      (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))]))
+              (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)))))))
+      (funcall clean-up))))
+
+(ert-deftest ibuffer-save-filters ()
+  "Tests that `ibuffer-save-filters' saves in the proper format."
+  (skip-unless (featurep 'ibuf-ext))
+  (let ((ibuffer-save-with-custom nil)
+        (ibuffer-saved-filters nil)
+        (test1 '((mode . org-mode)
+                 (or (size-gt . 10000)
+                     (and (not (starred-name))
+                          (filename-directory . "\<org\>")))))
+        (test2 '((or (mode . emacs-lisp-mode) (filename-extension . "elc?")
+                     (and (starred-name) (name . "elisp"))
+                     (mode . lisp-interaction-mode))))
+        (test3 '((size-lt . 100) (derived-mode . prog-mode)
+                 (or (filename-root . "scratch")
+                     (filename-root . "bonz")
+                     (filename-root . "temp")))))
+    (ibuffer-save-filters "test1" test1)
+    (should (equal (car ibuffer-saved-filters) (cons "test1" test1)))
+    (ibuffer-save-filters "test2" test2)
+    (should (equal (car ibuffer-saved-filters) (cons "test2" test2)))
+    (should (equal (cadr ibuffer-saved-filters) (cons "test1" test1)))
+    (ibuffer-save-filters "test3" test3)
+    (should (equal (car ibuffer-saved-filters) (cons "test3" test3)))
+    (should (equal (cadr ibuffer-saved-filters) (cons "test2" test2)))
+    (should (equal (car (cddr ibuffer-saved-filters)) (cons "test1" test1)))
+    (should (equal (cdr (assoc "test1" ibuffer-saved-filters)) test1))
+    (should (equal (cdr (assoc "test2" ibuffer-saved-filters)) test2))
+    (should (equal (cdr (assoc "test3" ibuffer-saved-filters)) test3))))
+
+(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))
+                         (filename-directory . "\\<org\\>"))))
+        (test5 '(or (filename-root . "scratch")
+                    (filename-root . "bonz")
+                    (filename-root . "temp")))
+        (test6 '(or (mode . emacs-lisp-mode) (filename-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 'filename-directory)
+                                              ": " "\\<org\\>")))))
+    (should (equal (ibuffer-format-qualifier test5)
+                   (funcall tag "OR"
+                            (funcall tag (funcall description 'filename-root)
+                                     ": "  "scratch")
+                            (funcall tag (funcall description 'filename-root)
+                                     ": " "bonz")
+                            (funcall tag (funcall description 'filename-root)
+                                     ": " "temp"))))
+    (should (equal (ibuffer-format-qualifier test6)
+                   (funcall tag "OR"
+                            (funcall tag (funcall description 'mode)
+                                     ": " "emacs-lisp-mode")
+                            (funcall tag (funcall description 'filename-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
-- 
2.10.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-11-17 15:53 Ibuffer improvements: filtering, documentation, bug fix, tests Christopher Genovese
@ 2016-11-18 13:08 ` Richard Stallman
  2016-11-19 11:17 ` Tino Calancha
  1 sibling, 0 replies; 10+ messages in thread
From: Richard Stallman @ 2016-11-18 13:08 UTC (permalink / raw)
  To: Christopher Genovese; +Cc: emacs-devel

[[[ To any NSA and FBI agents reading my email: please consider    ]]]
[[[ whether defending the US Constitution against all enemies,     ]]]
[[[ foreign or domestic, requires you to follow Snowden's example. ]]]

When it's a matter of submitting changes to the Emacs developers, any
method is ok as long as they can read what you want to send them.

But if the point is to publish it for users, please don't use
Github.  See http://gnu.org/software/repo-criteria.html for why.

-- 
Dr Richard Stallman
President, Free Software Foundation (gnu.org, fsf.org)
Internet Hall-of-Famer (internethalloffame.org)
Skype: No way! See stallman.org/skype.html.




^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-11-17 15:53 Ibuffer improvements: filtering, documentation, bug fix, tests Christopher Genovese
  2016-11-18 13:08 ` Richard Stallman
@ 2016-11-19 11:17 ` Tino Calancha
  2016-11-22 23:45   ` Christopher Genovese
  1 sibling, 1 reply; 10+ messages in thread
From: Tino Calancha @ 2016-11-19 11:17 UTC (permalink / raw)
  To: Christopher Genovese; +Cc: emacs-devel, tino.calancha

Christopher Genovese <genovese@cmu.edu> writes:

> I'd like to submit some mild changes in Ibuffer (ibuffer.el, ibuf-ext.el,
> and ibuffer-tests.el)

> The proposed changes are as follows:
>
>   + Compound filters
>
>     Add support for 'and' and normalize handling of 'not' to allow the
>     original "spliced" format as well as a more lispy "sexp" format.
>
>     Original documentation for the structure of compound filters was
>     almost completely lacking. The updated code documents compound
>     filter structure and clarifies the language used throughout,
>     providing a single authoritative source for documentation on each
>     concept.
>
>     Fixed bug in 'saved' filter handling. There was an inconsistency in
>     how the data was accessed at different points that would cause
>     failure. (I do wonder if anyone ever uses saved filters based on
>     this.) There are two choices in how to fix this; I made one but am
>     open to both.
>
>   + New pre-defined filters and an interactive filtering command
>
>     Several new filters are defined by default to handle some very
>     common filtering tasks (e.g., matching filename components since
>     the 'filename' filter matches on the absolute pathname). A new
>     command is offered to select a filter by completion on the
>     descriptions, which is very easy to use without remembering key
>     bindings.
>
>   + Documentation fixes throughout ibuf-ext.el
>
>   + Many new tests and fixed bug in original test.
>
Hi Chris,

thank you very much for your time preparing this patch!
I have some comments.

I)

ibuffer-filter-by-filename-extension

I would call this:
ibuffer-filter-by-extension
or
ibuffer-filter-by-file-extension

II)
*)
ibuffer-filter-by-filename-root

i don't think this deserves a separated keybinding.  Most of
the time you will be well served with
`ibuffer-filter-by-filename-base'.
Actually, I wouldn't introduce `ibuffer-filter-by-filename-root' at all.

You mention somewhere in the patch:
;; This should probably be called pathname but kept for backward compatibility
The word 'filename' is right; in Emacs it's standard to refer as filename to the
_full_ name of the file.

*)
ibuffer-filter-by-filename-base
I would call this:
ibuffer-filter-by-basename

But what i would do, instead of defining this command and binding it
to '/ F' i would define instead:

(define-ibuffer-filter buffer-name
    "Limit current view to buffers with its name matching QUALIFIER."
  (:description "buffer name"
   :reader (read-from-minibuffer
            "Filter by buffer name (regex): "))
  (string-match qualifier (buffer-name buf)))

And i would bind it to '/ b'.
This has the advantage that it would match any buffers not just those
visiting a file on disk.

*)
I like the new command `ibuffer-filter-chosen-by-completion', and
i think your proposal of binding it to '/ TAB' is a good choice; the
other command previously bound to '/ TAB' its also bound to '/ t', so
this change seems for better.

Similar thoughs applies to binding `ibuffer-filter-by-filename-directory'
to '/ /'; this is consistent with `ibuffer-mark-dired-buffers' ('* /').
Your alternative binding for `ibuffer-filter-disable' ('/ DEL')
is easy to remember.

That said, reassign key bindings is usually a matter of concern.
It might be people get used to '/ TAB' and '/ /' standing for their
current bindings.  It must be a consensus before changing any long
standing key bindings.
Alternatively, we could bind `ibuffer-mark-dired-buffers' to '/ d'.

III)
You use a macro from subr-x.el in ibuf-ext.el, so you need to:
(eval-when-compile (require 'subr-x))

IV) There are several trailing white spaces in your patch.

V) Your commit message  don't follow the Emacs standards.  For instance,
instead of:
* lisp/ibuf-ext.el: added paragraph to file commentary
* lisp/ibuf-ext.el (ibuffer-saved-filters): clarified documentation,
  specified customization type, and simplified data format to be
  consistent with `ibuffer-save-filters'
* lisp/ibuf-ext.el (ibuffer-update-saved-filters-format): new function
  that transforms `ibuffer-saved-filters'-style alist format

I should read:
* lisp/ibuf-ext.el: added paragraph to file commentary
(ibuffer-saved-filters): clarified documentation,
specified customization type, and simplified data format to be
consistent with `ibuffer-save-filters'.
(ibuffer-update-saved-filters-format): new function
that transforms `ibuffer-saved-filters'-style alist format.

that is: End sentences with a period.  Write the modified file
just one.

You might want to write NEWS entry for the new features.

VI)
I would change the wording in `ibuffer-included-in-filters-p' doc string.
Instead of
"Does the buffer BUF successfully pass all of the given FILTERS?"
someting like:
"Return non-nil if BUF pass all FILTERS."

VII)
Once you add `ibuffer-and-filter' there is code duplication with
`ibuffer-or-filter'.  I would extract the common code in a new
auxiliar function `ibuffer--or-and-filter' as follows:

(defun ibuffer--or-and-filter (op arg)
  (if arg
      (progn
        (when (or (null ibuffer-filtering-qualifiers)
                  (not (eq op (caar ibuffer-filtering-qualifiers))))
          (error "Top filter is not an %s" (upcase (symbol-name op))))
        (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 %s" (upcase (symbol-name op))))
    ;; If the second filter is an op, just add to it.
    (let ((first (pop ibuffer-filtering-qualifiers))
          (second (pop ibuffer-filtering-qualifiers)))
      (if (eq op (car second))
          (push (nconc (list op first) (cdr second))
                ibuffer-filtering-qualifiers)
        (push (list op first second)
              ibuffer-filtering-qualifiers))))
  (ibuffer-update nil t))

;;;###autoload
(defun ibuffer-or-filter (&optional reverse)
  "Replace the top two filters in this buffer with their logical OR.
If optional argument REVERSE is non-nil, instead break the top OR
filter into parts."
  (interactive "P")
  (ibuffer--or-and-filter 'or reverse))

;;;###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))

IX)
In `ibuffer-filter-by-starred-name' you are matching a buffer name
starting with "*".  That covers all special buffers but it might
add some garbage.  For instance, sometimes i miss-type a new buffer
"*foo", and then i just make a new one "*foo*" without deleting
"*foo".  I prefer if the filter do not show "*foo".
I use the following more paranoid regexp:
"\\`\\*[^*]+\\*\\(<?[[:digit:]]*>?\\)\\'"
This regexp matches "*foo*" and "*foo*<2>" but it doesn't match neither
"*foo" nor "foo*".

X)
>    Fixed bug in 'saved' filter handling. There was an inconsistency in
>    how the data was accessed at different points that would cause
>    failure. (I do wonder if anyone ever uses saved filters based on
>    this.) There are two choices in how to fix this; I made one but am
>    open to both.
Could you create a receipt where the bug cause an actual failure?

Even if there is no failure i agree it looks nicer because you decrease
1 level the nesting, and make `ibuffer-saved-filters' looks similar than
`ibuffer-saved-filter-groups'.  That is an advantage when the user is
writing filters by hand; but usually an user compose the filters from Ibuffer,
and save them with '/ s', so the actual format is an implementation detail.

As you know, we have an implicit 'AND', that is the reason why the original
implemention lack of an explicit `and'.  I don't object to the new format, though.
I agree is more clear when writing filters by hand.

I much prefer if this part of the patch go to a separated bug report.

Cheers,
Tino



^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-11-19 11:17 ` Tino Calancha
@ 2016-11-22 23:45   ` Christopher Genovese
  2016-11-26 10:53     ` Tino Calancha
  0 siblings, 1 reply; 10+ messages in thread
From: Christopher Genovese @ 2016-11-22 23:45 UTC (permalink / raw)
  To: Tino Calancha; +Cc: emacs-devel


[-- Attachment #1.1: Type: text/plain, Size: 16974 bytes --]

Hi Tino,

Thanks so much for your detailed and very helpful comments.

I've made almost all your suggested changes, and for the few
exceptions, I changed the code in the direction I think you
intended.

Below, I give specific responses to each of your points.
Please take a look. There are a few questions/points for
your consideration therein.

I haven't had a chance yet to split the commit to isolate the
saved filters fix, but I will do that tomorrow and submit
appropriate patches and bug reports. (Below, I do describe the
bug more precisely and give an example.)

I still thought it would be useful to respond to your
suggestions now. I've attached a patch file of the most recent
commit on my branch against the current master for reference.

Thanks again for all your feedback and help!

   Regards, Chris


> ibuffer-filter-by-filename-extension
>
> I would call this:
> ibuffer-filter-by-extension
> or
> ibuffer-filter-by-file-extension

I agree that those are simpler. I had gone with the more complicated
name to make clear that also filters on whether the buffer is visiting a
file. But that is also clear with the simpler name.

I've used file-extension to make clear that this applies to file buffers
only. By related reasoning, I've also changed filename-directory and
filename-base to directory and basename; see the note below in
response to your basename comment for how I've handled these in the
revision.


> ibuffer-filter-by-filename-root

I have eliminated this as you suggested.


> You mention somewhere in the patch:
> ;; This should probably be called pathname but kept for backward
compatibility
> The word 'filename' is right...

Good point. I've eliminated this comment.


> ibuffer-filter-by-filename-base
> I would call this:
> ibuffer-filter-by-basename
>
> But what i would do, instead of defining this command and binding it
> to '/ F' i would define instead:

I see your point, but I still think there are good reasons to keep this
one. This filter is useful to prevent inadvertent matching both against
random parts of a file's path and against utility (non-file) buffers
with systematically related names. I think this is a fairly common use
case. (For me, this is one of the filters I use most often
interactively.) Moreover, the buffer name and the buffer file name need
not be the same (e.g., with uniquify/ multiple files of the same name,
or the edge case of explicit renaming).

The buffer name filter you suggest is already available as
ibuffer-filter-by-name (/ n).

Here's what I've done on this:

 1. Add a `ibuffer-filter-by-visiting-file' (/ v) that selects
    buffers that are visiting a file. This is useful in its own
    right (see next item). It also makes '/ n' + '/ v' [that is,
    (and (name . "...") (visiting-file))] almost the same as
    my '/ F', or put another way, makes '/ F' a more precise
    shortcut.

 2. Changed `ibuffer-filter-by-filename-directory' to
    `ibuffer-filter-by-directory' and changed the functionality so that
    in a file buffer it matches against the file's path but in a
    non-file buffer matches against default-directory. This is of
    practical interest and '/ /' + '/ v'
    [that is, (and (directory . "...") (visiting-file))] gives the original
    functionality quite simply.

 3. Keep the `ibuffer-filter-by-basename', making the name
    change you suggested and keeping it on '/ F'.  It does
    no harm here and, I think, adds some value.

Let me know what you think.


> I like the new command `ibuffer-filter-chosen-by-completion', and
> i think your proposal of binding it to '/ TAB' is a good choice; the
> other command previously bound to '/ TAB' its also bound to '/ t', so
> this change seems for better.
>
> Similar thoughs applies to binding `ibuffer-filter-by-filename-directory'
> to '/ /'; this is consistent with `ibuffer-mark-dired-buffers' ('* /').
> Your alternative binding for `ibuffer-filter-disable' ('/ DEL')
> is easy to remember.
>
> That said, reassign key bindings is usually a matter of concern.
> It might be people get used to '/ TAB' and '/ /' standing for their
> current bindings.  It must be a consensus before changing any long
> standing key bindings.

Understood.

I think the new bindings are highly mnemonic and will happily advocate
for them. But the need for consensus makes total sense.

(Note: '/ d' is already bound to ibuffer-decompose-filter or I would have
used it. I felt that the change I made keeps the mnemonic strong with
less overall impact -- what's a better match for decompose?, for instance.)


> You use a macro from subr-x.el in ibuf-ext.el, so you need to:
> (eval-when-compile (require 'subr-x))

Done. Good catch!


> There are several trailing white spaces in your patch.

Fixed.


> Your commit message  don't follow the Emacs standards.

Thanks for spelling that out. I've fixed this on the new
commit rather than amend the old message and modify
the history. If you think more is required here, let
me know.


> You might want to write NEWS entry for the new features.

Done, and included in this commit/patch.


> I would change the wording in `ibuffer-included-in-filters-p' doc string.

Done.


> Once you add `ibuffer-and-filter' there is code duplication...I would
> extract the common code in a new auxiliar function....

Done. I removed some additional code duplication by using
ibuffer-decompose-filter as well and more with the push, while
eliminating unnecessary nesting in the result.


> In `ibuffer-filter-by-starred-name' you are matching a buffer name
> starting with "*".  That covers all special buffers but it might
> add some garbage.

That makes sense. This means thinking of starred buffers as special
entities, in which case you don't want to match '*foo''s. If you want
those, you can filter by name explicitly. I've made the suggested
change.


> Could you create a receipt where the bug cause an actual failure?
> ... I don't object to the new format, though.
> I agree is more clear when writing filters by hand.
>
> I much prefer if this part of the patch go to a separated bug report.

OK, I'll do that. Just for the discussion here, the issue is
at the following point in the *original* `ibuffer-save-filters':

  (ibuffer-aif (assoc name ibuffer-saved-filters)
      (setcdr it filters)
    (push (list name filters) ibuffer-saved-filters))

This treats existing filters (setcdr) and new filters (push)
inconsistently. Using the default value of ibuffer-saved-filters

        (("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)))))

and doing

   (ibuffer-save-filters "foo" '((name . "foo") (derived-mode . text-mode)))
   (ibuffer-save-filters "gnus" '((filename . ".")
                                  (or (derived-mode . prog-mode)
                                      (mode . "compilation-mode"))))

gives the following incorrect value for `ibuffer-saved-filters'

        (("foo"
          ((name . "foo")
           (derived-mode . text-mode)))
         ("gnus"
          (filename . ".")
          (or
           (derived-mode . prog-mode)
           (mode . "compilation-mode")))
         ("programming"
          ((or
            (mode . emacs-lisp-mode)
            (mode . cperl-mode)
            (mode . c-mode)
            (mode . java-mode)
            (mode . idl-mode)
            (mode . lisp-mode)))))

As you can see, the existing entry "gnus" breaks the expected format.
So to be more precise than I was earlier: In addition to the unnecessary
nesting level, this breaks anytime you save to an existing filter. My
change replaces the `list' with a `cons' and replaces various `cadr''s
with `cdr''s, making the two cases consistent and eliminating the extra
nesting.

Tomorrow, I will pull out the saved filter changes and submit a
formal bug report with patches for the two approaches, making
the other ibuffer changes independent. For the moment, to
facilitate discussion, I've included the commit with my previous
approach included and attached the patch. Sorry for the extra
delay on doing the splitting, but I'm on it.



On Sat, Nov 19, 2016 at 6:17 AM, Tino Calancha <tino.calancha@gmail.com>
wrote:

> Christopher Genovese <genovese@cmu.edu> writes:
>
> > I'd like to submit some mild changes in Ibuffer (ibuffer.el, ibuf-ext.el,
> > and ibuffer-tests.el)
>
> > The proposed changes are as follows:
> >
> >   + Compound filters
> >
> >     Add support for 'and' and normalize handling of 'not' to allow the
> >     original "spliced" format as well as a more lispy "sexp" format.
> >
> >     Original documentation for the structure of compound filters was
> >     almost completely lacking. The updated code documents compound
> >     filter structure and clarifies the language used throughout,
> >     providing a single authoritative source for documentation on each
> >     concept.
> >
> >     Fixed bug in 'saved' filter handling. There was an inconsistency in
> >     how the data was accessed at different points that would cause
> >     failure. (I do wonder if anyone ever uses saved filters based on
> >     this.) There are two choices in how to fix this; I made one but am
> >     open to both.
> >
> >   + New pre-defined filters and an interactive filtering command
> >
> >     Several new filters are defined by default to handle some very
> >     common filtering tasks (e.g., matching filename components since
> >     the 'filename' filter matches on the absolute pathname). A new
> >     command is offered to select a filter by completion on the
> >     descriptions, which is very easy to use without remembering key
> >     bindings.
> >
> >   + Documentation fixes throughout ibuf-ext.el
> >
> >   + Many new tests and fixed bug in original test.
> >
> Hi Chris,
>
> thank you very much for your time preparing this patch!
> I have some comments.
>
> I)
>
> ibuffer-filter-by-filename-extension
>
> I would call this:
> ibuffer-filter-by-extension
> or
> ibuffer-filter-by-file-extension
>
> II)
> *)
> ibuffer-filter-by-filename-root
>
> i don't think this deserves a separated keybinding.  Most of
> the time you will be well served with
> `ibuffer-filter-by-filename-base'.
> Actually, I wouldn't introduce `ibuffer-filter-by-filename-root' at all.
>
> You mention somewhere in the patch:
> ;; This should probably be called pathname but kept for backward
> compatibility
> The word 'filename' is right; in Emacs it's standard to refer as filename
> to the
> _full_ name of the file.
>
> *)
> ibuffer-filter-by-filename-base
> I would call this:
> ibuffer-filter-by-basename
>
> But what i would do, instead of defining this command and binding it
> to '/ F' i would define instead:
>
> (define-ibuffer-filter buffer-name
>     "Limit current view to buffers with its name matching QUALIFIER."
>   (:description "buffer name"
>    :reader (read-from-minibuffer
>             "Filter by buffer name (regex): "))
>   (string-match qualifier (buffer-name buf)))
>
> And i would bind it to '/ b'.
> This has the advantage that it would match any buffers not just those
> visiting a file on disk.
>
> *)
> I like the new command `ibuffer-filter-chosen-by-completion', and
> i think your proposal of binding it to '/ TAB' is a good choice; the
> other command previously bound to '/ TAB' its also bound to '/ t', so
> this change seems for better.
>
> Similar thoughs applies to binding `ibuffer-filter-by-filename-directory'
> to '/ /'; this is consistent with `ibuffer-mark-dired-buffers' ('* /').
> Your alternative binding for `ibuffer-filter-disable' ('/ DEL')
> is easy to remember.
>
> That said, reassign key bindings is usually a matter of concern.
> It might be people get used to '/ TAB' and '/ /' standing for their
> current bindings.  It must be a consensus before changing any long
> standing key bindings.
> Alternatively, we could bind `ibuffer-mark-dired-buffers' to '/ d'.
>
> III)
> You use a macro from subr-x.el in ibuf-ext.el, so you need to:
> (eval-when-compile (require 'subr-x))
>
> IV) There are several trailing white spaces in your patch.
>
> V) Your commit message  don't follow the Emacs standards.  For instance,
> instead of:
> * lisp/ibuf-ext.el: added paragraph to file commentary
> * lisp/ibuf-ext.el (ibuffer-saved-filters): clarified documentation,
>   specified customization type, and simplified data format to be
>   consistent with `ibuffer-save-filters'
> * lisp/ibuf-ext.el (ibuffer-update-saved-filters-format): new function
>   that transforms `ibuffer-saved-filters'-style alist format
>
> I should read:
> * lisp/ibuf-ext.el: added paragraph to file commentary
> (ibuffer-saved-filters): clarified documentation,
> specified customization type, and simplified data format to be
> consistent with `ibuffer-save-filters'.
> (ibuffer-update-saved-filters-format): new function
> that transforms `ibuffer-saved-filters'-style alist format.
>
> that is: End sentences with a period.  Write the modified file
> just one.
>
> You might want to write NEWS entry for the new features.
>
> VI)
> I would change the wording in `ibuffer-included-in-filters-p' doc string.
> Instead of
> "Does the buffer BUF successfully pass all of the given FILTERS?"
> someting like:
> "Return non-nil if BUF pass all FILTERS."
>
> VII)
> Once you add `ibuffer-and-filter' there is code duplication with
> `ibuffer-or-filter'.  I would extract the common code in a new
> auxiliar function `ibuffer--or-and-filter' as follows:
>
> (defun ibuffer--or-and-filter (op arg)
>   (if arg
>       (progn
>         (when (or (null ibuffer-filtering-qualifiers)
>                   (not (eq op (caar ibuffer-filtering-qualifiers))))
>           (error "Top filter is not an %s" (upcase (symbol-name op))))
>         (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 %s" (upcase (symbol-name op))))
>     ;; If the second filter is an op, just add to it.
>     (let ((first (pop ibuffer-filtering-qualifiers))
>           (second (pop ibuffer-filtering-qualifiers)))
>       (if (eq op (car second))
>           (push (nconc (list op first) (cdr second))
>                 ibuffer-filtering-qualifiers)
>         (push (list op first second)
>               ibuffer-filtering-qualifiers))))
>   (ibuffer-update nil t))
>
> ;;;###autoload
> (defun ibuffer-or-filter (&optional reverse)
>   "Replace the top two filters in this buffer with their logical OR.
> If optional argument REVERSE is non-nil, instead break the top OR
> filter into parts."
>   (interactive "P")
>   (ibuffer--or-and-filter 'or reverse))
>
> ;;;###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))
>
> IX)
> In `ibuffer-filter-by-starred-name' you are matching a buffer name
> starting with "*".  That covers all special buffers but it might
> add some garbage.  For instance, sometimes i miss-type a new buffer
> "*foo", and then i just make a new one "*foo*" without deleting
> "*foo".  I prefer if the filter do not show "*foo".
> I use the following more paranoid regexp:
> "\\`\\*[^*]+\\*\\(<?[[:digit:]]*>?\\)\\'"
> This regexp matches "*foo*" and "*foo*<2>" but it doesn't match neither
> "*foo" nor "foo*".
>
> X)
> >    Fixed bug in 'saved' filter handling. There was an inconsistency in
> >    how the data was accessed at different points that would cause
> >    failure. (I do wonder if anyone ever uses saved filters based on
> >    this.) There are two choices in how to fix this; I made one but am
> >    open to both.
> Could you create a receipt where the bug cause an actual failure?
>
> Even if there is no failure i agree it looks nicer because you decrease
> 1 level the nesting, and make `ibuffer-saved-filters' looks similar than
> `ibuffer-saved-filter-groups'.  That is an advantage when the user is
> writing filters by hand; but usually an user compose the filters from
> Ibuffer,
> and save them with '/ s', so the actual format is an implementation detail.
>
> As you know, we have an implicit 'AND', that is the reason why the original
> implemention lack of an explicit `and'.  I don't object to the new format,
> though.
> I agree is more clear when writing filters by hand.
>
> I much prefer if this part of the patch go to a separated bug report.
>
> Cheers,
> Tino
>

[-- Attachment #1.2: Type: text/html, Size: 20775 bytes --]

[-- Attachment #2: revised-ibuffer-and-filters.patch --]
[-- Type: text/x-diff, Size: 120185 bytes --]

From a331e8c0820b1c1e5c45c51e26d89cd9ffad6b9d Mon Sep 17 00:00:00 2001
From: "Christopher R. Genovese" <genovese@cmu.edu>
Date: Thu, 17 Nov 2016 00:44:27 -0500
Subject: [PATCH 1/2] Ibuffer improvements: filters, documentation, bug fixes,
 tests

+ Provides compound filter to support explicit logical 'and'

  While current and saved ibuffer filter lists offer an implicit
  logical 'and', it can be useful for defining complex filters
  and filter groups to be able to use 'and' explicitly within a
  filter. Although this could be achieved with DeMorgan's laws
  using 'or' and 'not', or saved filters, both options are
  unnecessarily onerous. Providing an 'and' conveniently
  increases filtering power at negligible cost.

+ Accepts 'not' compound in (not . qualifier), (not qualifier) forms

  The original 'not' compound filter expects the form
  (not . qualifier), e.g., (not size-gt . 100). This adds
  support, at negligible cost, for the alternative, more lispy,
  form like (not (size-gt . 100)) or (not (or ...)). The
  original looks nice with nullary filters like (not modified),
  and the new form is pleasantly consistent with sexp structure
  of 'and' and 'or'.

+ Significant documentation improvements for filtering

  The structure of compound filters had not been documented. The
  new documentation gives an authoritative source for each
  concept and makes the language used throughout more clear and
  consistent (e.g., distinguishing qualifier data from general
  filter specifications).

+ Defines several commonly needed filters

  The existing 'filename' matches against the full pathname of
  the buffer's file. This can be inconvenient for precisely
  filtering files, so several new filters are pre-defined to
  match particular pathname components. In addition, convenient
  nullary filters for starred and modified buffers are provided.

+ Fixes bug in `ibuffer-save-filters'

  The structure of `ibuffer-saved-filters' and
  `ibuffer-save-filter' were inconsistent, with the former
  having an extra list level in each alist element. This version
  fixes the inconsistency by simplifying `ibuffer-saved-filters'
  to remove the extra list level and automatically checks to
  repair existing formats. (Alternatively, the access code could
  be special-cased leaving the variable format intact. This
  alternative would arguably be lower impact, but the change
  made seemed asthetically nicer.)

+ Defines completion-based interactive filtering command

  New command to select a filter by completion on filter
  descriptions. Easy to use and bound to /-TAB mnemonically.

+ Fixes small bug in original test

  The one original test failed unexpectedly if ibuf-ext were
  loaded.

+ Adds a substantial number of additional tests with feature ibuf-ext

  Many new tests in ert, leaving the environment untouched,
  cover most aspects of filtering, old and new.

+ Makes a few mnemonic changes to default filtering part of keymap

These changs are mostly quite small but distributed across
several functions and docstrings. See the change log below.

Change Log: 2016-11-16	Christopher R. Genovese	 <genovese@cmu.edu>

* lisp/ibuf-ext.el: added paragraph to file commentary
* lisp/ibuf-ext.el (ibuffer-saved-filters): clarified documentation,
  specified customization type, and simplified data format to be
  consistent with `ibuffer-save-filters'
* lisp/ibuf-ext.el (ibuffer-update-saved-filters-format): new function
  that transforms `ibuffer-saved-filters'-style alist format
* lisp/ibuf-ext.el (ibuffer-repair-saved-filters): new function that
  transforms `ibuffer-saved-filters' to new format if needed
* lisp/ibuf-ext.el (ibuffer-filtering-qualifiers): new documentation
  is the authoritative source for filter specification format
* lisp/ibuf-ext.el (ibuffer-filter-groups): new documentation
  clarifies filter group structure and role
* lisp/ibuf-ext.el (ibuffer-unary-operand): new function transparently
  handles not formats for compound filters
* lisp/ibuf-ext.el (ibuffer-included-in-filter-p): new docstring
  and now handles 'not' fully
* lisp/ibuf-ext.el (ibuffer-included-in-filter-p-1): handles 'and'
  compound filters and consistent handling of 'saved' filter data
* lisp/ibuf-ext.el (ibuffer-decompose-filter): handles 'and' as well,
  made handling of 'saved' filter data and 'not' consistent with
  other uses
* lisp/ibuf-ext.el (ibuffer-and-filter): new function analogous to
  `ibuffer-or-filter' for completeness
* lisp/ibuf-ext.el (ibuffer-maybe-save-stuff): handle 'saved' filter
  data consistently with other uses
* lisp/ibuf-ext.el (ibuffer-format-qualifier): handle 'and' filters
* lisp/ibuf-ext.el (ibuffer-filter-by-*): new pre-defined filters
  filename-base, filename-extension, filename-directory,
  filename-root, starred-name, and modified
* lisp/ibuf-ext.el (ibuffer-filter-chosen-by-completion): new
  interactive command for easily choosing a filter
* lisp/ibuf-ext.el: many small improvements throughout to docstrings,
  variable naming, and spacing
* lisp/ibuffer.el: keymap and menu additions/changes for filtering
* test/lisp/ibuffer-tests.el (ibuffer-autoload): added appropriate
  skip specification
* test/lisp/ibuffer-tests.el (ibuffer-*): many additional tests
  that are skipped unless ibuf-ext is loaded.
---
 lisp/ibuf-ext.el           | 492 +++++++++++++++++++++++++++++++--------
 lisp/ibuffer.el            |  67 +++++-
 test/lisp/ibuffer-tests.el | 565 ++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 1021 insertions(+), 103 deletions(-)

diff --git a/lisp/ibuf-ext.el b/lisp/ibuf-ext.el
index b3d1452..a9b337b 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:
 
@@ -37,7 +44,9 @@
   (require 'ibuf-macs)
   (require 'cl-lib))
 
+
 ;;; Utility functions
+
 (defun ibuffer-delete-alist (key alist)
   "Delete all entries in ALIST that have a key equal to KEY."
   (let (entry)
@@ -119,35 +128,177 @@ Buffers whose major mode is in this list, are not searched."
 
 (defvar ibuffer-auto-buffers-changed nil)
 
-(defcustom ibuffer-saved-filters '(("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)))))
-
-  "An alist of filter qualifiers 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'.
-See also the variables `ibuffer-filtering-qualifiers',
-`ibuffer-filtering-alist', and the functions
-`ibuffer-switch-to-saved-filters', `ibuffer-save-filters'."
-  :type '(repeat sexp)
+(defun ibuffer-update-saved-filters-format (filters)
+  "Transforms alist from old to new `ibuffer-saved-filters' format.
+
+Specifically, converts old-format alist with values of the
+form (STRING (FILTER-SPECS...)) to alist with values of the
+form (STRING FILTER-SPECS...), where each filter spec should be a
+cons cell with a symbol in the car. Any elements in the latter
+form are kept as is.
+
+Returns (OLD-FORMAT-DETECTED? . UPDATED-SAVED-FILTERS-LIST)."
+  (when filters
+    (let* ((old-format-detected nil)
+           (fix-filter (lambda (filter-spec)
+                         (if (symbolp (car (cadr filter-spec)))
+                             filter-spec
+                           (setq old-format-detected t) ; side-effect
+                           (cons (car filter-spec) (cadr filter-spec)))))
+           (fixed (mapcar fix-filter filters)))
+      (cons old-format-detected fixed))))
+
+(defcustom ibuffer-saved-filters '(("programming"
+                                    (or (derived-mode . prog-mode)
+                                        (mode         . ess-mode)
+                                        (mode         . compilation-mode)))
+                                   ("text document"
+                                    (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))))
+
+  "An alist mapping saved filter names to filter specifications.
+
+Each element should look like (\"NAME\" . FILTER-LIST), where
+FILTER-LIST has the same structure as the variable
+`ibuffer-filtering-qualifiers', which see. The filters defined
+here are joined with an implicit logical `and' and associated
+with NAME. The combined specification can be used by name in
+other filter specifications via the `saved' qualifier (again, see
+`ibuffer-filtering-qualifiers'). They can also be switched to by
+name (see the functions `ibuffer-switch-to-saved-filters' and
+`ibuffer-save-filters'). The variable `ibuffer-save-with-custom'
+affects how this information is saved for future sessions. This
+variable can be set directly from lisp code."
+  :type '(alist :key-type (string :tag "Filter name")
+                :value-type (repeat :tag "Filter specification" sexp))
+  :set (lambda (symbol value)
+         ;; Just set-default but update legacy old-style format
+         (set-default symbol (cdr (ibuffer-update-saved-filters-format value))))
   :group 'ibuffer)
 
+(defvar ibuffer-old-saved-filters-warning
+  (concat "Deprecated format detected for variable `ibuffer-saved-filters'.
+
+The format has been repaired and the variable modified accordingly. 
+You can save the current value through the customize system by
+either clicking or hitting return "
+            (make-text-button
+             "here" nil
+             'face '(:weight bold :inherit button)
+             'mouse-face '(:weight normal :background "gray50" :inherit button)
+             'follow-link t
+             'help-echo "Click or RET: save new value in customize"
+             'action (lambda (b)
+                       (if (not (fboundp 'customize-save-variable))
+                           (message "Customize not available; value not saved")
+                         (customize-save-variable 'ibuffer-saved-filters
+                                                  ibuffer-saved-filters)
+                         (message "Saved updated ibuffer-saved-filters."))))
+            ". See below for
+an explanation and alternative ways to save the repaired value.
+
+Explanation: For the list variable `ibuffer-saved-filters',               
+elements of the form (STRING (FILTER-SPECS...)) are deprecated
+and should instead have the form (STRING FILTER-SPECS...), where
+each filter spec is a cons cell with a symbol in the car. See
+`ibuffer-saved-filters' for details. The repaired value fixes 
+this format without changing the meaning of the saved filters. 
+
+Alternative ways to save the repaired value:
+
+  1. Do M-x customize-variable and entering `ibuffer-saved-filters' 
+     when prompted. 
+
+  2. Set the updated value manually by copying the 
+     following emacs-lisp form to your emacs init file.
+
+%s
+"))
+
+(defun ibuffer-repair-saved-filters ()
+  "Updates `ibuffer-saved-filters' to its new-style format, if needed.
+
+If this list has any elements of the old-style format, a
+deprecation warning is raised, with a button allowing persistent
+update. Any updated filters retain their meaning in the new
+format. See `ibuffer-update-saved-filters-format' and
+`ibuffer-saved-filters' for details of the old and new formats."
+  (when (and (boundp 'ibuffer-saved-filters) ibuffer-saved-filters)
+    (let ((fixed (ibuffer-update-saved-filters-format ibuffer-saved-filters)))
+      (prog1
+          (setq ibuffer-saved-filters (cdr fixed))
+        (when-let (old-format-detected? (car fixed))
+          (let ((warning-series t)
+                (updated-form
+                 (with-output-to-string
+                   (pp `(setq ibuffer-saved-filters ',ibuffer-saved-filters)))))
+            (display-warning
+             'ibuffer
+             (format ibuffer-old-saved-filters-warning updated-form))))))))
+
 (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
@@ -179,10 +330,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."
@@ -192,20 +351,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)
 
@@ -512,18 +672,38 @@ To evaluate a form without viewing the buffer, see `ibuffer-do-eval'."
 
 ;;;###autoload
 (defun ibuffer-included-in-filters-p (buf filters)
+  "Does the buffer BUF successfully pass all of the given 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)
+  "Does the buffer BUF successfully 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)))
+        ;; ATTN: Allows (not (not ...)) etc. Is fixing this worthwhile?
+        (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)
@@ -531,17 +711,25 @@ 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
-	   (ibuffer-filter-disable t)
-	   (error "Unknown saved filter %s" (cdr filter)))
-	 (ibuffer-included-in-filters-p buf (cadr data))))
+       (let ((data (assoc (cdr filter) ibuffer-saved-filters)))
+         (unless data
+           (ibuffer-filter-disable t)
+           (error "Unknown saved filter %s" (cdr filter)))
+         (ibuffer-included-in-filters-p buf (cdr data))))
       (_
        (pcase-let ((`(,_type ,_desc ,func)
                     (assq (car filter) ibuffer-filtering-alist)))
@@ -828,39 +1016,36 @@ 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)
   (when (null ibuffer-filtering-qualifiers)
     (error "No filters in effect"))
   (let ((lim (pop ibuffer-filtering-qualifiers)))
     (pcase (car lim)
-      (`or
+      ((or 'or 'and)
        (setq ibuffer-filtering-qualifiers (append
-					  (cdr lim)
-					  ibuffer-filtering-qualifiers)))
+                                           (cdr lim)
+                                           ibuffer-filtering-qualifiers)))
       (`saved
-       (let ((data
-	      (assoc (cdr lim)
-		     ibuffer-saved-filters)))
-	 (unless data
-	   (ibuffer-filter-disable)
-	   (error "Unknown saved filter %s" (cdr lim)))
-	 (setq ibuffer-filtering-qualifiers (append
-					    (cadr data)
-					    ibuffer-filtering-qualifiers))))
+       (let ((data (assoc (cdr lim) ibuffer-saved-filters)))
+         (unless data
+           (ibuffer-filter-disable)
+           (error "Unknown saved filter %s" (cdr lim)))
+         (setq ibuffer-filtering-qualifiers (append
+                                             (cdr data)
+                                             ibuffer-filtering-qualifiers))))
       (`not
-       (push (cdr lim)
-	     ibuffer-filtering-qualifiers))
+       (push (ibuffer-unary-operand lim) ibuffer-filtering-qualifiers))
       (_
        (error "Filter type %s is not compound" (car lim)))))
   (ibuffer-update nil t))
@@ -892,12 +1077,12 @@ turned into two separate filters [name: foo] and [mode: bar-mode]."
   (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
+  (if decompose
       (progn
 	(when (or (null ibuffer-filtering-qualifiers)
 		  (not (eq 'or (caar ibuffer-filtering-qualifiers))))
@@ -917,6 +1102,32 @@ filter into parts."
 	      ibuffer-filtering-qualifiers))))
   (ibuffer-update nil t))
 
+;;;###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")
+  (if decompose
+      (progn
+        (when (or (null ibuffer-filtering-qualifiers)
+                  (not (eq 'and (caar ibuffer-filtering-qualifiers))))
+          (error "Top filter is not an AND"))
+        (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 AND"))
+    ;; If the second filter is an AND, just add to it.
+    (let ((first (pop ibuffer-filtering-qualifiers))
+          (second (pop ibuffer-filtering-qualifiers)))
+      (if (eq 'and (car second))
+          (push (nconc (list 'and first) (cdr second))
+                ibuffer-filtering-qualifiers)
+        (push (list 'and first second)
+              ibuffer-filtering-qualifiers))))
+  (ibuffer-update nil t))
+
 (defun ibuffer-maybe-save-stuff ()
   (when ibuffer-save-with-custom
     (if (fboundp 'customize-save-variable)
@@ -939,7 +1150,7 @@ Interactively, prompt for NAME, and use the current filters."
       ibuffer-filtering-qualifiers)))
   (ibuffer-aif (assoc name ibuffer-saved-filters)
       (setcdr it filters)
-    (push (list name filters) ibuffer-saved-filters))
+    (push (cons name filters) ibuffer-saved-filters))
   (ibuffer-maybe-save-stuff))
 
 ;;;###autoload
@@ -989,7 +1200,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)
@@ -998,14 +1211,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."
@@ -1023,7 +1238,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))
@@ -1043,7 +1258,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"
@@ -1062,7 +1277,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
@@ -1073,22 +1288,83 @@ 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 with *."
+  (:description "starred buffer name"
+   :reader nil)
+  (string-match "\\`*" (buffer-name buf)))
+
+;; This should probably be called pathname but kept for backward compatibility
 ;;;###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): "))
+(define-ibuffer-filter filename  
+  "Limit current view to buffers with full file pathname matching QUALIFIER.
+
+For example, for a buffer associated with file '/a/b/c.d', this
+matches against '/a/b/c.d'."
+  (:description "file pathname"
+   :reader (read-from-minibuffer "Filter by file pathname (regexp): "))
   (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
     (string-match qualifier it)))
 
+;; If filename above were renamed to pathname, this could be called filename.
+;;;###autoload (autoload 'ibuffer-filter-by-filename-base "ibuf-ext")
+(define-ibuffer-filter filename-base 
+    "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-filename-extension "ibuf-ext")
+(define-ibuffer-filter filename-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-filename-root "ibuf-ext")
+(define-ibuffer-filter filename-root
+    "Limit current view to buffers with file basename matching QUALIFIER.
+
+The filename root is the part of the full pathname of the file without
+the directory or extension/suffix components. For example, for a buffer
+associated with file '/a/b/c.d', this matches against 'c'."
+  (:description "filename root"
+   :reader (read-from-minibuffer "Filter by filename root (regex): "))
+  (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
+    (string-match qualifier (file-name-base it))))
+
+;;;###autoload (autoload 'ibuffer-filter-by-filename-directory "ibuf-ext")
+(define-ibuffer-filter filename-directory
+    "Limit current view to buffers with filename directory matching QUALIFIER.
+
+For example, for a buffer associated with file '/a/b/c.d', this
+matches against '/a/b'."
+  (:description "directory name"
+   :reader (read-from-minibuffer "Filter by directory name (regex): "))
+  (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
+    (let ((dirname (file-name-directory it)))
+      (when dirname (string-match qualifier dirname)))))
+
 ;;;###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: ")))
@@ -1097,16 +1373,23 @@ 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-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
@@ -1116,12 +1399,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 b33c2e3..181a01c 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -518,28 +518,40 @@ 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 "/ F") 'ibuffer-filter-by-filename-base)
+    (define-key map (kbd "/ .") 'ibuffer-filter-by-filename-extension)
+    (define-key map (kbd "/ r") 'ibuffer-filter-by-filename-root)
+    (define-key map (kbd "/ /") 'ibuffer-filter-by-filename-directory)
     (define-key map (kbd "/ <") 'ibuffer-filter-by-size-lt)
-    (define-key map (kbd "/ r") 'ibuffer-switch-to-saved-filters)
+    (define-key map (kbd "/ >") 'ibuffer-filter-by-size-gt)
+    (define-key map (kbd "/ i") 'ibuffer-filter-by-modified)
+    (define-key map (kbd "/ c") 'ibuffer-filter-by-content)
+    (define-key map (kbd "/ e") 'ibuffer-filter-by-predicate)
+    (define-key map (kbd "/ TAB") 'ibuffer-filter-chosen-by-completion)
+
+    (define-key map (kbd "/ w") '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 "/ <up>") '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-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
-    (define-key map (kbd "/ /") 'ibuffer-filter-disable)
+    (define-key map (kbd "/ DEL") 'ibuffer-filter-disable)
 
     (define-key map (kbd "M-n") 'ibuffer-forward-filter-group)
     (define-key map "\t" 'ibuffer-forward-filter-group)
@@ -647,29 +659,62 @@ directory, like `default-directory'."
     (define-key-after map [menu-bar view filter filter-disable]
       '(menu-item "Disable all filtering" ibuffer-filter-disable
         :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers)))
+
     (define-key-after map [menu-bar view filter filter-by-mode]
       '(menu-item "Add filter by any major mode..." ibuffer-filter-by-mode))
     (define-key-after map [menu-bar view filter filter-by-used-mode]
       '(menu-item "Add filter by a major mode in use..."
         ibuffer-filter-by-used-mode))
     (define-key-after map [menu-bar view filter filter-by-derived-mode]
-      '(menu-item "Add filter by derived mode..."
+      '(menu-item "Add filter by derived mode..." 
                   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 pathname..." 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-filename-base]
+      '(menu-item "Add filter by file basename..."
+                  ibuffer-filter-by-filename-base
+                  :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-filename-extension]
+      '(menu-item "Add filter by filename extension..."
+                  ibuffer-filter-by-filename-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-filename-root]
+      '(menu-item "Add filter by filename root..."
+                  ibuffer-filter-by-filename-root
+                  :help (concat "For a buffer associated with file '/a/b/c.d', "
+                                "list buffer if a given pattern matches 'c'")))
+    (define-key-after map [menu-bar view filter filter-by-filename-directory]
+      '(menu-item "Add filter by filename's directory..."
+                  ibuffer-filter-by-filename-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-content]
       '(menu-item "Add filter by content (regexp)..."
         ibuffer-filter-by-content))
     (define-key-after map [menu-bar view filter filter-by-predicate]
       '(menu-item "Add filter by Lisp predicate..."
         ibuffer-filter-by-predicate))
+
     (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)))
@@ -682,6 +727,12 @@ directory, like `default-directory'."
     (define-key-after map [menu-bar view filter negate-filter]
       '(menu-item "Negate top filter" ibuffer-negate-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 decompose-filter]
       '(menu-item "Decompose top filter" ibuffer-decompose-filter
         :enable (and (featurep 'ibuf-ext)
diff --git a/test/lisp/ibuffer-tests.el b/test/lisp/ibuffer-tests.el
index de281c0..aa06994 100644
--- a/test/lisp/ibuffer-tests.el
+++ b/test/lisp/ibuffer-tests.el
@@ -22,7 +22,8 @@
 (require 'ibuffer)
 
 (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
@@ -30,5 +31,567 @@
     (symbol-function
      'ibuffer-mark-unsaved-buffers))))
 
+;; 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 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)
+                              (filename-base . "\\`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 (filename-base . "\\`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))
+                              (filename-base . "\\`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")))
+          (message "--> %s" buf)
+          (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)))))
+          (message "--> %s" buf))
+      (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 '((filename-base . "ibuf-test-3"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((filename-root . "ibuf-test-3"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((filename-base . "test-3\\.a"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA '((filename-extension . "a"))))
+          (should (ibuffer-included-in-filters-p
+                   bufA (list (cons 'filename-directory dirA))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB '((filename-base . "ibuf-test-3"))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB '((filename-root . "ibuf-test-3"))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB '((filename-extension . "b"))))
+          (should-not (ibuffer-included-in-filters-p
+                       bufB (list (cons 'filename-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 '((filename-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 (filename-extension . "el")
+                              (derived-mode . prog-mode)
+                              (not modified)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((or (filename-extension . "tex")
+                             (derived-mode . prog-mode)
+                             (modified)))))
+          (should (ibuffer-included-in-filters-p
+                   buf '((filename-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 '((filename-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
+                        "<HTML><BODY><H1>Hello, World!</H1></BODY></HTML>")))
+          (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 on a single buffer."
+    (skip-unless (featurep 'ibuf-ext))
+    (unwind-protect
+        (let ((buf
+               (funcall create-non-file-buffer "ibuf-test-8"
+                        :mode #'artist-mode)))
+          (should (ibuffer-included-in-filters-p
+                   buf '((and (not (starred-name))
+                              (name . "test-8")
+                              (not (size-gt . 100))
+                              (mode . picture-mode))))))
+      (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))]))
+              (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)))))))
+      (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))]))
+              (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)))))))
+      (funcall clean-up))))
+
+(ert-deftest ibuffer-save-filters ()
+  "Tests that `ibuffer-save-filters' saves in the proper format."
+  (skip-unless (featurep 'ibuf-ext))
+  (let ((ibuffer-save-with-custom nil)
+        (ibuffer-saved-filters nil)
+        (test1 '((mode . org-mode)
+                 (or (size-gt . 10000)
+                     (and (not (starred-name))
+                          (filename-directory . "\<org\>")))))
+        (test2 '((or (mode . emacs-lisp-mode) (filename-extension . "elc?")
+                     (and (starred-name) (name . "elisp"))
+                     (mode . lisp-interaction-mode))))
+        (test3 '((size-lt . 100) (derived-mode . prog-mode)
+                 (or (filename-root . "scratch")
+                     (filename-root . "bonz")
+                     (filename-root . "temp")))))
+    (ibuffer-save-filters "test1" test1)
+    (should (equal (car ibuffer-saved-filters) (cons "test1" test1)))
+    (ibuffer-save-filters "test2" test2)
+    (should (equal (car ibuffer-saved-filters) (cons "test2" test2)))
+    (should (equal (cadr ibuffer-saved-filters) (cons "test1" test1)))
+    (ibuffer-save-filters "test3" test3)
+    (should (equal (car ibuffer-saved-filters) (cons "test3" test3)))
+    (should (equal (cadr ibuffer-saved-filters) (cons "test2" test2)))
+    (should (equal (car (cddr ibuffer-saved-filters)) (cons "test1" test1)))
+    (should (equal (cdr (assoc "test1" ibuffer-saved-filters)) test1))
+    (should (equal (cdr (assoc "test2" ibuffer-saved-filters)) test2))
+    (should (equal (cdr (assoc "test3" ibuffer-saved-filters)) test3))))
+
+(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))
+                         (filename-directory . "\\<org\\>"))))
+        (test5 '(or (filename-root . "scratch")
+                    (filename-root . "bonz")
+                    (filename-root . "temp")))
+        (test6 '(or (mode . emacs-lisp-mode) (filename-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 'filename-directory)
+                                              ": " "\\<org\\>")))))
+    (should (equal (ibuffer-format-qualifier test5)
+                   (funcall tag "OR"
+                            (funcall tag (funcall description 'filename-root)
+                                     ": "  "scratch")
+                            (funcall tag (funcall description 'filename-root)
+                                     ": " "bonz")
+                            (funcall tag (funcall description 'filename-root)
+                                     ": " "temp"))))
+    (should (equal (ibuffer-format-qualifier test6)
+                   (funcall tag "OR"
+                            (funcall tag (funcall description 'mode)
+                                     ": " "emacs-lisp-mode")
+                            (funcall tag (funcall description 'filename-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
-- 
2.10.0


From 1f6e42b641993bf484be43ad4e9b4946bd20e60a Mon Sep 17 00:00:00 2001
From: "Christopher R. Genovese" <genovese@cmu.edu>
Date: Tue, 22 Nov 2016 11:24:50 -0500
Subject: [PATCH 2/2] Further fixes and improvements in response to emacs-devel
 comments

These include:

  + Renaming new filename filters

       filename-base      -> basename
       filename-extension -> file-extension
       filename-directory -> directory

    and eliminating filename-root.

    The directory filter now matches the filename's directory
    component in file buffers and default-directory in non-file
    buffers.

  + Added new pre-defined filter visiting-file bound to '/ v'

  + Restored '/ r' binding to its original state, '/ w' removed

  + Required 'subr-x in eval-when-compile

  + Added NEWS entry for the new user-focused features

  + Improved coding of ibuffer-and-filter and ibuffer-or-filter

  + Robustified regex for starred-name. It now matches
    buffers in emacs "starred" style  *name* or *name*<digit>
    but not *name alone.

  + Added some new tests. All tests pass on a fresh build of emacs.
---
 etc/NEWS                   |  86 ++++++++++++++++
 lisp/ibuf-ext.el           | 176 ++++++++++++++------------------
 lisp/ibuffer.el            |  35 ++++---
 test/lisp/ibuffer-tests.el | 246 ++++++++++++++++++++++++++++++++++-----------
 4 files changed, 369 insertions(+), 174 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 15c264f..df38f96 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -283,6 +283,92 @@ the file's actual content before prompting the user.
 ** Ibuffer
 
 ---
+*** A new filter command 'ibuffer-filter-by-basename'
+(with qualifier 'basename' in filter data) matches in a
+file buffer against the file base name (analogous to what
+'file-name-nondirectory' returns); bound to '/ b'.
+
+---
+*** A new filter command 'ibuffer-filter-by-file-extension'
+(with qualifier 'file-extension' in filter data) matches
+against the file name's extension without the separator
+in a file buffer; bound to '/ .'.
+
+--- 
+*** A new filter command 'ibuffer-filter-by-directory'
+(with qualifier 'directory' in filter data) matches
+against the filename's directory component in a file
+buffer and against 'default-directory' otherwise;
+bound to '/ /'.
+
+---
+*** A new filter command 'ibuffer-filter-by-starred-name'
+(with qualifier 'starred-name' in filter data) matches
+buffers whose names begin and end with *, along with an
+optional suffix of the form digits or <digits>;
+bound to '/ *'.
+
+---
+*** A new filter command 'ibuffer-filter-by-modified'
+(with qualifier 'modified' in filter data) matches buffers
+that are marked modified; bound to '/ i'.
+
+---
+*** A new filter command 'ibuffer-filter-by-visiting-file'
+(with tag 'visiting-file' in filter data) matches buffers
+that are visiting files; bound to '/ v'.
+
+---
+*** A new command 'ibuffer-filter-chosen-by-completion' to
+select and apply a filter interactively by completion on
+the filter description; bound to '/ TAB'.
+
+---
+*** The data format specifying filters has been extended
+to allow i. explicit logical 'and' of other filters and
+ii. a more flexible form for logical 'not' of a
+filter. This is useful for constructing complex filters
+and filter groups, especially when doing so manually. See
+documentation for 'ibuffer-filtering-qualifiers' for full
+details.
+
+---
+*** A new command 'ibuffer-and-filter' that replaces the
+top two filters on the filter stack with their logical
+'and' as a single filter; bound to '/ &'.
+
+---
+*** The command 'ibuffer-or-filter' is bound to '/ |' as
+well as the original binding '/ o'.
+
+---
+*** The command 'ibuffer-filter-disable' is now bound
+to '/ DEL' instead of '/ /'.
+
+---
+*** The commands 'ibuffer-pop-filter' and
+'ibuffer-pop-filter-group' now have alternative key
+bindings '/ <up>' and '/ S-<up>', respectively.  These
+commands continue to be bound to '/ p' and '/ P',
+respectively, as well.
+
+---
+*** The command 'ibuffer-exchange-filter' continues
+to be bound to '/ t' but is no longer bound to '/ TAB'.
+
+---
+*** The format of 'ibuffer-saved-filters' has been
+simplified slightly, removing an unnecessary level of
+parentheses around the filter list. See documentation for
+the variable 'ibuffer-saved-filters' and the function
+'ibuffer-update-saved-filters-format' for details of the
+new and old formats. Filters saved through the customize
+mechanism (the default) are updated automatically; those
+who set the saved filters manually can run the new command
+'ibuffer-repair-saved-filters' to check the variable's
+format and easily update it if necessary.
+
+---
 *** A new command 'ibuffer-copy-buffername-as-kill'; bound
 to 'B'.
 
diff --git a/lisp/ibuf-ext.el b/lisp/ibuf-ext.el
index a9b337b..da13674 100644
--- a/lisp/ibuf-ext.el
+++ b/lisp/ibuf-ext.el
@@ -42,7 +42,8 @@
 
 (eval-when-compile
   (require 'ibuf-macs)
-  (require 'cl-lib))
+  (require 'cl-lib)
+  (require 'subr-x))
 
 
 ;;; Utility functions
@@ -199,37 +200,37 @@ variable can be set directly from lisp code."
 (defvar ibuffer-old-saved-filters-warning
   (concat "Deprecated format detected for variable `ibuffer-saved-filters'.
 
-The format has been repaired and the variable modified accordingly. 
+The format has been repaired and the variable modified accordingly.
 You can save the current value through the customize system by
 either clicking or hitting return "
-            (make-text-button
-             "here" nil
-             'face '(:weight bold :inherit button)
-             'mouse-face '(:weight normal :background "gray50" :inherit button)
-             'follow-link t
-             'help-echo "Click or RET: save new value in customize"
-             'action (lambda (b)
-                       (if (not (fboundp 'customize-save-variable))
-                           (message "Customize not available; value not saved")
-                         (customize-save-variable 'ibuffer-saved-filters
-                                                  ibuffer-saved-filters)
-                         (message "Saved updated ibuffer-saved-filters."))))
-            ". See below for
+          (make-text-button
+           "here" nil
+           'face '(:weight bold :inherit button)
+           'mouse-face '(:weight normal :background "gray50" :inherit button)
+           'follow-link t
+           'help-echo "Click or RET: save new value in customize"
+           'action (lambda (b)
+                     (if (not (fboundp 'customize-save-variable))
+                         (message "Customize not available; value not saved")
+                       (customize-save-variable 'ibuffer-saved-filters
+                                                ibuffer-saved-filters)
+                       (message "Saved updated ibuffer-saved-filters."))))
+          ". See below for
 an explanation and alternative ways to save the repaired value.
 
-Explanation: For the list variable `ibuffer-saved-filters',               
+Explanation: For the list variable `ibuffer-saved-filters',
 elements of the form (STRING (FILTER-SPECS...)) are deprecated
 and should instead have the form (STRING FILTER-SPECS...), where
 each filter spec is a cons cell with a symbol in the car. See
-`ibuffer-saved-filters' for details. The repaired value fixes 
-this format without changing the meaning of the saved filters. 
+`ibuffer-saved-filters' for details. The repaired value fixes
+this format without changing the meaning of the saved filters.
 
 Alternative ways to save the repaired value:
 
-  1. Do M-x customize-variable and entering `ibuffer-saved-filters' 
-     when prompted. 
+  1. Do M-x customize-variable and entering `ibuffer-saved-filters'
+     when prompted.
 
-  2. Set the updated value manually by copying the 
+  2. Set the updated value manually by copying the
      following emacs-lisp form to your emacs init file.
 
 %s
@@ -243,6 +244,7 @@ deprecation warning is raised, with a button allowing persistent
 update. Any updated filters retain their meaning in the new
 format. See `ibuffer-update-saved-filters-format' and
 `ibuffer-saved-filters' for details of the old and new formats."
+  (interactive)
   (when (and (boundp 'ibuffer-saved-filters) ibuffer-saved-filters)
     (let ((fixed (ibuffer-update-saved-filters-format ibuffer-saved-filters)))
       (prog1
@@ -672,7 +674,7 @@ To evaluate a form without viewing the buffer, see `ibuffer-do-eval'."
 
 ;;;###autoload
 (defun ibuffer-included-in-filters-p (buf filters)
-  "Does the buffer BUF successfully pass all of the given FILTERS?
+  "Returns non-nil if buffer BUF passes all FILTERS.
 
 BUF is a lisp buffer object, and FILTERS is a list of filter
 specifications with the same structure as
@@ -700,8 +702,8 @@ specification, with the same structure as an element of the list
 `ibuffer-filtering-qualifiers'."
   (if (eq (car filter) 'not)
       (let ((inner (ibuffer-unary-operand filter)))
-        ;; ATTN: Allows (not (not ...)) etc. Is fixing this worthwhile?
-        (if (eq (car inner) 'not) 
+        ;; 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)))
@@ -1033,17 +1035,15 @@ turned into separate filters, like [name: foo] and [mode: bar-mode]."
   (let ((lim (pop ibuffer-filtering-qualifiers)))
     (pcase (car lim)
       ((or 'or 'and)
-       (setq ibuffer-filtering-qualifiers (append
-                                           (cdr lim)
-                                           ibuffer-filtering-qualifiers)))
+       (setq ibuffer-filtering-qualifiers
+             (nconc (cdr lim) ibuffer-filtering-qualifiers)))
       (`saved
        (let ((data (assoc (cdr lim) ibuffer-saved-filters)))
          (unless data
            (ibuffer-filter-disable)
            (error "Unknown saved filter %s" (cdr lim)))
-         (setq ibuffer-filtering-qualifiers (append
-                                             (cdr data)
-                                             ibuffer-filtering-qualifiers))))
+         (setq ibuffer-filtering-qualifiers
+               (append (cdr data) ibuffer-filtering-qualifiers))))
       (`not
        (push (ibuffer-unary-operand lim) ibuffer-filtering-qualifiers))
       (_
@@ -1076,31 +1076,28 @@ turned into separate filters, like [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 decompose)
   "Replace the top two filters in this buffer with their logical OR.
 If optional argument DECOMPOSE is non-nil, instead break the top OR
 filter into parts."
   (interactive "P")
-  (if decompose
-      (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)
@@ -1108,25 +1105,7 @@ filter into parts."
 If optional argument DECOMPOSE is non-nil, instead break the top AND
 filter into parts."
   (interactive "P")
-  (if decompose
-      (progn
-        (when (or (null ibuffer-filtering-qualifiers)
-                  (not (eq 'and (caar ibuffer-filtering-qualifiers))))
-          (error "Top filter is not an AND"))
-        (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 AND"))
-    ;; If the second filter is an AND, just add to it.
-    (let ((first (pop ibuffer-filtering-qualifiers))
-          (second (pop ibuffer-filtering-qualifiers)))
-      (if (eq 'and (car second))
-          (push (nconc (list 'and first) (cdr second))
-                ibuffer-filtering-qualifiers)
-        (push (list 'and first second)
-              ibuffer-filtering-qualifiers))))
-  (ibuffer-update nil t))
+  (ibuffer--or-and-filter 'and decompose))
 
 (defun ibuffer-maybe-save-stuff ()
   (when ibuffer-save-with-custom
@@ -1295,15 +1274,16 @@ currently used by buffers."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-starred-name "ibuf-ext")
 (define-ibuffer-filter starred-name
-    "Limit current view to buffers with name beginning with *."
+    "Limit current view to buffers with name beginning and ending
+with *, along with an optional suffix of the form digits or
+<digits>."
   (:description "starred buffer name"
    :reader nil)
-  (string-match "\\`*" (buffer-name buf)))
+  (string-match "\\`\\*[^*]+\\*\\(?:<[[:digit:]]+>\\)?\\'" (buffer-name buf)))
 
-;; This should probably be called pathname but kept for backward compatibility
 ;;;###autoload (autoload 'ibuffer-filter-by-filename "ibuf-ext")
-(define-ibuffer-filter filename  
-  "Limit current view to buffers with full file pathname matching QUALIFIER.
+(define-ibuffer-filter filename
+    "Limit current view to buffers with full file pathname matching QUALIFIER.
 
 For example, for a buffer associated with file '/a/b/c.d', this
 matches against '/a/b/c.d'."
@@ -1313,8 +1293,8 @@ matches against '/a/b/c.d'."
     (string-match qualifier it)))
 
 ;; If filename above were renamed to pathname, this could be called filename.
-;;;###autoload (autoload 'ibuffer-filter-by-filename-base "ibuf-ext")
-(define-ibuffer-filter filename-base 
+;;;###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
@@ -1325,8 +1305,8 @@ matches against 'c.d'."
   (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
     (string-match qualifier (file-name-nondirectory it))))
 
-;;;###autoload (autoload 'ibuffer-filter-by-filename-extension "ibuf-ext")
-(define-ibuffer-filter filename-extension
+;;;###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
@@ -1338,29 +1318,19 @@ pattern. For example, for a buffer associated with file
   (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
     (string-match qualifier (or (file-name-extension it) ""))))
 
-;;;###autoload (autoload 'ibuffer-filter-by-filename-root "ibuf-ext")
-(define-ibuffer-filter filename-root
-    "Limit current view to buffers with file basename matching QUALIFIER.
+;;;###autoload (autoload 'ibuffer-filter-by-directory "ibuf-ext")
+(define-ibuffer-filter directory
+    "Limit current view to buffers with directory matching QUALIFIER.
 
-The filename root is the part of the full pathname of the file without
-the directory or extension/suffix components. For example, for a buffer
-associated with file '/a/b/c.d', this matches against 'c'."
-  (:description "filename root"
-   :reader (read-from-minibuffer "Filter by filename root (regex): "))
-  (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
-    (string-match qualifier (file-name-base it))))
-
-;;;###autoload (autoload 'ibuffer-filter-by-filename-directory "ibuf-ext")
-(define-ibuffer-filter filename-directory
-    "Limit current view to buffers with filename directory matching QUALIFIER.
-
-For example, for a buffer associated with file '/a/b/c.d', this
-matches against '/a/b'."
+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-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
-    (let ((dirname (file-name-directory it)))
-      (when dirname (string-match qualifier dirname)))))
+  (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
@@ -1373,7 +1343,7 @@ matches against '/a/b'."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-size-lt  "ibuf-ext")
 (define-ibuffer-filter size-lt
-   "Limit 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: ")))
@@ -1382,11 +1352,19 @@ matches against '/a/b'."
 
 ;;;###autoload (autoload 'ibuffer-filter-by-modified "ibuf-ext")
 (define-ibuffer-filter modified
-   "Limit current view to buffers that are marked as 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.
+This includes buffers visiting a directory in dired."
+  (:description "visiting a file"
+   :reader nil)
+  (with-current-buffer buf (ibuffer-buffer-file-name)))
+
 ;;;###autoload (autoload 'ibuffer-filter-by-content "ibuf-ext")
 (define-ibuffer-filter content
    "Limit current view to buffers whose contents match QUALIFIER."
diff --git a/lisp/ibuffer.el b/lisp/ibuffer.el
index 181a01c..77a6880 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -524,18 +524,18 @@ directory, like `default-directory'."
     (define-key map (kbd "/ n") 'ibuffer-filter-by-name)
     (define-key map (kbd "/ *") 'ibuffer-filter-by-starred-name)
     (define-key map (kbd "/ f") 'ibuffer-filter-by-filename)
-    (define-key map (kbd "/ F") 'ibuffer-filter-by-filename-base)
-    (define-key map (kbd "/ .") 'ibuffer-filter-by-filename-extension)
-    (define-key map (kbd "/ r") 'ibuffer-filter-by-filename-root)
-    (define-key map (kbd "/ /") 'ibuffer-filter-by-filename-directory)
+    (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-directory)
     (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 "/ TAB") 'ibuffer-filter-chosen-by-completion)
 
-    (define-key map (kbd "/ w") 'ibuffer-switch-to-saved-filters)
+    (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)
@@ -666,7 +666,7 @@ directory, like `default-directory'."
       '(menu-item "Add filter by a major mode in use..."
         ibuffer-filter-by-used-mode))
     (define-key-after map [menu-bar view filter filter-by-derived-mode]
-      '(menu-item "Add filter by derived mode..." 
+      '(menu-item "Add filter by derived mode..."
                   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))
@@ -679,24 +679,19 @@ directory, like `default-directory'."
                   :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-filename-base]
+    (define-key-after map [menu-bar view filter filter-by-basename]
       '(menu-item "Add filter by file basename..."
-                  ibuffer-filter-by-filename-base
+                  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-filename-extension]
-      '(menu-item "Add filter by filename extension..."
-                  ibuffer-filter-by-filename-extension
+    (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-filename-root]
-      '(menu-item "Add filter by filename root..."
-                  ibuffer-filter-by-filename-root
-                  :help (concat "For a buffer associated with file '/a/b/c.d', "
-                                "list buffer if a given pattern matches 'c'")))
-    (define-key-after map [menu-bar view filter filter-by-filename-directory]
+    (define-key-after map [menu-bar view filter filter-by-directory]
       '(menu-item "Add filter by filename's directory..."
-                  ibuffer-filter-by-filename-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'")))
@@ -708,6 +703,10 @@ directory, like `default-directory'."
     (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 modified buffer..."
+                  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))
diff --git a/test/lisp/ibuffer-tests.el b/test/lisp/ibuffer-tests.el
index aa06994..e747bab 100644
--- a/test/lisp/ibuffer-tests.el
+++ b/test/lisp/ibuffer-tests.el
@@ -66,7 +66,7 @@
           "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.  
+        :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))
@@ -80,7 +80,7 @@
             buf)))
        (create-non-file-buffer
         (lambda (prefix &rest args-plist)
-          "Create a file and buffer with designated properties.
+          "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.
@@ -128,12 +128,12 @@
                    buf '((and (size-gt . 99)
                               (content . "ring to rule them all")
                               (mode . fundamental-mode)
-                              (filename-base . "\\`ibuf-test-1")))))
+                              (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 (filename-base . "\\`ibuf-test-1")))))))
+                                  (not (basename . "\\`ibuf-test-1")))))))
           (should (ibuffer-included-in-filters-p
                    buf '((and (or (size-gt . 99) (size-lt . 10))
                               (and (content . "ring.*all")
@@ -141,9 +141,9 @@
                                    (content . "them all")
                                    (content . "One"))
                               (not (mode . text-mode))
-                              (filename-base . "\\`ibuf-test-1"))))))
+                              (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))
@@ -152,7 +152,6 @@
                (funcall create-file-buffer "ibuf-test-2" :size 200
                         :mode #'text-mode
                         :include-content "and in the darkness find them\n")))
-          (message "--> %s" buf)
           (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))))
@@ -177,8 +176,7 @@
                              (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)))))
-          (message "--> %s" buf))
+                                 (derived-mode . emacs-lisp-mode))))))
       (funcall clean-up)))
 
   (ert-deftest ibuffer-filter-inclusion-3 ()
@@ -196,23 +194,19 @@
                (dirA (with-current-buffer bufA default-directory))
                (dirB (with-current-buffer bufB default-directory)))
           (should (ibuffer-included-in-filters-p
-                   bufA '((filename-base . "ibuf-test-3"))))
-          (should (ibuffer-included-in-filters-p
-                   bufA '((filename-root . "ibuf-test-3"))))
+                   bufA '((basename . "ibuf-test-3"))))
           (should (ibuffer-included-in-filters-p
-                   bufA '((filename-base . "test-3\\.a"))))
+                   bufA '((basename . "test-3\\.a"))))
           (should (ibuffer-included-in-filters-p
-                   bufA '((filename-extension . "a"))))
+                   bufA '((file-extension . "a"))))
           (should (ibuffer-included-in-filters-p
-                   bufA (list (cons 'filename-directory dirA))))
+                   bufA (list (cons 'directory dirA))))
           (should-not (ibuffer-included-in-filters-p
-                       bufB '((filename-base . "ibuf-test-3"))))
+                       bufB '((basename . "ibuf-test-3"))))
           (should-not (ibuffer-included-in-filters-p
-                       bufB '((filename-root . "ibuf-test-3"))))
-          (should-not (ibuffer-included-in-filters-p
-                       bufB '((filename-extension . "b"))))
-          (should-not (ibuffer-included-in-filters-p
-                       bufB (list (cons 'filename-directory dirB))))
+                       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
@@ -228,7 +222,7 @@
                         :mode #'emacs-lisp-mode :suffix ".el"
                         :include-content "(message \"--%s--\" 'emacs-rocks)\n")))
           (should (ibuffer-included-in-filters-p
-                   buf '((filename-extension . "el"))))
+                   buf '((file-extension . "el"))))
           (should (ibuffer-included-in-filters-p
                    buf '((derived-mode . prog-mode))))
           (should (ibuffer-included-in-filters-p
@@ -240,15 +234,15 @@
           (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 (filename-extension . "el")
+                   buf '((and (file-extension . "el")
                               (derived-mode . prog-mode)
                               (not modified)))))
           (should (ibuffer-included-in-filters-p
-                   buf '((or (filename-extension . "tex")
+                   buf '((or (file-extension . "tex")
                              (derived-mode . prog-mode)
                              (modified)))))
           (should (ibuffer-included-in-filters-p
-                   buf '((filename-extension . "el")
+                   buf '((file-extension . "el")
                          (derived-mode . prog-mode)
                          (not modified)))))
       (funcall clean-up)))
@@ -259,11 +253,11 @@
     (unwind-protect
         (let ((buf
                (funcall create-non-file-buffer "ibuf-test-5.el"
-                        :mode #'emacs-lisp-mode 
+                        :mode #'emacs-lisp-mode
                         :include-content
                         "(message \"--%s--\" \"It really does!\")\n")))
           (should-not (ibuffer-included-in-filters-p
-                       buf '((filename-extension . "el"))))
+                       buf '((file-extension . "el"))))
           (should (ibuffer-included-in-filters-p
                    buf '((size-gt . 18))))
           (should (ibuffer-included-in-filters-p
@@ -334,17 +328,57 @@
       (funcall clean-up)))
 
   (ert-deftest ibuffer-filter-inclusion-8 ()
-    "Tests inclusion with various filters on a single buffer."
+    "Tests inclusion with various filters."
     (skip-unless (featurep 'ibuf-ext))
     (unwind-protect
-        (let ((buf
-               (funcall create-non-file-buffer "ibuf-test-8"
-                        :mode #'artist-mode)))
-          (should (ibuffer-included-in-filters-p
-                   buf '((and (not (starred-name))
-                              (name . "test-8")
-                              (not (size-gt . 100))
-                              (mode . picture-mode))))))
+        (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
@@ -371,7 +405,7 @@
        (clean-up
         (lambda ()
           "Restore all emacs state modified during the tests"
-          (when ibuffer-to-kill         ; created ibuffer 
+          (when ibuffer-to-kill         ; created ibuffer
             (with-current-buffer ibuffer-to-kill
               (set-buffer-modified-p nil)
               (bury-buffer))
@@ -438,7 +472,8 @@
           (with-current-buffer ibuf
             (let ((ibuffer-filtering-qualifiers nil)
                   (ibuffer-filter-groups nil)
-                  (filters [(size-gt . 100) (not (starred-name))]))
+                  (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)
@@ -455,9 +490,57 @@
                                     (pop ibuffer-filtering-qualifiers))
                              (equal (aref filters 1)
                                     (pop ibuffer-filtering-qualifiers))
-                             (null 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))
@@ -466,7 +549,8 @@
           (with-current-buffer ibuf
             (let ((ibuffer-filtering-qualifiers nil)
                   (ibuffer-filter-groups nil)
-                  (filters [(size-gt . 100) (not (starred-name))]))
+                  (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)
@@ -483,7 +567,55 @@
                                     (pop ibuffer-filtering-qualifiers))
                              (equal (aref filters 1)
                                     (pop ibuffer-filtering-qualifiers))
-                             (null 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-save-filters ()
@@ -494,14 +626,14 @@
         (test1 '((mode . org-mode)
                  (or (size-gt . 10000)
                      (and (not (starred-name))
-                          (filename-directory . "\<org\>")))))
-        (test2 '((or (mode . emacs-lisp-mode) (filename-extension . "elc?")
+                          (directory . "\<org\>")))))
+        (test2 '((or (mode . emacs-lisp-mode) (file-extension . "elc?")
                      (and (starred-name) (name . "elisp"))
                      (mode . lisp-interaction-mode))))
         (test3 '((size-lt . 100) (derived-mode . prog-mode)
-                 (or (filename-root . "scratch")
-                     (filename-root . "bonz")
-                     (filename-root . "temp")))))
+                 (or (filename . "scratch")
+                     (filename . "bonz")
+                     (filename . "temp")))))
     (ibuffer-save-filters "test1" test1)
     (should (equal (car ibuffer-saved-filters) (cons "test1" test1)))
     (ibuffer-save-filters "test2" test2)
@@ -523,11 +655,11 @@
         (test3 '(derived-mode . prog-mode))
         (test4 '(or (size-gt . 10000)
                     (and (not (starred-name))
-                         (filename-directory . "\\<org\\>"))))
-        (test5 '(or (filename-root . "scratch")
-                    (filename-root . "bonz")
-                    (filename-root . "temp")))
-        (test6 '(or (mode . emacs-lisp-mode) (filename-extension . "elc?")
+                         (directory . "\\<org\\>"))))
+        (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)
@@ -554,21 +686,21 @@
                                                                 'starred-name)
                                                        ": " "nil"))
                                      (funcall tag
-                                              (funcall description 'filename-directory)
+                                              (funcall description 'directory)
                                               ": " "\\<org\\>")))))
     (should (equal (ibuffer-format-qualifier test5)
                    (funcall tag "OR"
-                            (funcall tag (funcall description 'filename-root)
+                            (funcall tag (funcall description 'filename)
                                      ": "  "scratch")
-                            (funcall tag (funcall description 'filename-root)
+                            (funcall tag (funcall description 'filename)
                                      ": " "bonz")
-                            (funcall tag (funcall description 'filename-root)
+                            (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 'filename-extension)
+                            (funcall tag (funcall description 'file-extension)
                                      ": " "elc?")
                             (funcall tag "AND"
                                      (funcall tag
-- 
2.10.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-11-22 23:45   ` Christopher Genovese
@ 2016-11-26 10:53     ` Tino Calancha
  2016-12-01  3:10       ` Christopher Genovese
  0 siblings, 1 reply; 10+ messages in thread
From: Tino Calancha @ 2016-11-26 10:53 UTC (permalink / raw)
  To: Christopher Genovese; +Cc: emacs-devel, Tino Calancha


Hi Christopher,

thank you very much for your time working on this!
I have checked your newest changes and i got a few additional
comments.  See below.

>I haven't had a chance yet to split the commit to isolate the
>saved filters fix, but I will do that tomorrow and submit
>appropriate patches and bug reports.
Very good.  That would be helpful.  Thank you.

>The buffer name filter you suggest is already available as
>ibuffer-filter-by-name (/ n).
Right! I totally overlooked that.

> 1. Add a `ibuffer-filter-by-visiting-file' (/ v) that selects
>    buffers that are visiting a file.
+(define-ibuffer-filter visiting-file
+    "Limit current view to buffers that are visiting a file.
+This includes buffers visiting a directory in dired."
I wouldn't include Dired buffers here.  In Emacs a buffer
visiting a file means a non-directory file.  For instance,
look doc string of `buffer-file-name'.

I suggest instead:
(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))

> `ibuffer-filter-by-directory' and changed the functionality so that
> in a file buffer it matches against the file's path but in a
> non-file buffer matches against default-directory.
I am wondering if it's worth to have the `ibuffer-filter-by-directory'
in the way you are proposing.  I guess `ibuffer-filter-by-filename'
would suffice most of the times.
In the other hand we have `ibuffer-mark-dired-buffers' bound to '*/'
that is handy.  We might want `ibuffer-filter-by-directory' to do
the symmetric thing: that is, to filter buffers in Dired mode, i.e.,
like a shortcut for '/ m' dired-mode RET.

Alternatively we could accept a prefix in this command:
1) Without prefix, just filter buffers in Dired mode.
2) With a prefix, behave as you wish, as follows:

(define-ibuffer-filter directory
     "Limit current view to Dired buffers.

With prefix argument prompt for a regexp and show just
those buffers with their directory matching that regexp.

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 (and current-prefix-arg
                              (read-from-minibuffer "Filter by directory name (regex): ")))
   (with-current-buffer buf
     (if qualifier
         (let ((dirname
                (ibuffer-aif (ibuffer-buffer-file-name)
                    (file-name-directory it)
                  default-directory)))
           (and dirname (string-match qualifier dirname)))
       (eq major-mode 'dired-mode))))

This means the command do a different thing if we provide the prefix.
I don't know what approach is more useful.
Does 1) or 2) has sense for you?

>3. Keep the `ibuffer-filter-by-basename', making the name
I saw you bound this command to '/ b'.  Good!
I find easier to remember and type '/ b' than '/ F'.

>(Note: '/ d' is already bound to ibuffer-decompose-filter or I would have
>used it.
Opps!  I didn't notice this.  Thanks.

>I think the new bindings are highly mnemonic and will happily advocate
>for them. But the need for consensus makes total sense.
>I felt that the change I made keeps the mnemonic strong.
Once we are happy with the changes we might ask opinion to other
colleagues in Emacs-dev about what to do with the bindings.

>> Your commit message  don't follow the Emacs standards.
> I've fixed this on the new commit.
>* lisp/ibuf-ext.el: added paragraph to file commentary, along
                      ^^^^^
>(ibuffer-saved-filters): clarified documentation,
                           ^^^^^^^^^ 
Please, start sentences with upper case.


>> You might want to write NEWS entry for the new features.
>Done, and included in this commit/patch.
Thank you.  They like quite verbose for NEWS entries.  We just need
to announce the changes.  It's OK to group all new commands
in the same entry.
Let's ignore the entry for the bug fix.  We will back to that issue
once we open the bug report.
I suggest the following shorter entries:
---
*** 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'; bound to '/ TAB' and '/&'
respectively.

---
*** The key binding for `ibuffer-filter-disable' has being changed
to '/DEL'; the commands `ibuffer-pop-filter', `ibuffer-pop-filter-group'
and `ibuffer-or-filter' have the alternative bindings '/<up>', '/S-<up>'
and '/|'.

>> Could you create a receipt where the bug cause an actual failure?
>As you can see, the existing entry "gnus" breaks the expected format.
>So to be more precise than I was earlier: In addition to the unnecessary
>nesting level, this breaks anytime you save to an existing filter.
You are right, it seems there is a bug.

>I will pull out the saved filter changes and submit a
>formal bug report with patches for the two approaches, making
>the other ibuffer changes independent.
That will be very useful.  Thank you.

Regards,
Tino



^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-11-26 10:53     ` Tino Calancha
@ 2016-12-01  3:10       ` Christopher Genovese
  2016-12-02 15:56         ` Tino Calancha
  0 siblings, 1 reply; 10+ messages in thread
From: Christopher Genovese @ 2016-12-01  3:10 UTC (permalink / raw)
  To: Tino Calancha; +Cc: emacs-devel


[-- Attachment #1.1: Type: text/plain, Size: 6703 bytes --]

Tino,

  Sorry it took so long to get this to you; it's been a crazy week.
I've attached a patch file with all the changes we have discussed
(except one, see below) to the code, change logs, and NEWS
and with the saved filter bug changes completely removed as you
requested.  This is all up to date with the current master. I think
the patch has everything, but let me know if I missed something.

  The one change I did not make is to the ibuffer-filter-by-directory
filter as discussed in your most recent note.  My intent with that
one is to make it easy to filter on the directory component of a
filename (or of the buffer) without patterns in the filename interfering.
I don't see the advantage of conflating this with dired mode
or adding a prefix, especially as the two functionalities are
not intuitively comparable.  I'm happy to discuss this, but
I left it in the way I prefer for the time being.

 Thanks again for your help, suggestions, and patience.

  Regards, Chris





On Sat, Nov 26, 2016 at 5:53 AM, Tino Calancha <tino.calancha@gmail.com>
wrote:

>
> Hi Christopher,
>
> thank you very much for your time working on this!
> I have checked your newest changes and i got a few additional
> comments.  See below.
>
> I haven't had a chance yet to split the commit to isolate the
>> saved filters fix, but I will do that tomorrow and submit
>> appropriate patches and bug reports.
>>
> Very good.  That would be helpful.  Thank you.
>
> The buffer name filter you suggest is already available as
>> ibuffer-filter-by-name (/ n).
>>
> Right! I totally overlooked that.
>
> 1. Add a `ibuffer-filter-by-visiting-file' (/ v) that selects
>>    buffers that are visiting a file.
>>
> +(define-ibuffer-filter visiting-file
> +    "Limit current view to buffers that are visiting a file.
> +This includes buffers visiting a directory in dired."
> I wouldn't include Dired buffers here.  In Emacs a buffer
> visiting a file means a non-directory file.  For instance,
> look doc string of `buffer-file-name'.
>
> I suggest instead:
> (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))
>
> `ibuffer-filter-by-directory' and changed the functionality so that
>> in a file buffer it matches against the file's path but in a
>> non-file buffer matches against default-directory.
>>
> I am wondering if it's worth to have the `ibuffer-filter-by-directory'
> in the way you are proposing.  I guess `ibuffer-filter-by-filename'
> would suffice most of the times.
> In the other hand we have `ibuffer-mark-dired-buffers' bound to '*/'
> that is handy.  We might want `ibuffer-filter-by-directory' to do
> the symmetric thing: that is, to filter buffers in Dired mode, i.e.,
> like a shortcut for '/ m' dired-mode RET.
>
> Alternatively we could accept a prefix in this command:
> 1) Without prefix, just filter buffers in Dired mode.
> 2) With a prefix, behave as you wish, as follows:
>
> (define-ibuffer-filter directory
>     "Limit current view to Dired buffers.
>
> With prefix argument prompt for a regexp and show just
> those buffers with their directory matching that regexp.
>
> 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 (and current-prefix-arg
>                              (read-from-minibuffer "Filter by directory
> name (regex): ")))
>   (with-current-buffer buf
>     (if qualifier
>         (let ((dirname
>                (ibuffer-aif (ibuffer-buffer-file-name)
>                    (file-name-directory it)
>                  default-directory)))
>           (and dirname (string-match qualifier dirname)))
>       (eq major-mode 'dired-mode))))
>
> This means the command do a different thing if we provide the prefix.
> I don't know what approach is more useful.
> Does 1) or 2) has sense for you?
>
> 3. Keep the `ibuffer-filter-by-basename', making the name
>>
> I saw you bound this command to '/ b'.  Good!
> I find easier to remember and type '/ b' than '/ F'.
>
> (Note: '/ d' is already bound to ibuffer-decompose-filter or I would have
>> used it.
>>
> Opps!  I didn't notice this.  Thanks.
>
> I think the new bindings are highly mnemonic and will happily advocate
>> for them. But the need for consensus makes total sense.
>> I felt that the change I made keeps the mnemonic strong.
>>
> Once we are happy with the changes we might ask opinion to other
> colleagues in Emacs-dev about what to do with the bindings.
>
> Your commit message  don't follow the Emacs standards.
>>>
>> I've fixed this on the new commit.
>> * lisp/ibuf-ext.el: added paragraph to file commentary, along
>>
>                      ^^^^^
>
>> (ibuffer-saved-filters): clarified documentation,
>>
>                           ^^^^^^^^^ Please, start sentences with upper
> case.
>
>
> You might want to write NEWS entry for the new features.
>>>
>> Done, and included in this commit/patch.
>>
> Thank you.  They like quite verbose for NEWS entries.  We just need
> to announce the changes.  It's OK to group all new commands
> in the same entry.
> Let's ignore the entry for the bug fix.  We will back to that issue
> once we open the bug report.
> I suggest the following shorter entries:
> ---
> *** 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'; bound to '/ TAB' and '/&'
> respectively.
>
> ---
> *** The key binding for `ibuffer-filter-disable' has being changed
> to '/DEL'; the commands `ibuffer-pop-filter', `ibuffer-pop-filter-group'
> and `ibuffer-or-filter' have the alternative bindings '/<up>', '/S-<up>'
> and '/|'.
>
> Could you create a receipt where the bug cause an actual failure?
>>>
>> As you can see, the existing entry "gnus" breaks the expected format.
>> So to be more precise than I was earlier: In addition to the unnecessary
>> nesting level, this breaks anytime you save to an existing filter.
>>
> You are right, it seems there is a bug.
>
> I will pull out the saved filter changes and submit a
>> formal bug report with patches for the two approaches, making
>> the other ibuffer changes independent.
>>
> That will be very useful.  Thank you.
>
> Regards,
> Tino
>

[-- Attachment #1.2: Type: text/html, Size: 10190 bytes --]

[-- Attachment #2: ibuffer-and-filters-revised.patch --]
[-- Type: text/x-diff, Size: 75247 bytes --]

From 03c286393c6c8c83d4120807c749f38115b4916c Mon Sep 17 00:00:00 2001
From: "Christopher R. Genovese" <genovese@cmu.edu>
Date: Thu, 17 Nov 2016 00:44:27 -0500
Subject: [PATCH] Ibuffer refinements: filters, documentation, tests

Summary of overall changes:

+ Extends specification of compound filters

  1. Supports *explicit* logical 'and' compound filter to
  supplement 'or' and 'not', which can be convenient for complex
  rules, especially those created manually.

  2. Accepts two forms of logical 'not': (not qualifier . data)
  and (not (qualifier . data)). The original looks nice with
  nullary filters like (not modified), and the new form is
  pleasantly consistent with sexp structure of 'and' and 'or'.

+ Significant documentation improvements for filtering

  The structure of compound filters had not been documented. The
  new documentation gives an authoritative source for each
  concept and makes the language used throughout more clear and
  consistent (e.g., distinguishing qualifier data from general
  filter specifications).

+ Defines several commonly needed filters

  The new filters are basename, directory, file-extension,
  starred-name, modified, and visiting-file, each bound to
  mnemonic keys in the '/ ' filtering keymap.

+ New interactive filtering command

  New command 'ibuffer-filter-chosen-by-completion' to select a filter
  by completion on filter descriptions.

+ Two changes in filtering '/ '  sub-keymap

  '/ TAB', which was an alternative binding to 'ibuffer-exchange-filters'
           on '/ t' is now bound to 'ibuffer-filter-chosen-by-completion

  '/ /',   which was bound to 'ibuffer-filter-disable' is now bound
           to 'ibuffer-filter-by-directory'. 'ibuffer-filter-disable'
           has been moved to '/ DEL'.

  I believe these are all meaningful and mnemonic choices, but the
  change should be decided by consensus.

+ Fixes small bug in original test

  The one original test failed unexpectedly if ibuf-ext were
  loaded.

+ Adds a substantial number of additional tests with feature ibuf-ext

  Many new tests in ert, leaving the environment untouched,
  cover most aspects of filtering, old and new.

Change Log:

* lisp/ibuf-ext.el: Add paragraph to file commentary, along
with many small improvements throughout to docstrings,
variable naming, and spacing.
(ibuffer-saved-filters): Clarify documentation and
specify customization type.
(ibuffer-filtering-qualifiers): Improve documentation,
making it the authoritative source for filter specification
format.
(ibuffer-filter-groups): Add new documentation that
clarifies filter group structure and role.
(ibuffer-unary-operand): Add new function that transparently
handles 'not' formats for compound filters.
(ibuffer-included-in-filter-p): Add new docstring
and handle 'not' fully.
(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): Add new function analogous to
`ibuffer-or-filter' for completeness.
(ibuffer--or-and-filter): Add new function that handles
both 'or' and 'and' operations and inverses.
(ibuffer-format-qualifier): Handle 'and' filters as well.
lisp/ibuf-ext.el (ibuffer-filter-by-*): Add new pre-defined
filters basename, file-extension, directory, starred-name, modified,
and visiting-file.
(ibuffer-filter-chosen-by-completion): Add new interactive command
for easily choosing a filter from the descriptions.
* lisp/ibuffer.el: Add to filtering keymap and menu, with two
changed keybindings.
* test/lisp/ibuffer-tests.el (ibuffer-autoload): Add appropriate
skip specification.
(ibuffer-*): Add many additional tests that are skipped unless
ibuf-ext is loaded.
* etc/NEWS: Add entries for new user-facing features.
---
 etc/NEWS                   |  27 ++
 lisp/ibuf-ext.el           | 423 ++++++++++++++++++++--------
 lisp/ibuffer.el            |  62 ++++-
 test/lisp/ibuffer-tests.el | 667 ++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 1060 insertions(+), 119 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index cbce027..7e73c75 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -304,6 +304,33 @@ 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'; bound to '/ TAB' and '/&'
+respectively.
+
+---
+*** The key binding for `ibuffer-filter-disable' has being changed
+to '/DEL'; the commands `ibuffer-pop-filter', `ibuffer-pop-filter-group'
+and `ibuffer-or-filter' have the alternative bindings '/<up>', '/S-<up>'
+and '/|'.
+
+---
+*** The data format specifying filters has been extended
+to allow i. explicit logical 'and' of other filters and
+ii. a more flexible form for logical 'not' of a
+filter. This is useful for constructing complex filters
+and filter groups, especially when doing so manually. See
+documentation for 'ibuffer-filtering-qualifiers' 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 5ef0746..0699baf 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:
 
@@ -35,9 +42,12 @@
 
 (eval-when-compile
   (require 'ibuf-macs)
-  (require 'cl-lib))
+  (require 'cl-lib)
+  (require 'subr-x))
+
 
 ;;; Utility functions
+
 (defun ibuffer-delete-alist (key alist)
   "Delete all entries in ALIST that have a key equal to KEY."
   (let (entry)
@@ -119,35 +129,96 @@ Buffers whose major mode is in this list, are not searched."
 
 (defvar ibuffer-auto-buffers-changed nil)
 
-(defcustom ibuffer-saved-filters '(("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)))))
-
-  "An alist of filter qualifiers 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'.
-See also the variables `ibuffer-filtering-qualifiers',
-`ibuffer-filtering-alist', and the functions
-`ibuffer-switch-to-saved-filters', `ibuffer-save-filters'."
-  :type '(repeat sexp)
+(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)))))
+
+  "An alist mapping saved filter names to filter specifications.
+
+Each element should look like (\"NAME\" FILTER-LIST), where
+FILTER-LIST has the same structure as the variable
+`ibuffer-filtering-qualifiers', which see. The filters defined
+here are joined with an implicit logical `and' and associated
+with NAME. The combined specification can be used by name in
+other filter specifications via the `saved' qualifier (again, see
+`ibuffer-filtering-qualifiers'). They can also be switched to by
+name (see the functions `ibuffer-switch-to-saved-filters' and
+`ibuffer-save-filters'). The variable `ibuffer-save-with-custom'
+affects how this information is saved for future sessions. This
+variable can be set directly from lisp code."
+  :version "26.1"
+  :type '(alist :key-type (string :tag "Filter name")
+                :value-type (list :tag "Filter list"
+                                  (repeat (sexp :tag "Filter specification"))))
   :group 'ibuffer)
 
 (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
@@ -179,10 +250,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."
@@ -192,20 +271,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)
 
@@ -512,18 +592,38 @@ To evaluate a form without viewing the buffer, see `ibuffer-do-eval'."
 
 ;;;###autoload
 (defun ibuffer-included-in-filters-p (buf filters)
+  "Returns non-nil if buffer 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)
+  "Does the buffer BUF successfully 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)
@@ -531,17 +631,25 @@ 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
-	   (ibuffer-filter-disable t)
-	   (error "Unknown saved filter %s" (cdr filter)))
-	 (ibuffer-included-in-filters-p buf (cadr data))))
+       (let ((data (assoc (cdr filter) ibuffer-saved-filters)))
+         (unless data
+           (ibuffer-filter-disable t)
+           (error "Unknown saved filter %s" (cdr filter)))
+         (ibuffer-included-in-filters-p buf (cadr data))))
       (_
        (pcase-let ((`(,_type ,_desc ,func)
                     (assq (car filter) ibuffer-filtering-alist)))
@@ -828,39 +936,34 @@ 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)
   (when (null ibuffer-filtering-qualifiers)
     (error "No filters in effect"))
   (let ((lim (pop ibuffer-filtering-qualifiers)))
     (pcase (car lim)
-      (`or
-       (setq ibuffer-filtering-qualifiers (append
-					  (cdr lim)
-					  ibuffer-filtering-qualifiers)))
+      ((or 'or 'and)
+       (setq ibuffer-filtering-qualifiers
+             (nconc (cdr lim) ibuffer-filtering-qualifiers)))
       (`saved
-       (let ((data
-	      (assoc (cdr lim)
-		     ibuffer-saved-filters)))
-	 (unless data
-	   (ibuffer-filter-disable)
-	   (error "Unknown saved filter %s" (cdr lim)))
-	 (setq ibuffer-filtering-qualifiers (append
-					    (cadr data)
-					    ibuffer-filtering-qualifiers))))
+       (let ((data (assoc (cdr lim) ibuffer-saved-filters)))
+         (unless data
+           (ibuffer-filter-disable)
+           (error "Unknown saved filter %s" (cdr lim)))
+         (setq ibuffer-filtering-qualifiers
+               (append (cadr data) ibuffer-filtering-qualifiers))))
       (`not
-       (push (cdr lim)
-	     ibuffer-filtering-qualifiers))
+       (push (ibuffer-unary-operand lim) ibuffer-filtering-qualifiers))
       (_
        (error "Filter type %s is not compound" (car lim)))))
   (ibuffer-update nil t))
@@ -888,31 +991,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
@@ -986,7 +1094,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)
@@ -995,14 +1105,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."
@@ -1020,7 +1132,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))
@@ -1040,7 +1152,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"
@@ -1059,7 +1171,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
@@ -1070,22 +1182,74 @@ 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
+<digits>."
+  (: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 pathname matching QUALIFIER.
+
+For example, for a buffer associated with file '/a/b/c.d', this
+matches against '/a/b/c.d'."
+  (:description "file pathname"
+   :reader (read-from-minibuffer "Filter by file pathname (regexp): "))
   (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
     (string-match qualifier it)))
 
+;; If filename above were renamed to pathname, this could be called filename.
+;;;###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: ")))
@@ -1094,16 +1258,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
@@ -1113,12 +1291,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 51d7cb9..c20b5b9 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -518,28 +518,40 @@ 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-directory)
     (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 "/ TAB") 'ibuffer-filter-chosen-by-completion)
+
     (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 "/ <up>") '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-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
-    (define-key map (kbd "/ /") 'ibuffer-filter-disable)
+    (define-key map (kbd "/ DEL") 'ibuffer-filter-disable)
 
     (define-key map (kbd "M-n") 'ibuffer-forward-filter-group)
     (define-key map "\t" 'ibuffer-forward-filter-group)
@@ -647,6 +659,7 @@ directory, like `default-directory'."
     (define-key-after map [menu-bar view filter filter-disable]
       '(menu-item "Disable all filtering" ibuffer-filter-disable
         :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers)))
+
     (define-key-after map [menu-bar view filter filter-by-mode]
       '(menu-item "Add filter by any major mode..." ibuffer-filter-by-mode))
     (define-key-after map [menu-bar view filter filter-by-used-mode]
@@ -657,19 +670,50 @@ 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 pathname..." 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 modified buffer..."
+                  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))
     (define-key-after map [menu-bar view filter filter-by-predicate]
       '(menu-item "Add filter by Lisp predicate..."
         ibuffer-filter-by-predicate))
+
     (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)))
@@ -682,6 +726,12 @@ directory, like `default-directory'."
     (define-key-after map [menu-bar view filter negate-filter]
       '(menu-item "Negate top filter" ibuffer-negate-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 decompose-filter]
       '(menu-item "Decompose top filter" ibuffer-decompose-filter
         :enable (and (featurep 'ibuf-ext)
diff --git a/test/lisp/ibuffer-tests.el b/test/lisp/ibuffer-tests.el
index 3a4def3..2afd044 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
@@ -66,5 +67,669 @@
       (mapc (lambda (buf) (when (buffer-live-p buf)
                             (kill-buffer buf))) (list buf1 buf2)))))
 
+;; 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
+                        "<HTML><BODY><H1>Hello, World!</H1></BODY></HTML>")))
+          (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 . "\\<org\\>"))))
+        (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)
+                                              ": " "\\<org\\>")))))
+    (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
-- 
2.10.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-12-01  3:10       ` Christopher Genovese
@ 2016-12-02 15:56         ` Tino Calancha
  2016-12-09  1:00           ` Tino Calancha
  0 siblings, 1 reply; 10+ messages in thread
From: Tino Calancha @ 2016-12-02 15:56 UTC (permalink / raw)
  To: Christopher Genovese; +Cc: tino.calancha, emacs-devel

Christopher Genovese <genovese@cmu.edu> writes:

> Tino,
>
>   Sorry it took so long to get this to you; it's been a crazy week.
> I've attached a patch file with all the changes we have discussed
> (except one, see below) to the code, change logs, and NEWS
Without the fix to Bug#25049 is easier to review.  Thank you!
You made a great job and very fast!

I have divided the patch in two parts.

I) The first part includes all but the reassignment of original keybindings,
that is, it keeps `ibuffer-filter-disable' and `ibuffer-exchange-filters'
bound to '//' and '/TAB' respectively.
It also includes following trivial changes:

1) I have added (ignore qualifier) to silent the byte compiler
in `ibuffer-filter-by-starred-name', `ibuffer-filter-by-modified'
and `ibuffer-filter-by-visiting-file'.

2) Catched a typo in the menu definition of filter-by-visiting-file;
dropped the '...' in those menu entries not prompting user for
an argument.
     (define-key-after map [menu-bar view filter filter-by-filename]
-      '(menu-item "Add filter by full pathname..." ibuffer-filter-by-filename
+      '(menu-item "Add filter by full filename..." ibuffer-filter-by-filename
     (define-key-after map [menu-bar view filter filter-by-modified]
-      '(menu-item "Add filter by modified buffer..." ibuffer-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 modified buffer..."
+      '(menu-item "Add filter by buffer visiting a file"
                   ibuffer-filter-by-visiting-file

II) The second part of the patch completes your proposal:
it binds `ibuffer-filter-chosen-by-completion' and`ibuffer-filter-by-directory'
to '/TAB' and '//' respectively, and it reassigns `ibuffer-filter-disable'
to '/DEL'.

The first part of the patch I), it's OK for me: it improves a lot
the documentation of this mode; it adds new convenient commands and
lot of tests.

For the second part II) i would like to hear suggestion/opinion from
other colleagues.  Reassign key bindings might annoy other users
of this mode: it's something you want to do only when everyone agrees that
is for better.

Regards,
Tino

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
From 125f531ba4a5e5b162e701737631d0b14ba94a93 Mon Sep 17 00:00:00 2001
From: Christopher Genovese <genovese@cmu.edu>
Date: Sat, 3 Dec 2016 00:06:47 +0900
Subject: [PATCH 1/2] 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 '/|', '/&', '/<up>', '/S-<up>'
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.
---
 etc/NEWS                   |  21 ++
 lisp/ibuf-ext.el           | 426 +++++++++++++++++++++--------
 lisp/ibuffer.el            |  57 +++-
 test/lisp/ibuffer-tests.el | 667 ++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 1054 insertions(+), 117 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index cbce027..2dd6b5c 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -304,6 +304,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 '/<up>', '/S-<up>', '/|' 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 5ef0746..3421a01 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:
 
@@ -35,9 +42,12 @@
 
 (eval-when-compile
   (require 'ibuf-macs)
-  (require 'cl-lib))
+  (require 'cl-lib)
+  (require 'subr-x))
+
 
 ;;; Utility functions
+
 (defun ibuffer-delete-alist (key alist)
   "Delete all entries in ALIST that have a key equal to KEY."
   (let (entry)
@@ -119,35 +129,96 @@ ibuffer-tmp-show-regexps
 
 (defvar ibuffer-auto-buffers-changed nil)
 
-(defcustom ibuffer-saved-filters '(("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)))))
-
-  "An alist of filter qualifiers 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'.
-See also the variables `ibuffer-filtering-qualifiers',
-`ibuffer-filtering-alist', and the functions
-`ibuffer-switch-to-saved-filters', `ibuffer-save-filters'."
-  :type '(repeat sexp)
+(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)))))
+
+  "An alist mapping saved filter names to filter specifications.
+
+Each element should look like (\"NAME\" FILTER-LIST), where
+FILTER-LIST has the same structure as the variable
+`ibuffer-filtering-qualifiers', which see. The filters defined
+here are joined with an implicit logical `and' and associated
+with NAME. The combined specification can be used by name in
+other filter specifications via the `saved' qualifier (again, see
+`ibuffer-filtering-qualifiers'). They can also be switched to by
+name (see the functions `ibuffer-switch-to-saved-filters' and
+`ibuffer-save-filters'). The variable `ibuffer-save-with-custom'
+affects how this information is saved for future sessions. This
+variable can be set directly from lisp code."
+  :version "26.1"
+  :type '(alist :key-type (string :tag "Filter name")
+                :value-type (list :tag "Filter list"
+                                  (repeat (sexp :tag "Filter specification"))))
   :group 'ibuffer)
 
 (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
@@ -179,10 +250,18 @@ ibuffer-cached-filter-formats
 (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."
@@ -192,20 +271,21 @@ ibuffer-show-empty-filter-groups
 (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)
 
@@ -512,18 +592,38 @@ print
 
 ;;;###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)
@@ -531,17 +631,25 @@ ibuffer-included-in-filter-p-1
    (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
-	   (ibuffer-filter-disable t)
-	   (error "Unknown saved filter %s" (cdr filter)))
-	 (ibuffer-included-in-filters-p buf (cadr data))))
+       (let ((data (assoc (cdr filter) ibuffer-saved-filters)))
+         (unless data
+           (ibuffer-filter-disable t)
+           (error "Unknown saved filter %s" (cdr filter)))
+         (ibuffer-included-in-filters-p buf (cadr data))))
       (_
        (pcase-let ((`(,_type ,_desc ,func)
                     (assq (car filter) ibuffer-filtering-alist)))
@@ -828,39 +936,34 @@ ibuffer-pop-filter
     (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)
   (when (null ibuffer-filtering-qualifiers)
     (error "No filters in effect"))
   (let ((lim (pop ibuffer-filtering-qualifiers)))
     (pcase (car lim)
-      (`or
-       (setq ibuffer-filtering-qualifiers (append
-					  (cdr lim)
-					  ibuffer-filtering-qualifiers)))
+      ((or 'or 'and)
+       (setq ibuffer-filtering-qualifiers
+             (nconc (cdr lim) ibuffer-filtering-qualifiers)))
       (`saved
-       (let ((data
-	      (assoc (cdr lim)
-		     ibuffer-saved-filters)))
-	 (unless data
-	   (ibuffer-filter-disable)
-	   (error "Unknown saved filter %s" (cdr lim)))
-	 (setq ibuffer-filtering-qualifiers (append
-					    (cadr data)
-					    ibuffer-filtering-qualifiers))))
+       (let ((data (assoc (cdr lim) ibuffer-saved-filters)))
+         (unless data
+           (ibuffer-filter-disable)
+           (error "Unknown saved filter %s" (cdr lim)))
+         (setq ibuffer-filtering-qualifiers
+               (append (cadr data) ibuffer-filtering-qualifiers))))
       (`not
-       (push (cdr lim)
-	     ibuffer-filtering-qualifiers))
+       (push (ibuffer-unary-operand lim) ibuffer-filtering-qualifiers))
       (_
        (error "Filter type %s is not compound" (car lim)))))
   (ibuffer-update nil t))
@@ -888,31 +991,36 @@ ibuffer-negate-filter
 	  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
@@ -986,7 +1094,9 @@ ibuffer-format-filter-group-data
 
 (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)
@@ -995,14 +1105,16 @@ ibuffer-format-qualifier-1
      (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."
@@ -1020,7 +1132,7 @@ ibuffer-list-buffer-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))
@@ -1040,7 +1152,7 @@ mode
 
 ;;;###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"
@@ -1059,7 +1171,7 @@ used-mode
 
 ;;;###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
@@ -1070,22 +1182,75 @@ derived-mode
 
 ;;;###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
+<digits>."
+  (:description "starred buffer name"
+   :reader nil)
+  (ignore qualifier)
+  (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 pathname matching QUALIFIER.
+
+For example, for a buffer associated with file '/a/b/c.d', this
+matches against '/a/b/c.d'."
+  (:description "file pathname"
+   :reader (read-from-minibuffer "Filter by file pathname (regexp): "))
   (ibuffer-awhen (with-current-buffer buf (ibuffer-buffer-file-name))
     (string-match qualifier it)))
 
+;; If filename above were renamed to pathname, this could be called filename.
+;;;###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: ")))
@@ -1094,16 +1259,32 @@ size-gt
 
 ;;;###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)
+  (ignore qualifier)
+  (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)
+  (ignore qualifier)
+  (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
@@ -1113,12 +1294,33 @@ content
 
 ;;;###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 51d7cb9..0205861 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -518,26 +518,37 @@ ibuffer-mode-map
     (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 "/ <up>") '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-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
     (define-key map (kbd "/ /") 'ibuffer-filter-disable)
 
@@ -647,6 +658,7 @@ ibuffer-mode-map
     (define-key-after map [menu-bar view filter filter-disable]
       '(menu-item "Disable all filtering" ibuffer-filter-disable
         :enable (and (featurep 'ibuf-ext) ibuffer-filtering-qualifiers)))
+
     (define-key-after map [menu-bar view filter filter-by-mode]
       '(menu-item "Add filter by any major mode..." ibuffer-filter-by-mode))
     (define-key-after map [menu-bar view filter filter-by-used-mode]
@@ -657,19 +669,50 @@ ibuffer-mode-map
                   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))
     (define-key-after map [menu-bar view filter filter-by-predicate]
       '(menu-item "Add filter by Lisp predicate..."
         ibuffer-filter-by-predicate))
+
     (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)))
@@ -682,6 +725,12 @@ ibuffer-mode-map
     (define-key-after map [menu-bar view filter negate-filter]
       '(menu-item "Negate top filter" ibuffer-negate-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 decompose-filter]
       '(menu-item "Decompose top filter" ibuffer-decompose-filter
         :enable (and (featurep 'ibuf-ext)
diff --git a/test/lisp/ibuffer-tests.el b/test/lisp/ibuffer-tests.el
index 3a4def3..2afd044 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
@@ -66,5 +67,669 @@
       (mapc (lambda (buf) (when (buffer-live-p buf)
                             (kill-buffer buf))) (list buf1 buf2)))))
 
+;; 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
+                        "<HTML><BODY><H1>Hello, World!</H1></BODY></HTML>")))
+          (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 . "\\<org\\>"))))
+        (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)
+                                              ": " "\\<org\\>")))))
+    (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
-- 
2.10.2

From 4f951f46bab2ae36d9a3cf94489a9e0978ee6fb2 Mon Sep 17 00:00:00 2001
From: Christopher Genovese <genovese@cmu.edu>
Date: Sat, 3 Dec 2016 00:07:25 +0900
Subject: [PATCH 2/2] ibuffer: Update key bindings

* lisp/ibuffer.el (ibuffer-mode-map): Bind 'ibuffer-filter-by-directory'
and 'ibuffer-filter-chosen-by-completion' to '//' and '/TAB' respectively.
Rebind 'ibuffer-filter-disable' to '/ DEL'.
; * etc/NEWS: Update NEWS entries.
---
 etc/NEWS        | 10 ++++++----
 lisp/ibuffer.el |  5 +++--
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 2dd6b5c..ec56ea0 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -312,12 +312,14 @@ to '/b', '/.', '//', '/*', '/i' and '/v'.
 
 ---
 *** Two new commands 'ibuffer-filter-chosen-by-completion'
-and `ibuffer-and-filter', the second bound to '/&'.
+and `ibuffer-and-filter'; bound to '/ TAB' and '/&'
+respectively.
 
 ---
-*** The commands `ibuffer-pop-filter', `ibuffer-pop-filter-group',
-`ibuffer-or-filter' and `ibuffer-filter-disable' have the alternative
-bindings '/<up>', '/S-<up>', '/|' and '/DEL', respectively.
+*** The key binding for `ibuffer-filter-disable' has being changed
+to '/DEL'; the commands `ibuffer-pop-filter', `ibuffer-pop-filter-group'
+and `ibuffer-or-filter' have the alternative bindings '/<up>', '/S-<up>'
+and '/|'.
 
 ---
 *** The data format specifying filters has been extended to allow
diff --git a/lisp/ibuffer.el b/lisp/ibuffer.el
index 0205861..72e37f5 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -526,12 +526,14 @@ ibuffer-mode-map
     (define-key map (kbd "/ f") 'ibuffer-filter-by-filename)
     (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-directory)
     (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 "/ TAB") 'ibuffer-filter-chosen-by-completion)
 
     (define-key map (kbd "/ r") 'ibuffer-switch-to-saved-filters)
     (define-key map (kbd "/ a") 'ibuffer-add-saved-filters)
@@ -542,7 +544,6 @@ ibuffer-mode-map
     (define-key map (kbd "/ <up>") '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)
@@ -550,7 +551,7 @@ ibuffer-mode-map
     (define-key map (kbd "/ P") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ S-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
-    (define-key map (kbd "/ /") 'ibuffer-filter-disable)
+    (define-key map (kbd "/ DEL") 'ibuffer-filter-disable)
 
     (define-key map (kbd "M-n") 'ibuffer-forward-filter-group)
     (define-key map "\t" 'ibuffer-forward-filter-group)
-- 
2.10.2

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
In GNU Emacs 26.0.50.1 (x86_64-pc-linux-gnu, GTK+ Version 3.22.4)
 of 2016-12-02
Repository revision: 66d6e7e9ecf5e481f8c2c3a4f88411f66c869a6e



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-12-02 15:56         ` Tino Calancha
@ 2016-12-09  1:00           ` Tino Calancha
  2016-12-14 19:47             ` Christopher Genovese
  0 siblings, 1 reply; 10+ messages in thread
From: Tino Calancha @ 2016-12-09  1:00 UTC (permalink / raw)
  To: Christopher Genovese; +Cc: emacs-devel, tino.calancha

Tino Calancha <tino.calancha@gmail.com> writes:

> Christopher Genovese <genovese@cmu.edu> writes:
>
>> Tino,
>>
>>   Sorry it took so long to get this to you; it's been a crazy week.
>> I've attached a patch file with all the changes we have discussed
>> (except one, see below) to the code, change logs, and NEWS
> Without the fix to Bug#25049 is easier to review.  Thank you!
> You made a great job and very fast!
>
> I have divided the patch in two parts.

Hi Chris,

This week ibuffer.el and ibuf-ext.el have changed significatly
for bug fixing.  I have updated your patch in this thread to
be applied on top of the current master branch.

Let's test a few days more this updated patch to confirm that
everything works as expected.

The updated patch can be applied to the current state of the
master branch, i.e., currently the commit
f0a1e9ec3fba3d5bea5bd62f525dba3fb005d1b1

Regards,
Tino

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
From 1cffd494f352c8b990d36e99cebd8f70e746d6c4 Mon Sep 17 00:00:00 2001
From: Christopher Genovese <genovese@cmu.edu>
Date: Fri, 9 Dec 2016 09:13:06 +0900
Subject: [PATCH 1/2] 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 '/|', '/&', '/<up>', '/S-<up>'
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.
---
 etc/NEWS                   |  21 ++
 lisp/ibuf-ext.el           | 318 ++++++++++++++++-----
 lisp/ibuffer.el            |  55 +++-
 test/lisp/ibuffer-tests.el | 667 ++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 989 insertions(+), 72 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index a62668a..f60deb1 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -316,6 +316,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 '/<up>', '/S-<up>', '/|' 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 9ce7b5a..d1e70b6 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:
 
@@ -214,8 +221,48 @@ ibuffer-old-saved-filters-warning
 "))
 
 (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 +294,18 @@ ibuffer-cached-filter-formats
 (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 +315,21 @@ ibuffer-show-empty-filter-groups
 (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 +658,38 @@ print
 
 ;;;###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 +697,19 @@ ibuffer-included-in-filter-p-1
    (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 +1002,17 @@ ibuffer-pop-filter
     (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 +1021,14 @@ ibuffer-decompose-filter
          (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 +1057,36 @@ ibuffer-negate-filter
 	  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 +1160,9 @@ ibuffer-format-filter-group-data
 
 (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 +1171,16 @@ ibuffer-format-qualifier-1
      (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 +1198,7 @@ ibuffer-list-buffer-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 +1218,7 @@ mode
 
 ;;;###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 +1237,7 @@ used-mode
 
 ;;;###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 +1248,74 @@ derived-mode
 
 ;;;###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
+<digits>."
+  (:description "starred buffer name"
+   :reader nil)
+  (ignore qualifier)
+  (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 +1324,32 @@ size-gt
 
 ;;;###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)
+  (ignore qualifier)
+  (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)
+  (ignore qualifier)
+  (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 +1359,33 @@ content
 
 ;;;###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 94cee32..5a74084 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -518,26 +518,37 @@ ibuffer-mode-map
     (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 "/ <up>") '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-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
     (define-key map (kbd "/ /") 'ibuffer-filter-disable)
 
@@ -657,13 +668,43 @@ ibuffer-mode-map
                   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 @@ ibuffer-mode-map
     (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 92ed101..40760ab 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
+                        "<HTML><BODY><H1>Hello, World!</H1></BODY></HTML>")))
+          (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 . "\\<org\\>"))))
+        (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)
+                                              ": " "\\<org\\>")))))
+    (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
-- 
2.10.2

From 268f9de4ee4ef6cdba9d4c313a52e42653a1067c Mon Sep 17 00:00:00 2001
From: Christopher Genovese <genovese@cmu.edu>
Date: Fri, 9 Dec 2016 09:13:36 +0900
Subject: [PATCH 2/2] ibuffer: Update key bindings

* lisp/ibuffer.el (ibuffer-mode-map): Bind 'ibuffer-filter-by-directory'
and 'ibuffer-filter-chosen-by-completion' to '//' and '/TAB' respectively.
Rebind 'ibuffer-filter-disable' to '/ DEL'.
; * etc/NEWS: Update NEWS entries.
---
 etc/NEWS        | 10 ++++++----
 lisp/ibuffer.el |  5 +++--
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index f60deb1..fc2dfe5 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -324,12 +324,14 @@ to '/b', '/.', '//', '/*', '/i' and '/v'.
 
 ---
 *** Two new commands 'ibuffer-filter-chosen-by-completion'
-and `ibuffer-and-filter', the second bound to '/&'.
+and `ibuffer-and-filter'; bound to '/ TAB' and '/&'
+respectively.
 
 ---
-*** The commands `ibuffer-pop-filter', `ibuffer-pop-filter-group',
-`ibuffer-or-filter' and `ibuffer-filter-disable' have the alternative
-bindings '/<up>', '/S-<up>', '/|' and '/DEL', respectively.
+*** The key binding for `ibuffer-filter-disable' has being changed
+to '/DEL'; the commands `ibuffer-pop-filter', `ibuffer-pop-filter-group'
+and `ibuffer-or-filter' have the alternative bindings '/<up>', '/S-<up>'
+and '/|'.
 
 ---
 *** The data format specifying filters has been extended to allow
diff --git a/lisp/ibuffer.el b/lisp/ibuffer.el
index 5a74084..db9cfeb 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -526,12 +526,14 @@ ibuffer-mode-map
     (define-key map (kbd "/ f") 'ibuffer-filter-by-filename)
     (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-directory)
     (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 "/ TAB") 'ibuffer-filter-chosen-by-completion)
 
     (define-key map (kbd "/ r") 'ibuffer-switch-to-saved-filters)
     (define-key map (kbd "/ a") 'ibuffer-add-saved-filters)
@@ -542,7 +544,6 @@ ibuffer-mode-map
     (define-key map (kbd "/ <up>") '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)
@@ -550,7 +551,7 @@ ibuffer-mode-map
     (define-key map (kbd "/ P") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ S-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
-    (define-key map (kbd "/ /") 'ibuffer-filter-disable)
+    (define-key map (kbd "/ DEL") 'ibuffer-filter-disable)
 
     (define-key map (kbd "M-n") 'ibuffer-forward-filter-group)
     (define-key map "\t" 'ibuffer-forward-filter-group)
-- 
2.10.2


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
In GNU Emacs 26.0.50.1 (x86_64-pc-linux-gnu, GTK+ Version 3.22.4)
 of 2016-12-08
Repository revision: f0a1e9ec3fba3d5bea5bd62f525dba3fb005d1b1



^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-12-09  1:00           ` Tino Calancha
@ 2016-12-14 19:47             ` Christopher Genovese
  2016-12-15  6:27               ` Tino Calancha
  0 siblings, 1 reply; 10+ messages in thread
From: Christopher Genovese @ 2016-12-14 19:47 UTC (permalink / raw)
  To: Tino Calancha; +Cc: emacs-devel

[-- Attachment #1: Type: text/plain, Size: 76479 bytes --]

Tino,

  I just had a chance to apply the patch, rebuild, and test.

  Two things:

  1. The "(ignore qualifier)" statement you added to several of the
      filters causes the filters to always return nil. Removing this
      ignore fixes the problem and makes the tests pass.

      But I think this is actually a new problem that was introduced
      in more recent changes to ibuf-macs.el.  What seems to be happening
      on first look is that the filter code in define-ibuffer-filter is now
wrapped
      in a condition-case  inside a lambda making the ignore form the body
form
      and thus always giving a nil result.  In the versions when we started
this,
      the filter function was directly wrapped in a lambda so the ignore
directive
      you included could take effect.

      Looking in the current version of ibuf-macs.el for the definition of
      define-ibuffer-filter it has

                    (condition-case nil
                         ,@body
                       (error (ibuffer-pop-filter)
                          ...))

       but I think this should have the spliced ,@body wrapped in a progn.
       That would also solve the problem. Whether the ignore would suppress
        the compiler warnings in that position as you intended, I'm not
sure.

   2. This patch removed the additional default saved filters that I had
added
       ("TeX", "text document", "web"), which is fine.  But one of my tests
       used one of those saved filters because I had it predefined in
ibuffer-saved-filters.
       It's an easy change either way to fix this.

 All the other tests pass without a problem.

  Let me know how you'd like me to proceed.

 -- Chris


On Thu, Dec 8, 2016 at 8:00 PM, Tino Calancha <tino.calancha@gmail.com>
wrote:

> Tino Calancha <tino.calancha@gmail.com> writes:
>
> > Christopher Genovese <genovese@cmu.edu> writes:
> >
> >> Tino,
> >>
> >>   Sorry it took so long to get this to you; it's been a crazy week.
> >> I've attached a patch file with all the changes we have discussed
> >> (except one, see below) to the code, change logs, and NEWS
> > Without the fix to Bug#25049 is easier to review.  Thank you!
> > You made a great job and very fast!
> >
> > I have divided the patch in two parts.
>
> Hi Chris,
>
> This week ibuffer.el and ibuf-ext.el have changed significatly
> for bug fixing.  I have updated your patch in this thread to
> be applied on top of the current master branch.
>
> Let's test a few days more this updated patch to confirm that
> everything works as expected.
>
> The updated patch can be applied to the current state of the
> master branch, i.e., currently the commit
> f0a1e9ec3fba3d5bea5bd62f525dba3fb005d1b1
>
> Regards,
> Tino
>
> ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
> ;;;;;;;;;;;;;;;;;;;
> From 1cffd494f352c8b990d36e99cebd8f70e746d6c4 Mon Sep 17 00:00:00 2001
> From: Christopher Genovese <genovese@cmu.edu>
> Date: Fri, 9 Dec 2016 09:13:06 +0900
> Subject: [PATCH 1/2] 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 '/|', '/&', '/<up>', '/S-<up>'
> 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.
> ---
>  etc/NEWS                   |  21 ++
>  lisp/ibuf-ext.el           | 318 ++++++++++++++++-----
>  lisp/ibuffer.el            |  55 +++-
>  test/lisp/ibuffer-tests.el | 667 ++++++++++++++++++++++++++++++
> ++++++++++++++-
>  4 files changed, 989 insertions(+), 72 deletions(-)
>
> diff --git a/etc/NEWS b/etc/NEWS
> index a62668a..f60deb1 100644
> --- a/etc/NEWS
> +++ b/etc/NEWS
> @@ -316,6 +316,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 '/<up>', '/S-<up>', '/|' 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 9ce7b5a..d1e70b6 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:
>
> @@ -214,8 +221,48 @@ ibuffer-old-saved-filters-warning
>  "))
>
>  (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 +294,18 @@ ibuffer-cached-filter-formats
>  (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 +315,21 @@ ibuffer-show-empty-filter-groups
>  (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 +658,38 @@ print
>
>  ;;;###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 +697,19 @@ ibuffer-included-in-filter-p-1
>     (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 +1002,17 @@ ibuffer-pop-filter
>      (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 +1021,14 @@ ibuffer-decompose-filter
>           (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 +1057,36 @@ ibuffer-negate-filter
>           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 +1160,9 @@ ibuffer-format-filter-group-data
>
>  (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 +1171,16 @@ ibuffer-format-qualifier-1
>       (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 +1198,7 @@ ibuffer-list-buffer-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 +1218,7 @@ mode
>
>  ;;;###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 +1237,7 @@ used-mode
>
>  ;;;###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 +1248,74 @@ derived-mode
>
>  ;;;###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
> +<digits>."
> +  (:description "starred buffer name"
> +   :reader nil)
> +  (ignore qualifier)
> +  (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 +1324,32 @@ size-gt
>
>  ;;;###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)
> +  (ignore qualifier)
> +  (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)
> +  (ignore qualifier)
> +  (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 +1359,33 @@ content
>
>  ;;;###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 94cee32..5a74084 100644
> --- a/lisp/ibuffer.el
> +++ b/lisp/ibuffer.el
> @@ -518,26 +518,37 @@ ibuffer-mode-map
>      (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 "/ <up>") '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-<up>") 'ibuffer-pop-filter-group)
>      (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
>      (define-key map (kbd "/ /") 'ibuffer-filter-disable)
>
> @@ -657,13 +668,43 @@ ibuffer-mode-map
>                    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 @@ ibuffer-mode-map
>      (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 92ed101..40760ab 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
> +                        "<HTML><BODY><H1>Hello,
> World!</H1></BODY></HTML>")))
> +          (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 . "\\<org\\>"))))
> +        (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)
> +                                              ": " "\\<org\\>")))))
> +    (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
> --
> 2.10.2
>
> From 268f9de4ee4ef6cdba9d4c313a52e42653a1067c Mon Sep 17 00:00:00 2001
> From: Christopher Genovese <genovese@cmu.edu>
> Date: Fri, 9 Dec 2016 09:13:36 +0900
> Subject: [PATCH 2/2] ibuffer: Update key bindings
>
> * lisp/ibuffer.el (ibuffer-mode-map): Bind 'ibuffer-filter-by-directory'
> and 'ibuffer-filter-chosen-by-completion' to '//' and '/TAB' respectively.
> Rebind 'ibuffer-filter-disable' to '/ DEL'.
> ; * etc/NEWS: Update NEWS entries.
> ---
>  etc/NEWS        | 10 ++++++----
>  lisp/ibuffer.el |  5 +++--
>  2 files changed, 9 insertions(+), 6 deletions(-)
>
> diff --git a/etc/NEWS b/etc/NEWS
> index f60deb1..fc2dfe5 100644
> --- a/etc/NEWS
> +++ b/etc/NEWS
> @@ -324,12 +324,14 @@ to '/b', '/.', '//', '/*', '/i' and '/v'.
>
>  ---
>  *** Two new commands 'ibuffer-filter-chosen-by-completion'
> -and `ibuffer-and-filter', the second bound to '/&'.
> +and `ibuffer-and-filter'; bound to '/ TAB' and '/&'
> +respectively.
>
>  ---
> -*** The commands `ibuffer-pop-filter', `ibuffer-pop-filter-group',
> -`ibuffer-or-filter' and `ibuffer-filter-disable' have the alternative
> -bindings '/<up>', '/S-<up>', '/|' and '/DEL', respectively.
> +*** The key binding for `ibuffer-filter-disable' has being changed
> +to '/DEL'; the commands `ibuffer-pop-filter', `ibuffer-pop-filter-group'
> +and `ibuffer-or-filter' have the alternative bindings '/<up>', '/S-<up>'
> +and '/|'.
>
>  ---
>  *** The data format specifying filters has been extended to allow
> diff --git a/lisp/ibuffer.el b/lisp/ibuffer.el
> index 5a74084..db9cfeb 100644
> --- a/lisp/ibuffer.el
> +++ b/lisp/ibuffer.el
> @@ -526,12 +526,14 @@ ibuffer-mode-map
>      (define-key map (kbd "/ f") 'ibuffer-filter-by-filename)
>      (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-directory)
>      (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 "/ TAB") 'ibuffer-filter-chosen-by-completion)
>
>      (define-key map (kbd "/ r") 'ibuffer-switch-to-saved-filters)
>      (define-key map (kbd "/ a") 'ibuffer-add-saved-filters)
> @@ -542,7 +544,6 @@ ibuffer-mode-map
>      (define-key map (kbd "/ <up>") '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)
> @@ -550,7 +551,7 @@ ibuffer-mode-map
>      (define-key map (kbd "/ P") 'ibuffer-pop-filter-group)
>      (define-key map (kbd "/ S-<up>") 'ibuffer-pop-filter-group)
>      (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
> -    (define-key map (kbd "/ /") 'ibuffer-filter-disable)
> +    (define-key map (kbd "/ DEL") 'ibuffer-filter-disable)
>
>      (define-key map (kbd "M-n") 'ibuffer-forward-filter-group)
>      (define-key map "\t" 'ibuffer-forward-filter-group)
> --
> 2.10.2
>
>
> ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
> ;;;;;;;;;;;;;;;;;;;
> In GNU Emacs 26.0.50.1 (x86_64-pc-linux-gnu, GTK+ Version 3.22.4)
>  of 2016-12-08
> Repository revision: f0a1e9ec3fba3d5bea5bd62f525dba3fb005d1b1
>

[-- Attachment #2: Type: text/html, Size: 97309 bytes --]

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: Ibuffer improvements: filtering, documentation, bug fix, tests
  2016-12-14 19:47             ` Christopher Genovese
@ 2016-12-15  6:27               ` Tino Calancha
  0 siblings, 0 replies; 10+ messages in thread
From: Tino Calancha @ 2016-12-15  6:27 UTC (permalink / raw)
  To: Christopher Genovese; +Cc: tino.calancha, emacs-devel

Christopher Genovese <genovese@cmu.edu> writes:

>   I just had a chance to apply the patch, rebuild, and test.
>   1. The "(ignore qualifier)" statement you added to several of the
>       filters causes the filters to always return nil. Removing this
>       ignore fixes the problem and makes the tests pass.
>
>       But I think this is actually a new problem that was introduced
>       in more recent changes to ibuf-macs.el.  What seems to be happening
>       on first look is that the filter code in define-ibuffer-filter is now wrapped 
>       in a condition-case  inside a lambda making the ignore form the body form 
>       and thus always giving a nil result.  In the versions when we started this,
>       the filter function was directly wrapped in a lambda so the ignore directive 
>       you included could take effect.
>   
>       Looking in the current version of ibuf-macs.el for the definition of
>       define-ibuffer-filter it has  
>
>                     (condition-case nil
>                          ,@body
>                        (error (ibuffer-pop-filter)
>                           ...))
>
>        but I think this should have the spliced ,@body wrapped in a progn.
>        That would also solve the problem. Whether the ignore would suppress
>         the compiler warnings in that position as you intended, I'm not sure.
Right.  Thank you!  I was confused for the doc string of condition-case:
it calls the second argument bodyform, instead of form, so i though it's
a body of lisp expressions.  I have wrapped ,@body in a progn as you suggested
(commit 0a5898c).
Also, we don't need those (ignore qualifier) anymore because the handler
of the condition-case already uses qualifier.

>    2. This patch removed the additional default saved filters that I had added
>        ("TeX", "text document", "web"), which is fine.  But one of my tests
>        used one of those saved filters because I had it predefined in ibuffer-saved-filters.  
>        It's an easy change either way to fix this.
Sorry for that, i didn't notice that i drop that part :-S
Below is the updated patch with 1) and 2) fixed.

If we don't see any more issues until next week then let's
push the first part of the patch: that is without changing
previous keybindings.
After that, i will open a new thread to ask opinion to our colleagues
about whether they agree to update the keybindings.

Thank you very much.
Tino

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
From 978d08828593b3004c64a1741c042b7c37107ed4 Mon Sep 17 00:00:00 2001
From: Christopher Genovese <genovese@cmu.edu>
Date: Thu, 15 Dec 2016 15:09:01 +0900
Subject: [PATCH 1/2] 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 '/|', '/&', '/<up>', '/S-<up>'
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.
---
 etc/NEWS                   |  21 ++
 lisp/ibuf-ext.el           | 347 +++++++++++++++++------
 lisp/ibuffer.el            |  55 +++-
 test/lisp/ibuffer-tests.el | 667 ++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 1009 insertions(+), 81 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index fdd901f..d7b7431 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 '/<up>', '/S-<up>', '/|' 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 9ce7b5a..7ebfecd 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 @@ ibuffer-update-saved-filters-format
            (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 @@ ibuffer-old-saved-filters-warning
 "))
 
 (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 @@ ibuffer-cached-filter-formats
 (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 @@ ibuffer-show-empty-filter-groups
 (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 @@ print
 
 ;;;###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 @@ ibuffer-included-in-filter-p-1
    (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 @@ ibuffer-pop-filter
     (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 @@ ibuffer-decompose-filter
          (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 @@ ibuffer-negate-filter
 	  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 @@ ibuffer-format-filter-group-data
 
 (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 @@ ibuffer-format-qualifier-1
      (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 @@ ibuffer-list-buffer-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 @@ mode
 
 ;;;###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 @@ used-mode
 
 ;;;###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 @@ derived-mode
 
 ;;;###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
+<digits>."
+  (: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 @@ size-gt
 
 ;;;###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 @@ content
 
 ;;;###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 94cee32..5a74084 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -518,26 +518,37 @@ ibuffer-mode-map
     (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 "/ <up>") '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-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
     (define-key map (kbd "/ /") 'ibuffer-filter-disable)
 
@@ -657,13 +668,43 @@ ibuffer-mode-map
                   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 @@ ibuffer-mode-map
     (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 92ed101..40760ab 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
+                        "<HTML><BODY><H1>Hello, World!</H1></BODY></HTML>")))
+          (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 . "\\<org\\>"))))
+        (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)
+                                              ": " "\\<org\\>")))))
+    (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
-- 
2.10.2

From 84859fefa1c1ef0ca5ff77657852014fa751e619 Mon Sep 17 00:00:00 2001
From: Christopher Genovese <genovese@cmu.edu>
Date: Thu, 15 Dec 2016 15:09:49 +0900
Subject: [PATCH 2/2] ibuffer: Update key bindings

* lisp/ibuffer.el (ibuffer-mode-map): Bind 'ibuffer-filter-by-directory'
and 'ibuffer-filter-chosen-by-completion' to '//' and '/TAB' respectively.
Rebind 'ibuffer-filter-disable' to '/ DEL'.
; * etc/NEWS: Update NEWS entries.
---
 etc/NEWS        | 10 ++++++----
 lisp/ibuffer.el |  5 +++--
 2 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index d7b7431..7cfc234 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -341,12 +341,14 @@ to '/b', '/.', '//', '/*', '/i' and '/v'.
 
 ---
 *** Two new commands 'ibuffer-filter-chosen-by-completion'
-and `ibuffer-and-filter', the second bound to '/&'.
+and `ibuffer-and-filter'; bound to '/ TAB' and '/&'
+respectively.
 
 ---
-*** The commands `ibuffer-pop-filter', `ibuffer-pop-filter-group',
-`ibuffer-or-filter' and `ibuffer-filter-disable' have the alternative
-bindings '/<up>', '/S-<up>', '/|' and '/DEL', respectively.
+*** The key binding for `ibuffer-filter-disable' has being changed
+to '/DEL'; the commands `ibuffer-pop-filter', `ibuffer-pop-filter-group'
+and `ibuffer-or-filter' have the alternative bindings '/<up>', '/S-<up>'
+and '/|'.
 
 ---
 *** The data format specifying filters has been extended to allow
diff --git a/lisp/ibuffer.el b/lisp/ibuffer.el
index 5a74084..db9cfeb 100644
--- a/lisp/ibuffer.el
+++ b/lisp/ibuffer.el
@@ -526,12 +526,14 @@ ibuffer-mode-map
     (define-key map (kbd "/ f") 'ibuffer-filter-by-filename)
     (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-directory)
     (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 "/ TAB") 'ibuffer-filter-chosen-by-completion)
 
     (define-key map (kbd "/ r") 'ibuffer-switch-to-saved-filters)
     (define-key map (kbd "/ a") 'ibuffer-add-saved-filters)
@@ -542,7 +544,6 @@ ibuffer-mode-map
     (define-key map (kbd "/ <up>") '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)
@@ -550,7 +551,7 @@ ibuffer-mode-map
     (define-key map (kbd "/ P") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ S-<up>") 'ibuffer-pop-filter-group)
     (define-key map (kbd "/ D") 'ibuffer-decompose-filter-group)
-    (define-key map (kbd "/ /") 'ibuffer-filter-disable)
+    (define-key map (kbd "/ DEL") 'ibuffer-filter-disable)
 
     (define-key map (kbd "M-n") 'ibuffer-forward-filter-group)
     (define-key map "\t" 'ibuffer-forward-filter-group)
-- 
2.10.2

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
In GNU Emacs 26.0.50.1 (x86_64-pc-linux-gnu, GTK+ Version 3.22.4)
 of 2016-12-14
Repository revision: 0a5898c3dd2e32431268bc2bcf3536d4cd62ad39




^ permalink raw reply related	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2016-12-15  6:27 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2016-11-17 15:53 Ibuffer improvements: filtering, documentation, bug fix, tests Christopher Genovese
2016-11-18 13:08 ` Richard Stallman
2016-11-19 11:17 ` Tino Calancha
2016-11-22 23:45   ` Christopher Genovese
2016-11-26 10:53     ` Tino Calancha
2016-12-01  3:10       ` Christopher Genovese
2016-12-02 15:56         ` Tino Calancha
2016-12-09  1:00           ` Tino Calancha
2016-12-14 19:47             ` Christopher Genovese
2016-12-15  6:27               ` Tino Calancha

Code repositories for project(s) associated with this external index

	https://git.savannah.gnu.org/cgit/emacs.git
	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.