branch: elpa/logview
commit 619dee5c9766490bd2389c1dedbd35fb7c173e3c
Author: Paul Pogonyshev <pogonys...@gmail.com>
Commit: Paul Pogonyshev <pogonys...@gmail.com>

    Initial commit.
---
 .gitignore |    1 +
 README.md  |  105 +++++
 logview.el | 1422 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1528 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..c531d9867f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.elc
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..834cb8642e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,105 @@
+# Logview mode
+
+Logview major mode for Emacs provides syntax highlighting, filtering
+and other features for various log files.  The main target are files
+similar to ones generated by Log4j, Logback and other Java logging
+libraries, but there is really nothing Java-specific in the mode and
+it should work just fine with any log that follows similar structure,
+probably after some configuration.
+
+The mode is meant to be operated in read-only buffer, so all the
+command bindings lack modifiers.
+
+Out-of-the-box the mode should be able to parse standard SLF4J (Log4j,
+Logback) files as long as they use ISO 8601 timestamps.
+
+
+### Submodes
+
+Since there is no standard log file format, Logview mode has to try
+and guess how the log file it operates on is formatted.  It does so by
+trying to parse the very first line of the file against various
+submodes it has.
+
+If it succeeds in guessing, you will see major mode specifed as
+‘Logview/...’ in the modeline, where the second part is the submode
+name.  In case it fails, you will see it complain in the echo area and
+the buffer will not be highlighted.
+
+#### What to do if Logview mode fails to guess format
+
+Currently your only option is to customize the mode.  You will want to
+add some entries to either ‘Additional Level Mappings’, ‘Additional
+Submodes’, ‘Additional Timestamp Formats’, or maybe to all three.
+
+All these variables are well-documented in customization interface.
+
+
+### Commands
+
+Nearly all commands have some use for prefix argument.  It can be
+usually just guessed, but you can always check individual command
+documentation within Emacs.
+
+#### Movement
+
+* All standard Emacs commands
+* Move to the beginning of entry’s message: `TAB`
+* Move to next / previous entry: `n` / `p`
+* Move to next / previous ‘as important’ [*] entry: `N` / `P` 
+* Move to first / last entry: `<` / `>`
+
+[*] ‘As important’ means entries with the same or higher level.  For
+    example, if the current entry is a warning, ‘as important’ include
+    errors and warnings.
+
+#### Narrowing and widening
+
+* Narrow from / up to current entry: `[` / `]`
+* Widen: `w`
+* Widen upwards / downwards only: `{` / `}`
+
+#### Filtering by entry level
+
+* Show only errors: `l 1` or `l e`
+* Show errors and warnings: `l 2` or `l w`
+* Show errors, warnings and information: `l 3` or `l i`
+* Show all levels except trace: `l 4` or `l d`
+* Show entries of all levels: `l 5` or `l t`
+* Show entries ‘as important’ as current one: `+` or `l +`
+
+#### Filtering by entry’s logger name or thread
+
+* Add name include / exclude filter: `a` / `A`
+* Add thread include / exclude filter: `t` / `T`
+
+#### Resetting filters
+
+* Reset level filter: `r l`
+* Reset name filters: `r a`
+* Reset thread filters: `r t`
+* Reset all filters: `R`
+* Reset all filters, widen and show all explicitly hidden entries: `r e`
+
+#### Explicitly hide or show individual entries
+
+* Hide one entry: `h`
+* Hide entries in the region: `H`
+* Show some explicitly hidden entries: `s`
+* Show explicitly hidden entries in the region: `S`
+
+In Transient Mark mode `h` and `s` operate on region when mark is
+active.
+
+#### Change options for current buffer
+
+This options can be customized globally and additionally temporarily
+changed in each individual buffer.
+
+* Toggle ‘copy only visible text’: `o v`
+* Toggle ‘show ellipses’: `o e`
+
+#### Miscellaneous
+
+* Bury buffer: `q`
+* Universal prefix commands are bound within modifiers: `u`, `-`, `0`..`9`
diff --git a/logview.el b/logview.el
new file mode 100644
index 0000000000..781d24b3b5
--- /dev/null
+++ b/logview.el
@@ -0,0 +1,1422 @@
+;;; logview.el --- Major mode for viewing log files  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015 Paul Pogonyshev
+
+;; Author:     Paul Pogonyshev <pogonys...@gmail.com>
+;; Maintainer: Paul Pogonyshev <pogonys...@gmail.com>
+;; Version:    0.1
+;; Keywords:   files, tools
+;; Homepage:   https://github.com/doublep/logview
+;; Package-Requires: ((emacs "24.5"))
+
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of
+;; the License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see http://www.gnu.org/licenses.
+
+
+;;; Commentary:
+
+;; Logview mode provides syntax highlighting, filtering and other
+;; features for various log files.  The main target are files similar
+;; to ones generated by Log4j, Logback and other Java logging
+;; libraries, but there is really nothing Java-specific in the mode
+;; and it should work just fine with any log that follows similar
+;; structure, probably after some configuration.
+
+
+;;; Code:
+
+(eval-when-compile (require 'cl))
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.log\\(?:\\.[0-9]+\\)?\\'" . logview-mode))
+
+
+
+;;; Customization.
+
+(defgroup logview nil
+  "Log viewing mode."
+  :group 'text)
+
+
+(defcustom logview-additional-submodes nil
+  "Association list of log submodes (file parsing rules).
+
+A few common submodes are already defined by the mode in variable
+`logview-std-submodes', but the ones you add here always take
+precedence.
+
+Submode definition has one required and several optional fields:
+
+format
+
+    The only mandatory and the most important field that defines
+    how log entries are built from pieces.  There are currently
+    four such supported pieces: \"TIMESTAMP\", \"LEVEL\", \"NAME\"
+    and \"THREAD\".  All four are optional.  For example, Log4j,
+    by default formats entries according to this pattern:
+
+        TIMESTAMP [THREAD] LEVEL NAME -
+
+levels  [may be optional]
+
+    Level mapping (see `logview-additional-level-mappings') used
+    for this submode.  This field is optional only if the submode
+    lacks levels altogether.
+
+    There are some predefined values valid for this field:
+    \"SLF4J\" (and its alises \"Log4j\", \"Log4j2\", \"Logback\"
+    and \"JUL\".  See variable `logview-std-level-mappings' for
+    details.
+
+timestamp  [optional]
+
+    If set, must be a list of timestamp format names to try (see
+    `logview-additional-timestamp-formats').  If not set or
+    empty, all defined timestamp formats will be tried.
+
+aliases  [optional]
+
+    Submode can have any number of optional aliases, which work just
+    as the name."
+  :group 'logview
+  :type  '(repeat (cons (string :tag "Name")
+                        (list :tag "Definition"
+                              (cons :tag "" (const :tag "Format:" format) 
string)
+                              (set :inline t
+                                   (cons :tag "" (const :tag "Level map:" 
levels)    string)
+                                   (cons :tag "" (const :tag "Timestamp:" 
timestamp) (repeat string))
+                                   (cons :tag "" (const :tag "Aliases:"   
aliases)   (repeat string)))))))
+
+(defcustom logview-additional-level-mappings nil
+  "Association list of log level mappings.
+
+A few common maps are already defined by the mode in variable
+`logview-std-level-mappings', but the ones you add here always
+take precedence.
+
+Each mapping has a name, by which it is referred from submode
+definition.  Mapping itself consists of five lists of strings:
+error levels, warning levels, information levels, debug levels
+and trace levels.  In these lists you should add all possible
+real levels that can appear in log file, in descending order of
+severity.
+
+For example, for Java SLF4J (Log4j, Logback, etc.) the mapping
+looks like this:
+
+        Error levels:        ERROR
+        Warning levels:      WARN
+        Information levels:  INFO
+        Trace levels:        TRACE
+        Debug levels:        DEBUG
+
+This is not a coincidence, as the mode is primarily targeted at
+SLF4J log files.
+
+However, mapping for JUL (java.util.logging) framework looks more
+complicated:
+
+        Error levels:        SEVERE
+        Warning levels:      WARNING
+        Information levels:  INFO
+        Trace levels:        CONFIG, FINE
+        Debug levels:        FINER, FINEST
+
+JUL has seven severity levels and we need to map them to five the
+mode supports.  So the last two lists contain two levels each.
+It is also legal to have empty lists, usually if there are less
+than five levels.
+
+Mapping can have any number of optional aliases, which work just
+as the name."
+  :group 'logview
+  :type  '(repeat (cons (string :tag "Name")
+                        (list :tag "Definition"
+                              (cons :tag "" (const :tag "Error levels:"       
error)       (repeat string))
+                              (cons :tag "" (const :tag "Warning levels:"     
warning)     (repeat string))
+                              (cons :tag "" (const :tag "Information levels:" 
information) (repeat string))
+                              (cons :tag "" (const :tag "Debug levels:"       
debug)       (repeat string))
+                              (cons :tag "" (const :tag "Trace levels:"       
trace)       (repeat string))
+                              (set :inline t
+                                   (cons :tag "" (const :tag "Aliases:" 
aliases) (repeat string)))))))
+
+(defcustom logview-additional-timestamp-formats nil
+  "Association list of additional timestamp formats.
+
+A few common formats are already defined by the mode in variable
+`logview-std-timestamp-formats', but the ones you add here always
+take precedence.
+
+Each format has a name, by which it can be referred from submode
+definition.  A format is defined simply by a regular expression
+timestamp must match.  It is strongly recommended to make the
+expression as strict as possible to avoid false positives.  For
+example, if you entered something like \"\\w+\" as an expression,
+this would often lead to Logview mode autoselecting wrong submode
+and thus parsing log files incorrectly.
+
+Timestamp format can have any number of optional aliases, which
+work just as the name."
+  :group 'logview
+  :type  '(repeat (cons (string :tag "Name")
+                        (list :tag "Definition"
+                              (cons :tag "" (const :tag "Format:"  regexp)  
regexp)
+                              (set :inline t
+                                   (cons :tag "" (const :tag "Aliases:" 
aliases) (repeat string)))))))
+
+
+(defcustom logview-copy-visible-text-only t
+  "Whether to copy, kill, etc. only visible selected text.
+Standard Emacs behavior is to copy even invisible text, but that
+typically doesn't make much sense with filtering.
+
+You can temporarily change this on per-buffer basis using
+`logview-toggle-copy-visible-text-only' command (normally bound
+to o v)."
+  :group 'logview
+  :type  'boolean)
+
+(defcustom logview-show-ellipses t
+  "Whether to show ellipses to indicate hidden log entries.
+
+You can temporarily change this on per-buffer basis using
+`logview-toggle-show-ellipses' command (normally bound to o e)."
+  :group 'logview
+  :type  'boolean)
+
+
+(defgroup logview-faces nil
+  "Faces for Logview mode."
+  :group 'logview)
+
+(defface logview-level-error
+  '((t :inherit error))
+  "Face to use for error level strings.")
+
+(defface logview-error-entry
+  '((((background dark))
+     :background "#600000")
+    (t
+     :background "#ffe0e0"))
+  "Face to use for error log entries."
+  :group 'logview-faces)
+
+(defface logview-level-warning
+  '((t :inherit warning))
+  "Face to use for warning level strings."
+  :group 'logview-faces)
+
+(defface logview-warning-entry
+  '((((background dark))
+     :background "#606000")
+    (t
+     :background "#ffffe0"))
+  "Face to use for warning log entries."
+  :group 'logview-faces)
+
+(defface logview-level-information
+  '((t :inherit success))
+  "Face to use for information level strings."
+  :group 'logview-faces)
+
+(defface logview-information-entry
+  '((((background dark))
+     :background "#004000")
+    (t
+     :background "#f8fff8"))
+  "Face to use for information log entries."
+  :group 'logview-faces)
+
+(defface logview-level-debug
+  nil
+  "Face to use for debug level strings."
+  :group 'logview-faces)
+
+(defface logview-debug-entry
+  nil
+  "Face to use for debug log entries."
+  :group 'logview-faces)
+
+(defface logview-level-trace
+  '((t :inherit shadow))
+  "Face to use for trace level strings."
+  :group 'logview-faces)
+
+(defface logview-trace-entry
+  '((((background dark))
+     :background "#404040")
+    (t
+     :background "#f4f4f4"))
+  "Face to use for trace log entries."
+  :group 'logview-faces)
+
+(defface logview-timestamp
+  '((t :inherit font-lock-builtin-face))
+  "Face to use for log entry timestamp."
+  :group 'logview-faces)
+
+(defface logview-name
+  '((t :inherit font-lock-string-face))
+  "Face to use for logger name."
+  :group 'logview-faces)
+
+(defface logview-thread
+  '((t :inherit font-lock-variable-name-face))
+  "Face to use for logger thread."
+  :group 'logview-faces)
+
+
+
+;;; Public variables.
+
+(defvar logview-std-submodes
+  '(("SLF4J" . ((format  . "TIMESTAMP [THREAD] LEVEL NAME - ")
+                (levels  . "SLF4J")
+                (aliases . ("Log4j" "Log4j2" "Logback")))))
+  "Alist of standard submodes.  This value is used as the
+fallback for customizable `logview-additional-submodes'.")
+
+(defvar logview-std-level-mappings
+  '(("SLF4J" . ((error       "ERROR")
+                (warning     "WARN")
+                (information "INFO")
+                (debug       "DEBUG")
+                (trace       "TRACE")
+                (aliases     "Log4j" "Log4j2" "Logback")))
+    ("JUL"   . ((error       "ERROR")
+                (warning     "WARNING")
+                (information "INFO")
+                (debug       "CONFIG" "FINE")
+                (trace       "FINER" "FINEST"))))
+  "Alist of standard mappings of actual log levels to mode's
+final levels.  This value is used as the fallback for
+customizable `logview-additional-level-mappings'.")
+
+(defvar logview-std-timestamp-formats
+  '(("ISO 8601 datetime + millis"  . (; Silently handle both common decimal 
separators (dot and comma).
+                                      (regexp  . 
"[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} 
[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}[.,][0-9]\\{3\\}")
+                                      (aliases . ("yyyy-MM-dd HH:mm:ss.SSS"))))
+    ("ISO 8601 datetime"           . ((regexp  . 
"[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} [0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}")
+                                      (aliases . ("yyyy-MM-dd HH:mm:ss"))))
+    ("ISO 8601 time only + millis" . (; Silently handle both common decimal 
separators (dot and comma).
+                                      (regexp  . 
"[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}[.,][0-9]\\{3\\}")
+                                      (aliases . ("HH:mm:ss.SSS"))))
+    ("ISO 8601 time only"          . ((regexp  . 
"[0-9]\\{2\\}:[0-9]\\{2\\}:[0-9]\\{2\\}")
+                                      (aliases . ("HH:mm:ss")))))
+  "Alist of standard timestamp formats.  This is the fallback for
+customizable `logview-additional-timestamp-formats'.")
+
+
+
+;;; Internal variables and constants.
+
+(defconst logview--timestamp-group 1)
+(defconst logview--level-group     2)
+(defconst logview--name-group      3)
+(defconst logview--thread-group    4)
+
+(defconst logview--final-levels '(error warning information debug trace))
+
+(defconst logview--entry-parts '("TIMESTAMP" "LEVEL" "NAME" "THREAD"))
+(defconst logview--entry-part-regexp (rx (or (group bow "TIMESTAMP" eow)
+                                             (group bow "LEVEL" eow)
+                                             (group bow "NAME" eow)
+                                             (group bow "THREAD" eow))))
+
+
+(defvar logview--entry-regexp)
+(make-variable-buffer-local 'logview--entry-regexp)
+
+(defvar logview--submode-features)
+(make-variable-buffer-local 'logview--submode-features)
+
+(defvar logview--submode-level-alist nil
+  "Submode levels, least to most important, mapped to final
+levels.")
+(make-variable-buffer-local 'logview--submode-level-alist)
+
+(defvar logview--submode-level-data nil
+  "An alist of level string to the following lists:
+- level symbol (for quick filtering);
+- level entry face;
+- level string face.")
+(make-variable-buffer-local 'logview--submode-level-data)
+
+(defvar logview--min-shown-level)
+(make-variable-buffer-local 'logview--min-shown-level)
+
+(defvar logview--as-important-levels)
+(make-variable-buffer-local 'logview--as-important-levels)
+
+(defvar logview--applied-filters '(nil nil nil nil))
+(make-variable-buffer-local 'logview--applied-filters)
+
+(defvar logview--include-name-regexps)
+(make-variable-buffer-local 'logview--include-name-regexps)
+
+(defvar logview--exclude-name-regexps)
+(make-variable-buffer-local 'logview--exclude-name-regexps)
+
+(defvar logview--include-thread-regexps)
+(make-variable-buffer-local 'logview--include-thread-regexps)
+
+(defvar logview--exclude-thread-regexps)
+(make-variable-buffer-local 'logview--exclude-thread-regexps)
+
+(defvar logview--name-regexp-history)
+(defvar logview--thread-regexp-history)
+
+(defvar logview--process-buffer-changes)
+(make-variable-buffer-local 'logview--process-buffer-changes)
+
+
+
+;;; The mode.
+
+(defvar logview-mode-map
+  (let ((map (make-sparse-keymap)))
+    (dolist (binding '(;; Movement commands.
+                       ("TAB" logview-go-to-message-beginning)
+                       ("n"   logview-next-entry)
+                       ("p"   logview-previous-entry)
+                       ("N"   logview-next-as-important-entry)
+                       ("P"   logview-previous-as-important-entry)
+                       ("<"   logview-first-entry)
+                       (">"   logview-last-entry)
+                       ;; Narrowing/widening commands.
+                       ("["   logview-narrow-from-this-entry)
+                       ("]"   logview-narrow-up-to-this-entry)
+                       ("w"   widen)
+                       ("{"   logview-widen-upwards)
+                       ("}"   logview-widen-downwards)
+                       ;; Filtering by level commands.
+                       ("l 1" logview-show-only-errors)
+                       ("l e" logview-show-only-errors)
+                       ("l 2" logview-show-errors-and-warnings)
+                       ("l w" logview-show-errors-and-warnings)
+                       ("l 3" logview-show-errors-warnings-and-information)
+                       ("l i" logview-show-errors-warnings-and-information)
+                       ("l 4" 
logview-show-errors-warnings-information-and-debug)
+                       ("l d" 
logview-show-errors-warnings-information-and-debug)
+                       ("l 5" logview-show-all-levels)
+                       ("l t" logview-show-all-levels)
+                       ("+"   logview-show-only-as-important)
+                       ("l +" logview-show-only-as-important)
+                       ;; Filtering by name/thread commands.
+                       ("a"   logview-add-include-name-filter)
+                       ("A"   logview-add-exclude-name-filter)
+                       ("t"   logview-add-include-thread-filter)
+                       ("T"   logview-add-exclude-thread-filter)
+                       ;; Filter resetting commands.
+                       ("r l" logview-reset-level-filters)
+                       ("r a" logview-reset-name-filters)
+                       ("r t" logview-reset-thread-filters)
+                       ("R"   logview-reset-all-filters)
+                       ("r e" 
logview-reset-all-filters-restrictions-and-hidings)
+                       ;; Explicit entry hiding/showing commands.
+                       ("h"   logview-hide-entry)
+                       ("H"   logview-hide-region-entries)
+                       ("s"   logview-show-entries)
+                       ("S"   logview-show-region-entries)
+                       ;; Option changing commands.
+                       ("o v" logview-toggle-copy-visible-text-only)
+                       ("o e" logview-toggle-show-ellipses)
+                       ;; Miscellaneous commands.
+                       ("?"   logview-mode-help)
+                       ("q"   bury-buffer)
+                       ;; Simplified universal argument command rebindings.
+                       ("u"   universal-argument)
+                       ("-"   negative-argument)
+                       ("0"   digit-argument)
+                       ("1"   digit-argument)
+                       ("2"   digit-argument)
+                       ("3"   digit-argument)
+                       ("4"   digit-argument)
+                       ("5"   digit-argument)
+                       ("6"   digit-argument)
+                       ("7"   digit-argument)
+                       ("8"   digit-argument)
+                       ("9"   digit-argument)))
+      (define-key map (kbd (car binding)) (cadr binding)))
+    map))
+
+
+;;;###autoload
+(define-derived-mode logview-mode nil "Logview"
+  "Major mode for viewing and filtering various log files."
+  (logview--update-invisibility-spec)
+  (logview--guess-submode)
+  (logview--split-region-into-entries (point-min) (point-max) 'report-progress)
+  (add-hook 'after-change-functions 'logview--split-region-into-entries t t)
+  (read-only-mode 1)
+  (set (make-local-variable 'filter-buffer-substring-function) 
'logview--buffer-substring-filter)
+  (add-hook 'change-major-mode-hook 'logview--exiting-mode nil t))
+
+(defun logview--exiting-mode ()
+  ;; Remove custom invisibility property values, as otherwise other
+  ;; modes will show empty buffers.  Also remove face property, as we
+  ;; set it ourselves, not through font-lock.
+  (logview--std-matching-and-altering
+    (remove-text-properties 1 (1+ (buffer-size)) '(face nil invisible nil))))
+
+(defun logview--guess-submode ()
+  (save-excursion
+    (save-restriction
+      (widen)
+      (goto-char 1)
+      (end-of-line)
+      (let ((first-line (buffer-substring 1 (point))))
+        (catch 'success
+          (logview--iterate-split-alists (lambda (name definition)
+                                           (condition-case error
+                                               (logview--initialize-submode 
name definition first-line)
+                                             (error (warn 
(error-message-string error)))))
+                                         logview-additional-submodes 
logview-std-submodes)
+          (message "Logview mode was unable to determine log format; please 
consult documentation"))))))
+
+
+
+;;; Movement commands.
+
+(defun logview-go-to-message-beginning (&optional select-message)
+  "Put point at the beginning of the current entry's message.
+
+With prefix argument, additionally put mark at the end of the
+message, which is especially useful for multiline messages.  In
+Transient Mark mode also activate the region."
+  (interactive "P")
+  (logview--assert)
+  (let ((case-fold-search nil))
+    (when (logview--match-current-entry)
+      (goto-char (match-end 0))
+      (when select-message
+        (save-excursion
+          (push-mark (logview--linefeed-back (if (equal 
(logview--match-successive-entries 1) 0)
+                                                 (match-beginning 0)
+                                               (point-max)))
+                     t t))))))
+
+(defun logview-next-entry (&optional n)
+  "Move point vertically down N (1 by default) log entries.
+
+Point is positioned at the beginning of the message of the
+resulting entry.  If log entries are single-line, this is almost
+equal to `next-line'.  However, if messages span several lines,
+the function will have significantly different effect."
+  (interactive "p")
+  (logview--assert)
+  (when (/= n 0)
+    (let ((case-fold-search nil)
+          (original-point   (point))
+          (remaining        (logview--match-successive-entries n t)))
+      (goto-char (if remaining (match-end 0) original-point))
+      (logview--maybe-complain-about-movement n remaining))))
+
+(defun logview-previous-entry (&optional n)
+  "Move point vertically up N (1 by default) log entries.
+
+Point is positioned at the beginning of the message of the
+resulting entry.  If log entries are single-line, this is almost
+equal to `next-line'.  However, if messages span several lines,
+the function will have significantly different effect."
+  (interactive "p")
+  (logview-next-entry (if n (- n) -1)))
+
+(defun logview-next-as-important-entry (&optional n)
+  "Move point vertically down N 'as important' entries.
+
+Here 'as important' means any entry of level equal or higher than
+that of the current entry.  For example, if you start moving from
+a warning, the function will stop on all warnings and errors in
+the buffer, but skip all other 'less important' entries.  If the
+last used command is either `logview-next-as-important-entry' or
+`logview-previous-as-important-entry', list of what is considered
+'as important' is kept, otherwise it is recomputed anew.
+
+Point is positioned at the beginning of the message of the
+resulting entry."
+  (interactive "p")
+  (logview--assert 'level)
+  (when (/= n 0)
+    (let ((case-fold-search nil)
+          (original-point   (point)))
+      (unless (memq last-command '(logview-next-as-important-entry 
logview-previous-as-important-entry))
+        (setq logview--as-important-levels nil)
+        (logview--match-current-entry)
+        (let ((this-level (match-string logview--level-group))
+              (found))
+          (dolist (level-pair logview--submode-level-alist)
+            (unless found
+              (setq found (equal (car level-pair) this-level)))
+            (when found
+              (push (car level-pair) logview--as-important-levels)))))
+      (let ((remaining (logview--match-successive-entries
+                        n t (lambda ()
+                              (member (match-string logview--level-group) 
logview--as-important-levels)))))
+        (goto-char (if remaining (match-end 0) original-point))
+        (logview--maybe-complain-about-movement n remaining t)))))
+
+(defun logview-previous-as-important-entry (&optional n)
+  "Move point vertically up N 'as important' entries.
+
+See function `logview-next-as-important-entry' for definition of
+'as important'.
+
+Point is positioned at the beginning of the message of the
+resulting entry."
+  (interactive "p")
+  (logview-next-as-important-entry (if n (- n) -1)))
+
+(defun logview-first-entry ()
+  "Move point to the first log entry.
+
+Point is positioned at the beginning of the message of the entry.
+Otherwise this function is similar to `beginning-of-buffer'."
+  (interactive)
+  (logview--assert)
+  (unless (region-active-p)
+    (push-mark))
+  (goto-char (point-min))
+  (let ((case-fold-search nil))
+    (when (logview--match-current-entry)
+      (goto-char (match-end 0)))))
+
+(defun logview-last-entry ()
+  "Move point to the last log entry.
+
+Point is positioned at the beginning of the message of the entry.
+If the last entry is multiline, this makes the function quite
+different from `end-of-buffer'."
+  (interactive)
+  (logview--assert)
+  (unless (region-active-p)
+    (push-mark))
+  (goto-char (point-max))
+  (let ((case-fold-search nil))
+    (when (logview--match-current-entry)
+      (goto-char (match-end 0)))))
+
+
+
+;;; Narrowing/widening commands.
+
+(defun logview-narrow-from-this-entry (&optional n)
+  "Narrow the buffer so that previous log entries are hidden.
+
+If invoked interactively with a prefix argument, leave that many
+entries above the current visible after narrowing.  Note that as
+an exception to standard numeric prefix value rules, here no
+prefix means zero."
+  (interactive (list (when current-prefix-arg
+                       (prefix-numeric-value current-prefix-arg))))
+  (logview--do-narrow-one-side t n))
+
+(defun logview-narrow-up-to-this-entry (&optional n)
+  "Narrow the buffer so that following log entries are hidden.
+
+If invoked interactively with a prefix argument, leave that many
+entries under the current visible after narrowing.  Note that as
+an exception to standard numeric prefix value rules, here no
+prefix means zero."
+  (interactive (list (when current-prefix-arg
+                       (prefix-numeric-value current-prefix-arg))))
+  (logview--do-narrow-one-side nil n))
+
+(defun logview--do-narrow-one-side (upwards n)
+  (logview--assert)
+  (let ((from (point-min))
+        (to   (point-max)))
+    (widen)
+    (logview--std-matching
+      (narrow-to-region (if (and upwards (equal 
(logview--match-successive-entries (if n (- n) 0) t) 0))
+                            (match-beginning 0)
+                          from)
+                        (if (and (not upwards) (equal 
(logview--match-successive-entries (if n (1+ n) 1) t) 0))
+                            (match-beginning 0)
+                          to)))))
+
+(defun logview-widen-upwards ()
+  "Widen the buffer only upwards, i.e. keep the bottom restriction."
+  (interactive)
+  (let ((to (point-max)))
+    (widen)
+    (narrow-to-region (point-min) to)))
+
+(defun logview-widen-downwards ()
+  "Widen the buffer only downwards, i.e. keep the top restriction."
+  (interactive)
+  (let ((from (point-min)))
+    (widen)
+    (narrow-to-region from (point-max))))
+
+
+
+;;; Filtering by level commands.
+
+(defun logview-show-only-errors ()
+  "Show only error entries."
+  (interactive)
+  (logview--set-min-level (logview--find-min-level 'error)))
+
+(defun logview-show-errors-and-warnings ()
+  "Show only error and warning entries."
+  (interactive)
+  (logview--set-min-level (logview--find-min-level 'warning)))
+
+(defun logview-show-errors-warnings-and-information ()
+  "Show error, warning and information entries."
+  (interactive)
+  (logview--set-min-level (logview--find-min-level 'information)))
+
+(defun logview-show-errors-warnings-information-and-debug ()
+  "Show error, warning, information and debug entries.  I.e. all
+entries other than traces."
+  (interactive)
+  (logview--set-min-level (logview--find-min-level 'debug)))
+
+(defun logview-show-all-levels ()
+  "Show entries of all levels.  This doesn't cancel other filters
+that might be in effect though."
+  (interactive)
+  (logview--set-min-level (logview--find-min-level 'trace)))
+
+(defun logview-show-only-as-important ()
+  "Show entries 'as important' as the current.
+
+Here 'as important' means any entry of level equal or higher.
+For example, if you invoke this function while current entry is a
+warning, all entries other than warnings and errors will be
+hidden."
+  (interactive)
+  (logview--assert 'level)
+  (logview--std-matching
+    (when (logview--match-current-entry)
+      (logview--set-min-level (match-string logview--level-group)))))
+
+(defun logview--find-min-level (final-level)
+  "Find minimal submode level that maps to given FINAL-LEVEL or higher."
+  (logview--assert 'level)
+  (let ((result)
+        (final-level-index (cl-position final-level logview--final-levels)))
+    (dolist (level-pair logview--submode-level-alist)
+      (when (and (null result) (<= (cl-position (cdr level-pair) 
logview--final-levels :test 'equal) final-level-index))
+        (setq result (car level-pair))))
+    result))
+
+(defun logview--set-min-level (min-level)
+  (setq logview--min-shown-level min-level)
+  (logview--update-invisibility-spec))
+
+
+
+;;; Filtering by name/thread commands.
+
+(defun logview-add-include-name-filter ()
+  "Show only log entries with name matching entered regular
+expression.  If this command is invoked multiple times, show
+entries with name matching at least one of entered expression."
+  (interactive)
+  (logview--prompt-for-new-filter "Logger name regexp to show entries" 'name 
'logview--include-name-regexps))
+
+(defun logview-add-exclude-name-filter ()
+  "Show only log entries with name that doesn't match entered
+regular expression.  If this command is invoked multiple times,
+show entries with name that doesn't match any of entered
+expression."
+  (interactive)
+  (logview--prompt-for-new-filter "Logger name regexp to hide entries" 'name 
'logview--exclude-name-regexps))
+
+(defun logview-add-include-thread-filter ()
+  "Show only log entries with thread name matching entered
+regular expression.  If this command is invoked multiple times,
+show entries with thread name matching at least one of entered
+expression."
+  (interactive)
+  (logview--prompt-for-new-filter "Thread regexp to show entries" 'thread 
'logview--include-thread-regexps))
+
+(defun logview-add-exclude-thread-filter ()
+  "Show only log entries with thread name that doesn't match
+entered regular expression.  If this command is invoked multiple
+times, show entries with thread name that doesn't match any of
+entered expression."
+  (interactive)
+  (logview--prompt-for-new-filter "Thread regexp to hide entries" 'thread 
'logview--exclude-thread-regexps))
+
+(defun logview--prompt-for-new-filter (prompt type filter-list)
+  (logview--assert type)
+  (logview--std-matching
+    (let* ((default-value (when (logview--match-current-entry)
+                            (let ((base (regexp-quote (match-string (cdr (assq 
type (list (cons 'name   logview--name-group)
+                                                                               
           (cons 'thread logview--thread-group))))))))
+                              (list base (format "^%s$" base)))))
+           (regexp        (read-regexp prompt default-value (cdr (assq type 
'((name   . logview--name-regexp-history)
+                                                                              
(thread . logview--thread-regexp-history)))))))
+      (unless (logview--valid-regexp-p regexp)
+        (error "Invalid regular expression"))
+      (set filter-list (cons regexp (symbol-value filter-list)))
+      (logview--apply-filters))))
+
+;; This must have been a standard function.
+(defun logview--valid-regexp-p (regexp)
+  (ignore-errors
+    (string-match regexp "")
+    t))
+
+
+
+;;; Filters resetting commands.
+
+(defun logview-reset-level-filters ()
+  "Reset all level filters.
+
+This is actually the same as `logview-show-all-levels'."
+  (interactive)
+  (logview--assert 'level)
+  (logview-show-all-levels))
+
+(defun logview-reset-name-filters ()
+  "Reset all name filters."
+  (interactive)
+  (logview--assert 'name)
+  (setq logview--include-name-regexps nil
+        logview--exclude-name-regexps nil)
+  (logview--apply-filters))
+
+(defun logview-reset-thread-filters ()
+  "Reset all thread filters."
+  (interactive)
+  (logview--assert 'thread)
+  (setq logview--include-thread-regexps nil
+        logview--exclude-thread-regexps nil)
+  (logview--apply-filters))
+
+(defun logview-reset-all-filters ()
+  "Reset all filters (level, name, thread).  After this command
+only explictly hidden entries remain invisible."
+  (interactive)
+  (logview--do-reset-all-filters nil))
+
+(defun logview-reset-all-filters-restrictions-and-hidings ()
+  "Reset all filters, show all explictly hidden entries and
+cancel any narrowing restrictions."
+  (interactive)
+  (widen)
+  (logview--do-reset-all-filters t))
+
+(defun logview--do-reset-all-filters (also-cancel-explicit-hiding)
+  (logview--assert)
+  (when (memq 'level logview--submode-features)
+    (logview-reset-level-filters))
+  (when (or (memq 'name logview--submode-features) (memq 'thread 
logview--submode-features) also-cancel-explicit-hiding)
+    (setq logview--include-name-regexps   nil
+          logview--exclude-name-regexps   nil
+          logview--include-thread-regexps nil
+          logview--exclude-thread-regexps nil)
+    (logview--apply-filters also-cancel-explicit-hiding)))
+
+
+
+;;; Explicit entry hiding/showing commands.
+
+(defun logview-hide-entry (&optional n)
+  "Explicitly hide N currently visible entries starting at point.
+If N is negative, hide -N previous entries instead, not including
+the current.
+
+In Transient Mark mode, if the region is active and this command
+is invoked without prefix argument, hide all entries in the
+region instead (i.e. just like `logview-hide-region-entries')."
+  (interactive (list (if (or current-prefix-arg (not (use-region-p)))
+                         (prefix-numeric-value current-prefix-arg)
+                       'use-region)))
+  (if (eq n 'use-region)
+      (logview-hide-region-entries (point) (mark))
+    (logview--assert)
+    (logview--std-matching-and-altering
+      (logview--maybe-complain-about-movement
+       n (logview--iterate-successive-entries n (logview--hide-entry-callback 
'logview-hidden-entry) t) 0))))
+
+(defun logview-hide-region-entries (begin end)
+  "Explicitly hide all log entries that are fully or partially in
+the region.
+
+Note that this includes entries that are currently hidden due to
+filtering too.  If you later cancel filtering, all entries in the
+region will remain hidden until you also cancel the explicit
+hiding."
+  (interactive "r")
+  (logview--assert)
+  (logview--std-matching-and-altering
+    (logview--iterate-entries-in-region begin end 
(logview--hide-entry-callback 'logview-hidden-entry))) )
+
+(defun logview-show-entries (&optional n)
+  "Show explicitly hidden entries between the current entry and
+N'th after it (or before it if N is negative).
+
+In Transient Mark mode, if the region is active and this command
+is invoked without prefix argument, show explicitly hidden
+entries in the region instead (i.e. work just like
+`logview-show-region-entries')."
+  (interactive (list (if (or current-prefix-arg (not (use-region-p)))
+                         (prefix-numeric-value current-prefix-arg)
+                       'use-region)))
+  (if (eq n 'use-region)
+      (logview-show-region-entries (point) (mark))
+    (logview--assert)
+    ;; Much like 'logview--iterate-successive-entries', but because of
+    ;; peculiar semantics, not broken out into its own function.
+    (when (/= n 0)
+      (save-excursion
+        (logview--std-matching-and-altering
+          (let ((direction (cl-signum n))
+                (shower    (logview--show-entry-callback 
'logview-hidden-entry)))
+            (funcall (if (< n 0)
+                         'logview--iterate-entries-backward
+                       ;; To "not count" the current entry.
+                       (setq n (1+ n))
+                       'logview--iterate-entries-forward)
+                     (lambda (begin after-first-line entry-end)
+                       (if (invisible-p begin)
+                           (progn
+                             (funcall shower begin after-first-line entry-end)
+                             t)
+                         (/= (setq n (- n direction)) 0))))))))
+    (logview--maybe-complain-about-movement n n)))
+
+(defun logview-show-region-entries (begin end)
+  (interactive "r")
+  (logview--assert)
+  (logview--std-matching-and-altering
+    (logview--iterate-entries-in-region begin end 
(logview--show-entry-callback 'logview-hidden-entry))) )
+
+
+
+;;; Option changing commands.
+
+(defun logview-toggle-copy-visible-text-only (&optional arg)
+  "Toggle `logview-copy-visible-text-only' just for this buffer.
+
+If invoked with prefix argument, enable the option if the
+argument is positive, disable it otherwise."
+  (interactive (list (or current-prefix-arg 'toggle)))
+  (logview--toggle-option-locally 'logview-copy-visible-text-only arg 
(called-interactively-p 'interactive)
+                                  "Will copy only visible text now"
+                                  "Copying commands will behave as in the rest 
of Emacs"))
+
+(defun logview-toggle-show-ellipses (&optional arg)
+  "Toggle `logview-show-ellipses' just for this buffer.
+
+If invoked with prefix argument, enable the option if the
+argument is positive, disable it otherwise."
+  (interactive (list (or current-prefix-arg 'toggle)))
+  (logview--toggle-option-locally 'logview-show-ellipses arg 
(called-interactively-p 'interactive)
+                                  "Showing ellipses to indicate hidden log 
entries"
+                                  "Hidden log entries are completely 
invisible")
+  (logview--update-invisibility-spec))
+
+(defun logview--toggle-option-locally (variable arg &optional show-message 
message-if-true message-if-false)
+  (set (make-local-variable variable)
+       (if (eq arg 'toggle)
+           (not (symbol-value variable))
+         (> (prefix-numeric-value arg) 0)))
+  (when show-message
+    (message (if (symbol-value variable) message-if-true message-if-false))))
+
+
+
+;;; Miscellaneous commands.
+
+(defun logview-mode-help ()
+  (interactive)
+  )
+
+
+
+;;; Internal functions (except helpers for specific command groups).
+
+(defun logview--initialize-submode (name definition test-line)
+  (let* ((format    (cdr (assq 'format    definition)))
+         (timestamp (cdr (assq 'timestamp definition))))
+    (unless (and (stringp format) (> (length format) 0))
+      (error "Invalid submode '%s': no format string" name))
+    (catch 'failed
+      (if timestamp
+          (dolist (name timestamp)
+            (logview--try-initialize-submode name definition format
+                                             (logview--get-split-alists name 
"timestamp format"
+                                                                        
logview-additional-timestamp-formats logview-std-timestamp-formats)
+                                             test-line))
+        (logview--iterate-split-alists (lambda (_timestamp-name timestamp)
+                                         (logview--try-initialize-submode name 
definition format timestamp test-line))
+                                       logview-additional-timestamp-formats 
logview-std-timestamp-formats)))))
+
+(defun logview--try-initialize-submode (name submode format timestamp 
test-line)
+  (let* ((search-from 0)
+         (next)
+         (end)
+         (terminator)
+         (levels)
+         (parts '("^"))
+         (features)
+         (add-text-part (lambda (from to)
+                          (push (replace-regexp-in-string "[ \t]+" "[ \t]+" 
(regexp-quote (substring format from to))) parts))))
+    (while (setq next (string-match logview--entry-part-regexp format 
search-from))
+      (when (> next search-from)
+        (funcall add-text-part search-from next))
+      (setq end        (match-end 0)
+            terminator (when (< end (length format))
+                         (aref format end)))
+      (cond ((match-beginning logview--timestamp-group)
+             (push (format "\\(?%d:%s\\)" logview--timestamp-group (cdr (assq 
'regexp timestamp))) parts)
+             (push 'timestamp features))
+            ((match-beginning logview--level-group)
+             (setq levels (logview--get-split-alists (cdr (assq 'levels 
submode)) "level mapping"
+                                                     
logview-additional-level-mappings logview-std-level-mappings))
+             (push (format "\\(?%d:%s\\)" logview--level-group
+                           (regexp-opt (apply 'append (mapcar (lambda 
(final-level) (cdr (assq final-level levels)))
+                                                              
logview--final-levels))))
+                   parts)
+             (push 'level features))
+            (t
+             (dolist (k (list logview--name-group logview--thread-group))
+               (when (match-beginning k)
+                 (push (format "\\(?%d:%s*\\)" k (cond ((and terminator (/= 
terminator ? ))
+                                                        (format "[^%c]*" 
terminator))
+                                                       (terminator
+                                                        "[^ \t]+")
+                                                       (t
+                                                        ".+")))
+                       parts)
+                 (push (if (= k logview--name-group) 'name 'thread) 
features)))))
+      (setq search-from end))
+    (when (< search-from (length format))
+      (funcall add-text-part search-from nil))
+    (let ((regexp (apply 'concat (reverse parts))))
+      (if (string-match regexp test-line)
+          (progn
+            (setq logview--process-buffer-changes t
+                  logview--entry-regexp           regexp
+                  logview--submode-features       features
+                  logview--submode-level-alist    nil
+                  mode-name                       (format "Logview/%s" name))
+            (when (memq 'level features)
+              (dolist (final-level logview--final-levels)
+                (dolist (level (cdr (assoc final-level levels)))
+                  (setq logview--submode-level-alist (cons (cons level 
final-level) logview--submode-level-alist))
+                  (push (cons level (list (make-symbol level)
+                                          (intern (format "logview-%s-entry" 
(symbol-name final-level)))
+                                          (intern (format "logview-level-%s" 
(symbol-name final-level)))))
+                        logview--submode-level-data))))
+            (throw 'success nil))
+        (when (not (memq 'timestamp features))
+          ;; Else we will maybe retry with different timestamp formats.
+          (throw 'failed nil))))))
+
+
+(defun logview--assert (&rest assertions)
+  (unless logview--entry-regexp
+    (error "Logview mode was unable to determine log format; please consult 
documentation"))
+  (dolist (assertion assertions)
+    (unless (memq assertion logview--submode-features)
+      (error (cdr (assq assertion '((level  . "Log lacks entry levels")
+                                    (name   . "Log lacks logger names")
+                                    (thread . "Log doesn't include thread 
names"))))))))
+
+
+(defun logview--maybe-complain-about-movement (direction remaining &optional 
as-important-entries)
+  ;; Using 'equal' since 'remaining' may also be nil.
+  (unless (equal remaining 0)
+    (error (if as-important-entries
+               (if (> direction 0) "No next (visible) as important entry" "No 
previous (visible) as important entry")
+             (if (> direction 0) "No next (visible) entry" "No previous 
(visible) entry")))))
+
+
+(defmacro logview--std-matching (&rest body)
+  (declare (indent 0) (debug t))
+  `(save-excursion
+     (let ((case-fold-search nil))
+       ,@body)))
+
+(defmacro logview--std-matching-and-altering (&rest body)
+  (declare (indent 0) (debug t))
+  `(save-excursion
+     (let ((logview--process-buffer-changes nil)
+           (case-fold-search                nil)
+           (inhibit-read-only               t))
+       (with-silent-modifications
+         ,@body))))
+
+
+(defun logview--match-current-entry ()
+  "Match the header of the log entry where the point currently is.
+
+Return value is non-nil on success.  Point is either before or
+after the header, i.e. still in the same log entry, but there are
+no more guarantees.  Match data is set appropriately for the
+header."
+  (forward-line 0)
+  (or (looking-at logview--entry-regexp)
+      (re-search-backward logview--entry-regexp nil t)
+      (re-search-forward  logview--entry-regexp nil t)))
+
+(defun logview--match-successive-entries (n &optional only-visible validator)
+  "Match N entries after (if N is positive) or before (negative)
+the current one.  If N is zero, match just the current entry.
+
+If ONLY-VISIBLE is non-nil, hidden entries are skipped.  If
+VALIDATOR is non-nil, entries for which the function returns nil
+are skipped too.
+
+Returns the remaining number, i.e. zero if there are enough valid
+entries.  If it never found any valid entries, returns nil.
+There is no guarantees about point location after the call, but
+match data will be set for the last valid matched header."
+  (let* ((forward          (> n 0))
+         (direction        (cl-signum n))
+         (successful-match '(nil)))
+    (when (logview--match-current-entry)
+      (when (or (null validator) (funcall validator))
+        (match-data t successful-match))
+      (when (/= n 0)
+        (while (and (= (forward-line direction) 0)
+                    (if forward
+                        (re-search-forward logview--entry-regexp nil t)
+                      (or (looking-at logview--entry-regexp)
+                          (re-search-backward logview--entry-regexp nil t)))
+                    (if (or (and only-visible (invisible-p (match-beginning 
0)))
+                            (and validator (not (funcall validator))))
+                        t
+                      (match-data t successful-match)
+                      (/= (setq n (- n direction)) 0))))))
+    (if (equal successful-match '(nil))
+        nil
+      (set-match-data successful-match)
+      n)))
+
+
+(defun logview--iterate-entries-forward (callback &optional only-visible 
validator)
+  "Invoke CALLBACK for successive valid log entries starting at
+point and forward, until it returns nil or end of buffer is
+reached.
+
+CALLBACK is called with three arguments: beginning of the entry,
+end of its first line and its end (the last two are equal unless
+the entry spans multiple lines).  CALLBACK may not access match
+data and must make sure point and match data are preserved.
+
+If ONLY-VISIBLE is non-nil, hidden entries are skipped.  If
+VALIDATOR is non-nil, entries for which the function returns nil
+are skipped too."
+  (when (logview--match-current-entry)
+    (let ((entry-begin (match-beginning 0))
+          (after-first-line)
+          (entry-end)
+          (limit       (point-max))
+          (invalid))
+      (while (progn
+               (setq invalid (or (and only-visible (invisible-p entry-begin))
+                                 (and validator    (not (funcall validator)))))
+               (forward-line)
+               (setq after-first-line (point)
+                     entry-end        (if (re-search-forward 
logview--entry-regexp nil t)
+                                          (match-beginning 0)
+                                        limit))
+               (when (or invalid (funcall callback entry-begin 
after-first-line entry-end))
+                 (/= (setq entry-begin entry-end) limit)))))))
+
+(defun logview--iterate-entries-backward (callback &optional only-visible 
validator)
+  "Invoke CALLBACK for successive valid log entries before the
+current entry until it returns nil or beginning of buffer is
+reached.
+
+See `logview--iterate-entries-forward' for details."
+  (when (logview--match-current-entry)
+    (let ((entry-begin (match-beginning 0))
+          (entry-end))
+      (while (and (= (forward-line -1) 0)
+                  (or (looking-at logview--entry-regexp)
+                      (re-search-backward logview--entry-regexp nil t))
+                  (progn
+                    (setq entry-end   entry-begin
+                          entry-begin (match-beginning 0))
+                    (or (and only-visible (invisible-p entry-begin))
+                        (and validator    (not (funcall validator)))
+                        (when (funcall callback entry-begin (progn 
(forward-line) (point)) entry-end)
+                          (goto-char entry-begin)))))))))
+
+(defun logview--iterate-successive-entries (n callback &optional only-visible 
validator)
+  (when (/= n 0)
+    (let ((direction (cl-signum n)))
+      (funcall (if (> n 0) 'logview--iterate-entries-forward 
'logview--iterate-entries-backward)
+               (lambda (begin after-first-line entry-end)
+                 (funcall callback begin after-first-line entry-end)
+                 (/= (setq n (- n direction)) 0))
+               only-visible validator)))
+  n)
+
+(defun logview--iterate-entries-in-region (begin end callback &optional 
only-visible validator)
+  (goto-char (min begin end))
+  (let ((limit (max begin end)))
+    (logview--iterate-entries-forward (lambda (begin after-first-line end)
+                                        (funcall callback begin 
after-first-line end)
+                                        (< end limit))
+                                      only-visible validator)))
+
+
+(defun logview--update-invisibility-spec ()
+  (let ((invisibility-spec '(logview-filtered logview-hidden-entry 
logview-hidden-details))
+        (found nil))
+    ;; Initially it's nil.
+    (when logview--min-shown-level
+      (dolist (level-pair logview--submode-level-alist)
+        (when (string= (car level-pair) logview--min-shown-level)
+          (setq found t))
+        (unless found
+          (setq invisibility-spec (cons (nth 0 (cdr (assoc (car level-pair) 
logview--submode-level-data))) invisibility-spec)))))
+    (setq buffer-invisibility-spec
+          (if logview-show-ellipses
+              (mapcar (lambda (x) (cons x t)) invisibility-spec)
+            invisibility-spec))
+    ;; This weird looking command was suggested in
+    ;; irc.freenode.net#emacs and seems to force buffer redraw.
+    ;; Otherwise change to 'buffer-invisibility-spec' doesn't have
+    ;; immediate effect here.
+    (force-mode-line-update)))
+
+
+(defun logview--apply-filters (&optional also-cancel-explicit-hiding)
+  (let* ((include-name-regexp   (logview--build-filter-regexp 
logview--include-name-regexps))
+         (exclude-name-regexp   (logview--build-filter-regexp 
logview--exclude-name-regexps))
+         (include-thread-regexp (logview--build-filter-regexp 
logview--include-thread-regexps))
+         (exclude-thread-regexp (logview--build-filter-regexp 
logview--exclude-thread-regexps))
+         (filters               (list include-name-regexp exclude-name-regexp 
include-thread-regexp exclude-thread-regexp)))
+    (when (or (not (equal logview--applied-filters filters)) 
also-cancel-explicit-hiding)
+      (logview--std-matching-and-altering
+        (save-restriction
+          (widen)
+          (goto-char (point-min))
+          (let ((reporter        (make-progress-reporter "Filtering..." 
(point-min) (point-max) (point)))
+                (hider           (logview--hide-entry-callback 
'logview-filtered))
+                (shower          (logview--show-entry-callback 
'logview-filtered))
+                (explicit-shower (and also-cancel-explicit-hiding 
(logview--show-entry-callback 'logview-hidden-entry)))
+                (num-hidden      0)
+                (num-visible     0)
+                (matches))
+            ;; Because 'callback' doesn't get access to match data,
+            ;; while in 'validator' doesn't know all entry limits, we
+            ;; use both and pass 'matches' from the validator to the
+            ;; callback.
+            (logview--iterate-entries-forward (lambda (begin after-first-line 
end)
+                                                (if matches
+                                                    (progn (funcall shower 
begin after-first-line end)
+                                                           (setq num-visible 
(1+ num-visible)))
+                                                  (funcall hider begin 
after-first-line end)
+                                                  (setq num-hidden (1+ 
num-hidden)))
+                                                ;; Yeah, it's two modification 
of properties on the
+                                                ;; same text chunk, but that's 
rarely used and so
+                                                ;; hardly important.
+                                                (when explicit-shower
+                                                  (funcall explicit-shower 
begin after-first-line end))
+                                                (progress-reporter-update 
reporter end)
+                                                ;; Always continue.
+                                                t)
+                                              nil
+                                              (lambda ()
+                                                (let ((name    (match-string 
logview--name-group))
+                                                      (thread  (match-string 
logview--thread-group)))
+                                                  (setq matches (and (or (null 
include-name-regexp)
+                                                                         
(string-match include-name-regexp name))
+                                                                     (or (null 
exclude-name-regexp)
+                                                                         (not 
(string-match exclude-name-regexp name)))
+                                                                     (or (null 
include-thread-regexp)
+                                                                         
(string-match include-thread-regexp thread))
+                                                                     (or (null 
exclude-thread-regexp)
+                                                                         (not 
(string-match exclude-thread-regexp thread))))))
+                                                ;; Operate on all entries.
+                                                t))
+            (cond ((= num-hidden 0)
+                   (message (if (equal filters '(nil nil nil nil)) "Filters 
are reset" "Filtering complete, nothing was hidden")))
+                  ((= num-visible 0)
+                   (message "Filtering complete, all entries were hidden"))
+                  (t
+                   (message "Filtering complete, %d %s out of %d (%.1f%%) %s 
hidden"
+                            num-hidden (if (= num-hidden 1) "entry" "entries") 
(+ num-hidden num-visible)
+                            (/ (* num-hidden 100.0) (+ num-hidden 
num-visible)) (if (= num-hidden 1) "was" "were"))))))))
+      (setq logview--applied-filters filters)))
+
+;; FIXME: Resulting regexp will not be valid if any of the options
+;;        uses group backreferences (\N) and maybe some other
+;;        constructs.
+(defun logview--build-filter-regexp (options)
+  (when options
+    (mapconcat 'identity options "\\|")))
+
+
+(defun logview--show-entry-callback (hider)
+  (lambda (begin after-first-line end)
+    (let ((value (get-text-property begin 'invisible)))
+      (when (memq hider value)
+        (put-text-property (logview--linefeed-back-checked begin) 
(logview--linefeed-back after-first-line)
+                           'invisible (remq hider value))
+        (when (> end after-first-line)
+          (put-text-property (logview--linefeed-back after-first-line) 
(logview--linefeed-back end)
+                             'invisible (remq hider (get-text-property 
after-first-line 'invisible))))))))
+
+(defun logview--hide-entry-callback (hider)
+  (lambda (begin after-first-line end)
+    (let ((value (get-text-property begin 'invisible)))
+      (unless (memq hider value)
+        (put-text-property (logview--linefeed-back-checked begin) 
(logview--linefeed-back after-first-line)
+                           'invisible (cons hider value))
+        (when (> end after-first-line)
+          (put-text-property (logview--linefeed-back after-first-line) 
(logview--linefeed-back end)
+                             'invisible (cons hider (get-text-property 
after-first-line 'invisible))))))))
+
+
+;; The following (inlined) functions are needed when applying
+;; 'invisible' property.  Generally we count entry from start of its
+;; line to the start of next entry's line.  This works nice e.g. for
+;; highlighting.  However, for hiding entries we need to take linefeed
+;; that _preceeds_ the entry, otherwise ellipses show at line
+;; beginnings, which is ugly and shifts actual buffer text.
+
+(defsubst logview--linefeed-back-checked (position)
+  "Assuming POSITION is at the beginning of a line, return
+position just before the preceding linefeed, if possible."
+  (if (> position 1)
+      (1- position)
+    1))
+
+(defsubst logview--linefeed-back (position)
+  "Assuming POSITION is at the beginning of a non-first line,
+return position just before the preceding linefeed."
+  (1- position))
+
+
+(defun logview--iterate-split-alists (callback &rest alists)
+  (let ((seen (make-hash-table :test 'equal)))
+    (dolist (alist alists)
+      (dolist (entry alist)
+        (unless (gethash (car entry) seen)
+          (funcall callback (car entry) (cdr entry))
+          (puthash (car entry) t seen)
+          (dolist (alias (cdr (assq 'aliases (cdr entry))))
+            (puthash alias t seen)))))))
+
+(defun logview--get-split-alists (key type &rest alists)
+  (catch 'found
+    (apply 'logview--iterate-split-alists (lambda (name value)
+                                            (when (or (equal name key) (member 
key (cdr (assq 'aliases value))))
+                                              (throw 'found value)))
+           alists)
+    (error "Unknown %s '%s'" type key)))
+
+
+
+;;; Internal commands meant as hooks.
+
+(defun logview--split-region-into-entries (begin end &optional old-length)
+  "Parse log entries in given region.  Optional third argument is
+to make the function suitable for `after-change-functions' and is
+ignored there.  Special value 'report-progress for this argument
+is treated differently."
+  (when logview--process-buffer-changes
+    (save-excursion
+      (save-match-data
+        (save-restriction
+          (with-silent-modifications
+            (widen)
+            (goto-char begin)
+            (forward-line 0)
+            (let ((inhibit-read-only t)
+                  (case-fold-search  nil)
+                  (anchored          t))
+              ;; Inlining `logview--match-successive-entries' for
+              ;; performance reasons.
+              (unless (or (looking-at logview--entry-regexp)
+                          (re-search-backward logview--entry-regexp nil t))
+                (let ((anchor (if (re-search-forward logview--entry-regexp nil 
t)
+                                  (match-beginning 0)
+                                (setq anchored nil)
+                                (point-max))))
+                  (put-text-property 1 anchor 'face      nil)
+                  (put-text-property 1 anchor 'invisible nil)))
+              (when anchored
+                (let* ((entry-begin    (match-beginning 0))
+                       (after-first-line)
+                       (level-data)
+                       (reporter       (when (eq old-length 'report-progress)
+                                         (make-progress-reporter "Parsing 
buffer..." entry-begin end entry-begin)))
+                       (have-timestamp (memq 'timestamp 
logview--submode-features))
+                       (have-level     (memq 'level     
logview--submode-features))
+                       (have-name      (memq 'name      
logview--submode-features))
+                       (have-thread    (memq 'thread    
logview--submode-features)))
+                  (while (progn
+                           (forward-line)
+                           (setq after-first-line (point))
+                           (when have-level
+                             (setq level-data (cdr (assoc (match-string 
logview--level-group) logview--submode-level-data)))
+                             (put-text-property entry-begin after-first-line 
'face (nth 1 level-data))
+                             (put-text-property 
(logview--linefeed-back-checked entry-begin) (logview--linefeed-back 
after-first-line)
+                                                'invisible (list (nth 0 
level-data)))
+                             (add-face-text-property (match-beginning 
logview--level-group)
+                                                     (match-end       
logview--level-group)
+                                                     (nth 2 level-data)))
+                           (when have-timestamp
+                             (add-face-text-property (match-beginning 
logview--timestamp-group)
+                                                     (match-end       
logview--timestamp-group)
+                                                     'logview-timestamp))
+                           (when have-name
+                             (add-face-text-property (match-beginning 
logview--name-group)
+                                                     (match-end       
logview--name-group)
+                                                     'logview-name))
+                           (when have-thread
+                             (add-face-text-property (match-beginning 
logview--thread-group)
+                                                     (match-end       
logview--thread-group)
+                                                     'logview-thread))
+                           (setq entry-begin (if (or (looking-at 
logview--entry-regexp)
+                                                     (re-search-forward 
logview--entry-regexp nil t))
+                                                 (match-beginning 0)
+                                               (point-max)))
+                           ;; Here 'entry-begin' is actually for the next 
entry.
+                           (when (< after-first-line entry-begin)
+                             (when have-level
+                               (put-text-property after-first-line entry-begin 
'face (nth 1 level-data)))
+                             (put-text-property (logview--linefeed-back 
after-first-line) (logview--linefeed-back entry-begin)
+                                                'invisible (list (nth 0 
level-data) 'logview-details)))
+                           (when reporter
+                             (progress-reporter-update reporter end))
+                           (< entry-begin end)))
+                  (when reporter
+                    (progress-reporter-done reporter)))))))))))
+
+(defun logview--buffer-substring-filter (begin end delete)
+  "Optionally remove invisible text from the substring."
+  (let ((substring (funcall (default-value 'filter-buffer-substring-function) 
begin end delete)))
+    (if logview-copy-visible-text-only
+        (let ((chunks)
+              (begin 0)
+              (end))
+          (while begin
+            (setq end (next-single-property-change begin 'invisible substring))
+            (when (not (invisible-p (get-text-property begin 'invisible 
substring)))
+              (push (substring substring begin end) chunks))
+            (setq begin end))
+          (apply 'concat (nreverse chunks)))
+      substring)))
+
+
+(provide 'logview)
+
+;;; logview.el ends here

Reply via email to