all messages for Emacs-related lists mirrored at yhetil.org
 help / color / mirror / code / Atom feed
From: Christopher Genovese <genovese@cmu.edu>
To: Tino Calancha <tino.calancha@gmail.com>
Cc: emacs-devel <emacs-devel@gnu.org>
Subject: Re: Ibuffer improvements: filtering, documentation, bug fix, tests
Date: Wed, 30 Nov 2016 22:10:58 -0500	[thread overview]
Message-ID: <CAPum5Fh2h3BoeF=GYmTJH=b5p2WVrQ7e4E5quwPobR3Z5p1XWQ@mail.gmail.com> (raw)
In-Reply-To: <alpine.DEB.2.20.1611261951360.4841@calancha-pc>


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


  reply	other threads:[~2016-12-01  3:10 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='CAPum5Fh2h3BoeF=GYmTJH=b5p2WVrQ7e4E5quwPobR3Z5p1XWQ@mail.gmail.com' \
    --to=genovese@cmu.edu \
    --cc=emacs-devel@gnu.org \
    --cc=tino.calancha@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.