;;; logview.el --- Major mode for viewing log files  -*- lexical-binding: t -*-

;; Copyright (C) 2015-2026 Paul Pogonyshev

;; Author:     Paul Pogonyshev <pogonyshev@gmail.com>
;; Maintainer: Paul Pogonyshev <pogonyshev@gmail.com>
;; Version:    0.19.4snapshot
;; Keywords:   files, tools
;; Homepage:   https://github.com/doublep/logview
;; Package-Requires: ((emacs "25.1") (datetime "0.8") (extmap "1.0") (compat "29"))

;; 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:

;; Internally, we cannot use the point for most purposes, since correct interpretation of
;; `logview-entry' text property value heavily depends on knowing *exact* entry beginning.
;; When moving point, Emacs always adjusts it so it doesn't fall inside an invisible
;; range, which screws things up for us.  In the same vein, we always operate on
;; temporarily widened buffer, because it is not even possible to query text properties
;; outside of narrowed-to region.
;;
;; While the above sounds like potential problems only for the case someone hides half of
;; an entry or narrows from the middle of an entry, it really isn't.  I have experienced
;; bugs even in normal testing with entries fully hidden or shown only.
;;
;; In short, use point (e.g. `goto-char') only when delivering results of internal
;; computations to the user.


(eval-when-compile (require 'cl-lib)
                   (require 'help-mode))
(require 'datetime)
(require 'extmap)
;; For `string-trim' in earlier Emacs versions.
(require 'subr-x)

;; Needed for warning-free byte-compilation at least.
(eval-when-compile (require 'compat))


;; We _append_ self to the list of mode rules so as to not clobber
;; other rules, as '.log' is a common file extension.  This also gives
;; the user an easy way to prevent 'logview' from being autoselected.
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.log\\(?:\\.[0-9\\-]+\\)?\\'" . logview-mode) t)



;;; Public variables.
;; This needs to go before customization, since the values are used in
;; compound widget types.

(defvar logview-std-submodes
  '(("SLF4J"            . ((format  . "TIMESTAMP [THREAD] LEVEL NAME -")
                           (levels  . "SLF4J")
                           (aliases . ("Log4j" "Log4j2" "Logback"))))
    ;; We misuse thread as a field for hostname.
    ("UNIX"             . ((format  . "TIMESTAMP THREAD NAME:")))
    ("Apache Error Log" . ((format  . "[TIMESTAMP] [NAME:LEVEL] [THREAD] MESSAGE")
                           (levels  . "RFC 5424 lowercase")))
    ("Monolog"          . ((format  . "[TIMESTAMP] NAME[THREAD].LEVEL: MESSAGE")
                           (levels  . "RFC 5424")
                           (aliases . ("PHP" "PSR-3"))))
    ;; This doesn't seem to match what is specified at
    ;; https://jupyter-server.readthedocs.io/en/latest/other/full-config.html, but it does match what '$
    ;; jupyter lab' produces.  Therefore called "JupyterLab", not just "Jupyter".
    ("JupyterLab"       . ((format  . "[LEVEL TIMESTAMP NAME] MESSAGE")
                           (levels  . "JupyterLab"))))
  "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       "SEVERE")
                             (warning     "WARNING")
                             (information "INFO")
                             (debug       "CONFIG" "FINE")
                             (trace       "FINER" "FINEST")))
    ("RFC 5424"           . ((error       "EMERGENCY" "ALERT" "CRITICAL" "ERROR")
                             (warning     "WARNING")
                             (information "NOTICE" "INFO")
                             (debug       "DEBUG")
                             (trace)
                             (aliases     "syslog")))
    ("RFC 5424 lowercase" . ((error       "emergency" "alert" "critical" "error")
                             (warning     "warning")
                             (information "notice" "info")
                             (debug       "debug")
                             (aliases     "Apache error log")))
    ("JupyterLab"         . ((error       "C" "E")
                             (warning     "W")
                             (information "I")
                             (debug       "D"))))
  "Standard mappings of actual log levels to mode's final levels.
This alist value is used as the fallback for customizable
`logview-additional-level-mappings'.")

(defvar logview-std-timestamp-formats
  (let (formats)
    (dolist (data '(("ISO 8601 datetime + millis"             "yyyy-MM-dd HH:mm:ss.SSS")
                    ("ISO 8601 datetime + micros"             "yyyy-MM-dd HH:mm:ss.SSSSSS")
                    ("ISO 8601 datetime"                      "yyyy-MM-dd HH:mm:ss")
                    ("ISO 8601 datetime (with 'T') + millis"  "yyyy-MM-dd'T'HH:mm:ss.SSS")
                    ("ISO 8601 datetime (with 'T') + micros"  "yyyy-MM-dd'T'HH:mm:ss.SSSSSS")
                    ("ISO 8601 datetime (with 'T')"           "yyyy-MM-dd'T'HH:mm:ss")
                    ("ISO 8601 time only + millis"            "HH:mm:ss.SSS")
                    ("ISO 8601 time only + micros"            "HH:mm:ss.SSSSSS")
                    ("ISO 8601 time only"                     "HH:mm:ss")
                    (nil                                      "EEE MMM dd HH:mm:ss.SSSSSS yyyy")
                    (nil                                      "MMM d HH:mm:ss")
                    (nil                                      "MMM d h:mm:ss a")
                    (nil                                      "h:mm:ss a")))
      (push (list (or (car data) (cadr data)) (cons 'java-pattern (cadr data))) formats)
      (when (string-match-p "\\." (cadr data))
        (nconc (car formats) '((datetime-options :any-decimal-separator t))))
      (when (car data)
        (nconc (car formats) (list (list 'aliases (cadr data))))))
    (nreverse formats))
  "Alist of standard timestamp formats.
This value is used as the fallback for customizable
`logview-additional-timestamp-formats'.")

(defvar logview-font-lock-defaults '(nil nil t nil
                                         (font-lock-fontify-region-function . logview--fontify-region))
  "Value of `font-lock-defaults' in Logview buffers.
Derived modes shouldn't touch the value of this variable.
Instead, in their initialization functions they can modify
`font-lock-defaults' (already having a buffer-local value) as
needed.  They may also use `font-lock-add-keywords' and similar
functions.

However, remember that Logview uses heavily optimized font-lock
overrides.  Some functionality you'd expect a major mode to
support might not work.  You can file a bug in the issue tracker
if you have troubles writing a derived mode.")



;;; Customization.

(defgroup logview nil
  "Log viewing mode."
  :group 'text)


(defun logview--set-submode-affecting-variable (variable value)
  (set variable value)
  (when (fboundp #'logview--maybe-guess-submodes-again)
    (logview--maybe-guess-submodes-again)))

(defun logview--set-highlight-affecting-variable (variable value)
  (set variable value)
  (dolist (buffer (buffer-list))
    (with-current-buffer buffer
      (when (and (eq major-mode 'logview-mode) (with-no-warnings logview--highlighted-view-name))
        (logview--refontify-buffer)))))

(defvar logview--additional-submodes-type
  (let* ((italicize      (lambda (string) (propertize string 'face 'italic)))
         (mapping-option (lambda (mapping)
                           (let ((name    (car mapping))
                                 (aliases (cdr (assq 'aliases (cdr mapping)))))
                             (list 'const
                                   :tag (if aliases
                                            (format "%s (aka: %s)" (funcall italicize name) (mapconcat italicize aliases ", "))
                                          (funcall italicize name))
                                   name)))))
    (list 'repeat (list 'cons '(string :tag "Name")
                        (list 'list :tag "Definition"
                              '(cons :tag "" (const :tag "Format:" format) string)
                              (list 'set :inline t
                                    (list 'cons :tag "" '(const :tag "Level map:" levels)
                                          (append '(choice)
                                                  (mapcar mapping-option logview-std-level-mappings)
                                                  '((string :tag "Other name"))))
                                    (list 'cons :tag "" '(const :tag "Timestamp:" timestamp)
                                          (list 'choice
                                                '(const :tag "Any supported" nil)
                                                (list 'repeat
                                                      (append '(choice)
                                                              (mapcar mapping-option logview-std-timestamp-formats)
                                                              '((string :tag "Other name"))))))
                                    '(cons :tag "" (const :tag "Aliases:"   aliases)   (repeat string))))))))

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

    Additionally, you can use special placeholder \"IGNORED\" if
    needed.  Usecase for it are log files that contain too many
    fields to map to the ones Logview supports natively.

    Finally, you can explicitly specify \"MESSAGE\" field at the
    very end of the format string.  Normally, you can leave that
    to Logview, just as in the example above.  However, when the
    mode adds the field itself, it also prepends it with a space,
    which might be incorrect for some special custom submodes.

    Regular expressions to match specified entry pieces are
    generated by Logview based on surrounding characters (e.g.
    spaces in the example for Log4j except for \"THREAD\", which
    is surrounded by brackets).  However, if these are not
    suitable in your case, for \"NAME\", \"THREAD\" and \"IGNORED\"
    you can explicitly specify the desired regexp.  For example:

        <<RX:IGNORED:[a-z]+>>

    will ignore one or more Latin letters, but not anything else.

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 aliases \"Log4j\", \"Log4j2\",
    \"Logback\", \"JUL\" and the syslog standard \"RFC 5424\".
    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."
  :type  logview--additional-submodes-type
  :set   #'logview--set-submode-affecting-variable
  :set-after '(logview-additional-timestamp-formats logview-additional-level-mappings))

(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
        Debug levels:        DEBUG
        Trace levels:        TRACE

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
        Debug levels:        CONFIG, FINE
        Trace 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, or if some of the levels do not conceptually
map to the levels of the mode.  This is the case with RFC 5424:

        Error levels:        EMERGENCY, ALERT, CRITICAL, ERROR
        Warning levels:      WARNING
        Information levels:  NOTICE, INFO
        Debug levels:        DEBUG
        Trace levels:

Mapping can have any number of optional aliases, which work just
as the name."
  :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))))))
  :set   #'logview--set-submode-affecting-variable)

(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 by Java-like pattern.  If the
pattern contains text strings, e.g. month names, you can specify
the locale to use (defaults to English).

See `datetime' library for the help about patterns, or read

    https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html

A more complicated and mostly obsolete way to specify format is
by using 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.  Regular expression is ignored if Java pattern is
also specified.

Timestamp format can have any number of optional aliases, which
work just as the name."
  :type  '(repeat (cons (string :tag "Name")
                        (list :tag "Definition"
                              (set :inline t
                                   (cons :tag "" (const :tag "Java pattern:"       java-pattern)     string)
                                   (cons :tag "" (const :tag "Locale:"             locale)           symbol)
                                   (cons :tag "" (const :tag "Datetime options:"   datetime-options) plist)
                                   (cons :tag "" (const :tag "Regular expression:" regexp)           regexp)
                                   (cons :tag "" (const :tag "Aliases:" aliases) (repeat string))))))
  :set   #'logview--set-submode-affecting-variable)


(defcustom logview-guess-lines 500
  "When guessing submodes, consider this many lines at the top.
If any line corresponds to a defined submode, all the others are
not even looked at.  If several lines look like log entry starts,
but still cannot be matched against known submodes, the rest is
skipped, see variable `logview-max-promising-lines'.  However,
setting this to a ridiculously large number can still be slow."
  :type  'integer)

(defcustom logview-max-promising-lines 3
  "Abort submode guessing after this many \"promising\" lines.
If a line generally looks like a start of log entry to Logview,
it is considered \"promising\".  If several such lines still give
no matching submode, Logview aborts guessing.  This helps
avoiding very long unsuccessful guessing times even when
`logview-guess-lines' is large.

Setting this to zero makes the mode match against all
`logview-guess-lines'."
  :type  'integer)

(defcustom logview-auto-revert-mode nil
  "Automatically put recognized buffers into Auto-Revert mode.
Buffers for which no appropriate submode can be guessed are not
affected as well buffers not associated with files.  Having this
set to \"Off\" doesn't prevent Global Auto-Revert mode from
affecting Logview buffers.

Whenever new text is added to the buffer, it is automatically
parsed, highlighted and all currently active filters are applied
to it.

To temporarily activate or deactivate Auto-Revert (Tail) mode in
a Logview buffer type `\\<logview-mode-map>\\[auto-revert-mode]' or `\\<logview-mode-map>\\[auto-revert-tail-mode]'."
  :type  '(choice (const :tag "Off"                   nil)
                  (const :tag "Auto-Revert mode"      auto-revert-mode)
                  (const :tag "Auto-Revert Tail mode" auto-revert-tail-mode)))

(defcustom logview-reassurance-chars 5000
  "Compare this many characters before appending file tail.
This value is used by the command `logview-append-log-file-tail'
to compare part of the file on disk with part of the buffer to
make sure (even if not with 100% guarantee) that the buffer
really represents beginning of its backing file.  The command
will refuse to complete operation unless this check succeeds."
  :type  'integer)


(defcustom logview-target-gap-length 60
  "Default target gap length for `\\<logview-mode-map>\\[logview-next-timestamp-gap]' and similar commands.
This must be a non-negative number of seconds.  Can be changed
temporarily for a single buffer with `\\<logview-mode-map>\\[logview-change-target-gap-length]'."
  :type  'number)

(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.

To temporarily change this on per-buffer basis type `\\<logview-mode-map>\\[logview-toggle-copy-visible-text-only]'."
  :type  'boolean)

(defcustom logview-search-only-in-messages nil
  "Whether to incrementally search only in messages.
Normally search is not restricted and matches can be found
anywhere.  However, it is sometimes useful to ignore other parts
of log entries, e.g. timestamp when searching for numbers.

To temporarily change this on per-buffer basis type `\\<logview-mode-map>\\[logview-toggle-search-only-in-messages]'."
  :type  'boolean)

(defcustom logview-preview-filter-changes t
  "Whether to preview filters as they are being edited.
This preview is activated whenever you change the filters in the
buffer popped up by `\\<logview-mode-map>\\[logview-edit-filters]' or `\\<logview-mode-map>\\[logview-edit-thread-narrowing-filters]'.

To temporarily change this on per-buffer basis type `\\<logview-mode-map>\\[logview-toggle-filter-preview]'."
  :type  'boolean)

(defcustom logview-show-ellipses t
  "Whether to show ellipses to indicate hidden log entries.

To temporarily change this on per-buffer basis type `\\<logview-mode-map>\\[logview-toggle-show-ellipses]'."
  :type  'boolean)

(defcustom logview-highlighted-entry-part 'whole
  "Which parts of an entry get highlighted with `\\<logview-mode-map>\\[logview-highlight-view-entries]'."
  :type  '(choice (const :tag "The whole entry"                  whole)
                  (const :tag "Entry header (date, level, etc.)" header)
                  (const :tag "Entry message"                    message))
  :set   #'logview--set-highlight-affecting-variable)


(defcustom logview-pulse-entries '(section-movement navigation-view timestamp-gap)
  "When to briefly highlight the current entry.
You can also pulse the current entry unconditionally with `\\<logview-mode-map>\\[logview-pulse-current-entry]' command."
  :type  '(set :inline t
               (const :tag "After section movement commands" section-movement)
               (const :tag "After navigating a view with `\\<logview-mode-map>\\[logview-next-navigation-view-entry]' or `\\<logview-mode-map>\\[logview-previous-navigation-view-entry]'" navigation-view)
               (const :tag "After navigating within the current entry with `\\<logview-mode-map>\\[logview-go-to-message-beginning]'" message-beginning)
               (const :tag "After finding large gaps in entry timestamps (`\\<logview-mode-map>\\[logview-next-timestamp-gap]' and similar)" timestamp-gap)
               (const :tag "After other entry movement commands" movement)))

(defcustom logview-views-file (locate-user-emacs-file "logview.views")
  "Simple text file in which defined views are stored."
  :type  'file)

(defcustom logview-cache-filename (locate-user-emacs-file "logview-cache.extmap")
  "Internal non-human-readable cache.
Customizable in case you want to put it somewhere else.  This
file can be safely deleted, but will be recreated by Logview next
time you use the mode.  Used to make startup faster."
  :type  'file)

(defcustom logview-completing-read-function nil
  "Completion system used by Logview."
  :type  '(radio
           (const :tag "Auto" nil)
           (function-item completing-read)
           (function-item ido-completing-read)
           (function :tag "Custom function")))


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

(defface logview-level-warning
  '((t :inherit warning))
  "Face to use for warning level strings.")

(defface logview-warning-entry
  '((((background dark))
     :background "#606000")
    (t
     :background "#ffffe0"))
  "Face to use for warning log entries.")

(defface logview-level-information
  '((t :inherit success))
  "Face to use for information level strings.")

(defface logview-information-entry
  '((((background dark))
     :background "#004000")
    (t
     :background "#e8ffe8"))
  "Face to use for information log entries.")

(defface logview-level-debug
  nil
  "Face to use for debug level strings.")

(defface logview-debug-entry
  nil
  "Face to use for debug log entries.")

(defface logview-level-trace
  '((t :inherit shadow))
  "Face to use for trace level strings.")

(defface logview-trace-entry
  '((((background dark))
     :background "#404040")
    (t
     :background "#f0f0f0"))
  "Face to use for trace log entries.")

(defface logview-timestamp
  '((t :inherit font-lock-builtin-face))
  "Face to use for log entry timestamp.")

(defface logview-name
  '((t :inherit font-lock-string-face))
  "Face to use for logger name.")

(defface logview-thread
  '((t :inherit font-lock-variable-name-face))
  "Face to use for logger thread.")

(defface logview-section
  '((t :inverse-video t
       :weight        bold))
  "Face to use for a section's header.")

(defface logview-highlight
  '((((background dark))
     :background "#8030e0")
    (t
     :background "#f8d0ff"))
  "Face to highlight entries of a view chosen with `\\<logview-mode-map>\\[logview-highlight-view-entries]'.
Variable `logview-highlighted-entry-part' controls how exactly
this face is applied.")

(defface logview-pulse
  '((((background dark))
     :background "#606000")
    (t
     :background "#c0c0ff"))
  "Face to briefly highlight entries to draw attention.
Variable `logview-pulse-entries' controls in which situations
this face is used.")

(defface logview-unsearchable
  '((t :inherit shadow))
  "Face used to “dim” unsearchable text when searching incrementally.
Currently used only if `logview-search-only-in-messages' is
active.  That option can be activated in multiple ways, including
by typing `\\<logview-isearch-map>\\[logview-toggle-search-only-in-messages]' during the search.")

(defface logview-unprocessed
  '((t :inherit shadow))
  "Face to highlight otherwise unfontified and unfiltered entries.
Logview tries to make Emacs more responsive by periodically
making \"pauses\" in its fontification and filtering process.  In
large buffers with strict filters that exclude most entries, this
can mean that you can sometimes see not-yet-processed entries.
Those will be highlighted (or rather dimmed, with the default
settings) with this face.")

(defface logview-edit-filters-type-prefix
  '((((background dark))
     :background "#604000"
     :weight     bold)
    (t
     :background "#ffe0c0"
     :weight     bold))
  "Face to use for type prefixes in filter editing buffer.")



;;; Internal variables and constants.

;; Keep in sync with `logview--entry-*' and `logview--find-region-entries'.
(defconst logview--timestamp-group 1)
(defconst logview--level-group     2)
(defconst logview--name-group      3)
(defconst logview--thread-group    4)
(defconst logview--ignored-group   5)
(defconst logview--message-group   6)

(defconst logview--final-levels '(error warning information debug trace)
  "List of final (submode-independent) levels, most to least severe.")

(defconst logview--entry-part-regexp (rx (or (seq bow (or (group "TIMESTAMP") (group "LEVEL") (group "NAME")
                                                          (group "THREAD") (group "IGNORED") (group "MESSAGE"))   ;; 1--6, see above
                                                  eow)
                                             (seq "<<RX:" (or (group "NAME") (group "THREAD") (group "IGNORED"))  ;; 7--9
                                                  ":" (group (+? anything)) ">>"))))                               ;; 10
(defconst logview--timestamp-entry-part-regexp (rx bow "TIMESTAMP" eow))

(defvar logview--datetime-matching-options '(:second-fractional-extension t
                                             :only-4-digit-years t
                                             :accept-leading-space t
                                             :require-leading-zeros t
                                             :forbid-unnecessary-zeros t))

(defvar logview--datetime-parsing-options  '(:second-fractional-extension t
                                             :case-insensitive t
                                             :lax-whitespace t))

(defvar logview--all-timestamp-formats-cache nil)

(defconst logview--valid-filter-prefixes '("lv" "LV" "a+" "a-" "t+" "t-" "m+" "m-"))

(defvar logview--custom-submode-revision 0)
(defvar logview--custom-submode-state nil)
(defvar logview--submode-guessing-timer nil)
(defvar logview--need-submode-guessing nil)
(defvar-local logview--custom-submode-guessed-with 0)


;; Don't access these as variables directly, use functions with the same name instead.
(defvar-local logview--point-min nil)
(defvar-local logview--point-max nil)

(defvar-local logview--submode-name nil)
(defvar-local logview--entry-regexp nil)
(defvar-local logview--submode-features nil)

(defvar-local logview--submode-level-data nil
  "An alist of level strings to (INDEX . (ENTRY-FACE . STRING-FACE)).")

(defvar-local logview--submode-level-faces nil
  "A vector of (ENTRY-FACE . STRING-FACE).")

(defvar-local logview--submode-timestamp-parser           nil)
(defvar-local logview--timestamp-difference-format-string nil)
(defvar-local logview--timestamp-gap-format-string        nil)

(defvar-local logview--as-important-level nil)
(defvar-local logview--hide-all-details   nil)

(defvar-local logview--timestamp-difference-base nil
  "Either nil or (ENTRY . START).
ENTRY will have its timestamp parsed.")
(defvar-local logview--timestamp-difference-per-thread-bases nil
  "Either nil or a hash-table of strings to cons cells.
See `logview--timestamp-difference-base' for details.")
(defvar-local logview--timestamp-difference-to-section-headers nil)

(defvar-local logview--buffer-target-gap-length nil)
(defvar-local logview--last-found-large-gap     nil)

(defvar-local logview--main-filter-text             "")
(defvar-local logview--thread-narrowing-filter-text "")
(defvar-local logview--narrow-to-section-headers    nil)

(defvar-local logview--preview-filter-text nil
  "Filters for which to show a preview.
Take precedence over real filters.  When set, must be a cons cell
of (IS-MAIN . FILTER-TEXT).")

(defvar-local logview--effective-filter nil
  "See `logview--do-parse-filters' for the format.")

;; I also considered using private cons cells where we could reset `car' e.g. from
;; `logview-filtered' to nil.  However, this is very shaky and will stop working if any
;; minor mode tweaks `invisible' property the "wrong" way.  Another possibility would be
;; to use uninterned symbols, but that would be very confusing, since in the output they
;; would look the same.  Therefore, I decided to include "generation" in the symbol
;; instead.
(defvar-local logview--filtered-symbol       'logview-filtered/0)
(defvar-local logview--hidden-entry-symbol   'logview-hidden-entry/0)
(defvar-local logview--hidden-details-symbol 'logview-hidden-details/0)

(defvar logview--submode-name-history     nil)
(defvar logview--timestamp-format-history nil)
(defvar logview--name-regexp-history      nil)
(defvar logview--thread-name-history      nil)
(defvar logview--thread-regexp-history    nil)
(defvar logview--message-regexp-history   nil)

(defvar-local logview--process-buffer-changes nil)

(defvar logview--views             nil)
(defvar logview--views-initialized nil)
(defvar logview--views-need-saving nil)

(defvar logview--view-name-history)

(defvar-local logview--section-view-name     nil)
(defvar-local logview--section-header-filter nil)
(defvar-local logview--sections-thread-bound t)

(defvar-local logview--navigation-view-name  nil)
(defvar-local logview--highlighted-view-name nil)
(defvar-local logview--highlighted-filter    nil)

(defvar-local logview--filter-editing-buffer                  nil)
(defvar-local logview--thread-narrowing-filter-editing-buffer nil)
(defvar       logview--view-editing-buffer                    nil)

;; Not too small to avoid calling `logview--fontify-region' and
;; `logview--find-region-entries' often: calling and setup involves some overhead.
(defvar logview--lazy-region-size 50000)
(defvar logview--max-fontified-in-row 10)

(defvar       logview--pending-refontifications nil)
(defvar-local logview--postpone-fontification   nil)
(defvar-local logview--num-fontified-in-row     0)


(defvar-local logview-filter-edit--mode                      nil)
(defvar-local logview-filter-edit--editing-views-for-submode nil)
(defvar-local logview-filter-edit--parent-buffer             nil)
(defvar-local logview-filter-edit--window-configuration      nil)
(defvar-local logview-filter-edit--preview-timer             nil)

(defvar logview-filter-edit--filters-hint-comment
  "# Press C-c C-c to save edited filters, C-c C-k to quit without saving.
# Use C-c C-a to apply the changes without quitting the buffer.
")

(defvar logview-filter-edit--thread-narrowing-filters-hint-comment
  "# Press C-c C-c to save edited filters, C-c C-k to quit without saving.
# Use C-c C-a to apply the changes without quitting the buffer.
# Only `t+' and `t-' filters are valid for thread narrowing.
")

(defvar logview-filter-edit--views-hint-comment
  "# Press C-c C-c to save edited views, C-c C-k to quit without saving.
# Use C-c C-a to apply the changes without quitting the buffer.
")

(defconst logview--view-header-regexp  (rx bol (group "view")    (1+ " ") (group (1+ nonl))          eol))
(defconst logview--view-submode-regexp (rx bol (group "submode") (1+ " ") (group (1+ nonl))          eol))
(defconst logview--view-index-regexp   (rx bol (group "index")   (1+ " ") (group (? "-") (1+ digit)) eol))


(defvar logview--cheat-sheet
  '(("Movement"
     (logview-go-to-message-beginning                                        "Beginning of entry’s message")
     (logview-next-entry                 logview-previous-entry              "Next / previous entry")
     (logview-next-as-important-entry    logview-previous-as-important-entry "Next / previous ‘as important’ entry")
     (logview-next-navigation-view-entry logview-previous-navigation-view-entry "Next / previous entry in the navigation view (see below)")
     (logview-next-timestamp-gap         logview-previous-timestamp-gap      "Next / previous large gap in entry timestamps")
     (logview-next-timestamp-gap-in-this-thread logview-next-timestamp-gap-in-this-thread "Same, but only within this thread")
     (logview-first-entry                logview-last-entry                  "First / last entry")
     "‘As important’ means entries with the same or higher level.  See also
commands in ‘Sections’ below")
    ("Narrowing and widening"
     (logview-narrow-from-this-entry     logview-narrow-up-to-this-entry     "Narrow from / up to this entry")
     (logview-widen                                                          "Widen")
     (logview-widen-upwards              logview-widen-downwards             "Widen upwards / downwards")
     "See also commands in ‘Sections’ below")
    ("Filtering by level"
     (logview-show-only-errors                                               "Show only errors")
     (logview-show-errors-and-warnings                                       "Show errors and warnings")
     (logview-show-errors-warnings-and-information                           "Show errors, warnings and information")
     (logview-show-errors-warnings-information-and-debug                     "Show all levels except trace")
     (logview-show-all-levels                                                "Show entries of all levels")
     (logview-show-only-as-important                                         "Show entries ‘as important’ as the current one"))
    ("Always show entries of certain levels"
     (logview-disable-unconditional-show                                     "Disable ‘always show’")
     (logview-always-show-errors                                             "Always (i.e. regardless of text filters) show errors")
     (logview-always-show-errors-and-warnings                                "Always show errors and warnings")
     (logview-always-show-errors-warnings-and-information                    "Always show errors, warnings and information")
     (logview-always-show-errors-warnings-information-and-debug              "Always show all levels except trace"))
    ("Text-based filtering"
     (logview-edit-filters                                                   "Edit filters as text in a separate buffer")
     (logview-add-include-name-filter    logview-add-exclude-name-filter     "Add name include / exclude filter")
     (logview-add-include-thread-filter  logview-add-exclude-thread-filter   "Add thread include / exclude filter")
     (logview-add-include-message-filter logview-add-exclude-message-filter  "Add message include / exclude filter"))
    ("Resetting filters"
     (logview-reset-level-filters                                            "Reset level filters")
     (logview-reset-name-filters                                             "Reset name filters")
     (logview-reset-thread-filters                                           "Reset thread filters")
     (logview-reset-message-filters                                          "Reset message filters")
     (logview-reset-all-filters                                              "Reset all filters")
     (logview-reset-all-filters-restrictions-and-hidings                     "Reset filters, widen and show explicitly hidden entries"))
    ("Views"
     (logview-switch-to-view                                                 "Switch to a view")
     (logview-set-section-view                                               "Choose a view for determining sections")
     (logview-set-navigation-view                                            "Choose a view for navigation")
     (logview-highlight-view-entries                                         "Select a view to highlight its entries")
     (logview-unhighlight-view-entries                                       "Remove view highlighting")
     (logview-save-filters-as-view-for-submode                               "Save the filters as a view for the current submode")
     (logview-save-filters-as-global-view                                    "Save the filters as a globally available view")
     (logview-edit-submode-views                                             "Edit views for the current submode")
     (logview-edit-all-views                                                 "Edit all views")
     (logview-assign-quick-access-index                                      "Assign a quick access index to the current view")
     (logview-delete-view                                                    "Delete a view")
     "You can also switch to a view by its quick access index: \\[logview-switch-to-view-by-index <0>]..\\[logview-switch-to-view-by-index <9>].
For larger indices use prefix argument, e.g.: \\[digit-argument <1>] \\[digit-argument <4>] \\[logview-switch-to-view].  This also
works for \\[logview-set-navigation-view] and \\[logview-highlight-view-entries] commands.")
    ("Sections"
     (logview-go-to-section-beginning                                        "Beginning of the current section")
     (logview-go-to-section-end                                              "End of the current section")
     (logview-next-section               logview-previous-section            "Next / previous section")
     (logview-next-section-any-thread    logview-previous-section-any-thread "Next / previous section in any thread")
     (logview-first-section              logview-last-section                "First / last section")
     (logview-first-section-any-thread   logview-last-section-any-thread     "First / last section in any thread")
     (logview-narrow-to-section                                              "Narrow to the current section and filter out other threads")
     (logview-narrow-to-section-keep-threads                                 "Narrow to the current section, but don’t touch thread filters")
     (logview-toggle-narrow-to-section-headers                               "Toggle “narrowing” to section headers, i.e. showing only headers")
     (logview-toggle-sections-thread-bound                                   "Toggle whether sections are thread-bound")
     "See also \\[logview-set-section-view].")
    ("Explicitly hide or show entries"
     (logview-hide-entry                                                     "Hide entry")
     (logview-hide-region-entries                                            "Hide entries in the region")
     (logview-show-entries                                                   "Show some explicitly hidden entries")
     (logview-show-region-entries                                            "Show explicitly hidden entries in the region")
     (logview-reset-manual-entry-hiding                                      "Show all explicitly hidden entries in the buffer"))
    ("Hide or show details of individual entries"
     (logview-toggle-entry-details                                           "Toggle details of the current entry")
     (logview-toggle-region-entry-details                                    "Toggle details of entries in the region")
     (logview-toggle-details-globally                                        "Toggle details in the whole buffer")
     (logview-reset-manual-details-hiding                                    "Show all hidden entry details in the buffer")
     "Here ‘details’ are the message lines after the first.")
    ("Display timestamp differences"
     (logview-difference-to-current-entry                                    "Show difference to the current entry for all other entries")
     (logview-thread-difference-to-current-entry                             "Show difference only for the entries of the same thread")
     (logview-difference-to-section-headers                                  "Show timestamp differences within each section")
     (logview-go-to-difference-base-entry                                    "Go to the entry difference to which timestamp is shown")
     (logview-forget-difference-base-entries                                 "Don’t show timestamp differences")
     (logview-forget-thread-difference-base-entry                            "Don’t show timestamp differences for this thread")
     (logview-cancel-difference-to-section-headers                           "Don’t show timestamp differences to section headers"))
    ("Change options for current buffer"
     (logview-change-target-gap-length                                       "Set gap length for ‘\\[logview-next-timestamp-gap]’ and similar commands")
     (auto-revert-mode                                                       "Toggle Auto-Revert mode")
     (auto-revert-tail-mode                                                  "Toggle Auto-Revert Tail mode")
     (logview-toggle-copy-visible-text-only                                  "Toggle ‘copy only visible text’")
     (logview-toggle-search-only-in-messages                                 "Toggle ‘search only in messages’")
     (logview-toggle-filter-preview                                          "Toggle ‘preview filter changes’")
     (logview-toggle-show-ellipses                                           "Toggle ‘show ellipses’")
     "Options can be customized globally or changed in each buffer.")
    ("Miscellaneous"
     (logview-pulse-current-entry                                            "Briefly highlight the current entry")
     (logview-choose-submode                                                 "Manually choose appropriate submode")
     (logview-customize-submode-options                                      "Customize options that affect submode selection")
     (bury-buffer                                                            "Bury buffer")
     (logview-refresh-buffer-as-needed                                       "Append tail or revert the buffer, as needed")
     (logview-prepare-for-new-contents                                       "Prepare the buffer to inspect only newly added contents")
     (logview-append-log-file-tail                                           "Append log file tail to the buffer")
     (logview-revert-buffer                                                  "Revert the buffer preserving active filters")
     "Universal prefix commands are bound without modifiers: \\[universal-argument], \\[negative-argument], \\[digit-argument <0>]..\\[digit-argument <9>].")))



;;; Macros and inlined functions.

;; Lisp is sensitive to declaration order, so these are collected at
;; the beginning of the file.

(defmacro logview--std-altering (&rest body)
  (declare (indent 0) (debug t))
  `(save-excursion
     (let ((logview--process-buffer-changes nil)
           (inhibit-read-only               t))
       (with-silent-modifications
         ,@body))))

(defmacro logview--temporarily-widening (&rest body)
  "Execute BODY with the current buffer fully widened.
Original point restrictions, if any, will not be possible to find
inside BODY.  In most cases (also if not sure) you should use
macro `logview--std-temporarily-widening' instead."
  (declare (indent 0) (debug t))
  `(logview--do-temporarily-widening (lambda () ,@body)))

(defun logview--do-temporarily-widening (body)
  (save-restriction
    ;; {LOCKED-NARROWING}
    ;; "Hurr-durr, mah security, you cannot unlock without knowing the tag."  Try all
    ;; tags I could find in Emacs source code.  Normally this should be enough, but
    ;; there is obviously no guarantee as macro `with-restriction' is part of public
    ;; Elisp interface now.
    (without-restriction
      :label 'long-line-optimizations-in-fontification-functions
      (without-restriction
        :label 'long-line-optimizations-in-command-hooks
        (logview--do-widen)
        (funcall body)))))

(defun logview--do-widen ()
  (widen)
  ;; If still not widened, then it is better to fail hard now than to face an arbitrary
  ;; and hard to predict failure later.  In particular, an infinite loop in fontification
  ;; code can irreversibly freeze Emacs, but this is of course "not a bug":
  ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=57804.  They care about responsiveness
  ;; with long lines, but not here, right.
  (unless (= (- (point-max) (point-min)) (buffer-size))
    (error "Logview is incompatible with locked narrowing; see https://github.com/doublep/logview#locked-narrowing")))

(defmacro logview--std-temporarily-widening (&rest body)
  "Execute BODY with the current buffer fully widened.
Original point restrictions are available as return values of
functions `logview--point-min' and `logview--point-max'.  This
macro can be nested, with inner calls not changing results of the
two functions (available since the first call) further."
  (declare (indent 0) (debug t))
  `(let ((logview--point-min (logview--point-min))
         (logview--point-max (logview--point-max)))
     (logview--temporarily-widening ,@body)))

(defmacro logview--locate-current-entry (entry start &rest body)
  (declare (indent 2) (debug (symbolp symbolp body)))
  (cond ((and entry start)
         (let ((entry+start (make-symbol "$entry+start")))
           `(let* ((,entry+start (logview--do-locate-current-entry))
                   (,entry       (car ,entry+start))
                   (,start       (cdr ,entry+start)))
              ,@body)))
        (entry
         `(let ((,entry (car (logview--do-locate-current-entry))))
            ,@body))
        (start
         `(let ((,start (cdr (logview--do-locate-current-entry))))
            ,@body))
        (t
         `(progn (logview--do-locate-current-entry)
                 ,@body))))


(defsubst logview--point-min ()
  (or logview--point-min (point-min)))

(defsubst logview--point-max ()
  (or logview--point-max (point-max)))


;; Value of text property `logview-entry' is a vector with the following elements:
;; - 0:   entry end offset, i.e. total entry length;
;; - 1:   message start offset, i.e. entry header length;
;; - 2-9: group 1-4 (timestamp, level, name, thread) begin/end offsets;
;; - 10:  details (second line) start offset, or nil if there is no second line;
;; - 11:  entry level as a number;
;; - 12:  entry timestamp as a float (or nil, if not parsed yet).
;;
;; Offsets are relative to the entry beginning.  We store offsets so that values remain
;; valid even if buffer text is shifted forwards or backwards.
;;
;; For all the following functions, ENTRY must be a value of `logview-entry' property.
(defsubst logview--entry-end (entry start)
  (+ start (aref entry 0)))

(defsubst logview--entry-message-start (entry start)
  (+ start (aref entry 1)))

(defsubst logview--entry-message (entry start)
  (buffer-substring-no-properties (logview--entry-message-start entry start) (logview--entry-end entry start)))

(defsubst logview--entry-group-start (entry start group)
  (+ start (aref entry (* group 2))))

(defsubst logview--entry-group-end (entry start group)
  (+ start (aref entry (1+ (* group 2)))))

(defsubst logview--entry-group (entry start group)
  (let ((base (* group 2)))
    (buffer-substring-no-properties (+ start (aref entry base)) (+ start (aref entry (1+ base))))))

(defsubst logview--entry-details-start (entry start)
  (let ((details-offset (aref entry 10)))
    (when details-offset
      (+ start details-offset))))

(defsubst logview--entry-level (entry)
  (aref entry 11))

;; Parse entry timestamp.  This value is cached.
(defsubst logview--entry-timestamp (entry start)
  (or (aref entry 12)
      (aset entry 12 (funcall logview--submode-timestamp-parser (logview--entry-group entry start logview--timestamp-group)))))


;; 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--character-back-checked (position)
  "Return end of previous line.
This function assumes POSITION is at the beginning of a line.  If
this is the first line, don't change POSITION."
  (if (> position (point-min))
      (1- position)
    (point-min)))

(defsubst logview--character-back (position)
  "Return end of previous line assumin non-first line.
This function assumes POSITION is at the beginning of a line and
that the line is not the first in the buffer."
  (1- position))

(defsubst logview--space-back (position)
  (if (eq (get-char-code-property (char-before position) 'general-category) 'Zs)
      (1- position)
    position))



;;; The mode.

(defvar logview-mode-map
  (let ((map (make-sparse-keymap)))
    (suppress-keymap map)
    (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)
                       ("M-n" logview-next-navigation-view-entry)
                       ("M-p" logview-previous-navigation-view-entry)
                       ("<"   logview-first-entry)
                       (">"   logview-last-entry)
                       ;; Narrowing/widening commands.
                       ("["   logview-narrow-from-this-entry)
                       ("]"   logview-narrow-up-to-this-entry)
                       ("w"   logview-widen)
                       ("{"   logview-widen-upwards)
                       ("}"   logview-widen-downwards)
                       ("y"   logview-narrow-to-thread)
                       ("Y"   logview-edit-thread-narrowing-filters)
                       ;; 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)
                       ("L 1" logview-always-show-errors)
                       ("L e" logview-always-show-errors)
                       ("L 2" logview-always-show-errors-and-warnings)
                       ("L w" logview-always-show-errors-and-warnings)
                       ("L 3" logview-always-show-errors-warnings-and-information)
                       ("L i" logview-always-show-errors-warnings-and-information)
                       ("L 4" logview-always-show-errors-warnings-information-and-debug)
                       ("L d" logview-always-show-errors-warnings-information-and-debug)
                       ("L 0" logview-disable-unconditional-show)
                       ("L L" logview-disable-unconditional-show)
                       ;; Filtering by name/thread/message commands.
                       ("f"   logview-edit-filters)
                       ("a"   logview-add-include-name-filter)
                       ("A"   logview-add-exclude-name-filter)
                       ("t"   logview-add-include-thread-filter)
                       ("T"   logview-add-exclude-thread-filter)
                       ("m"   logview-add-include-message-filter)
                       ("M"   logview-add-exclude-message-filter)
                       ;; Filter resetting commands.
                       ("r l" logview-reset-level-filters)
                       ("r a" logview-reset-name-filters)
                       ("r t" logview-reset-thread-filters)
                       ("r m" logview-reset-message-filters)
                       ("R"   logview-reset-all-filters)
                       ("r e" logview-reset-all-filters-restrictions-and-hidings)
                       ;; View commands.
                       ("v"   logview-switch-to-view)
                       ("V c" logview-set-section-view)
                       ("V n" logview-set-navigation-view)
                       ("V h" logview-highlight-view-entries)
                       ("V u" logview-unhighlight-view-entries)
                       ("V s" logview-save-filters-as-view-for-submode)
                       ("V S" logview-save-filters-as-global-view)
                       ("V e" logview-edit-submode-views)
                       ("V E" logview-edit-all-views)
                       ("V i" logview-assign-quick-access-index)
                       ("V d" logview-delete-view)
                       ;; Section commands.
                       ("c a" logview-go-to-section-beginning)
                       ("c e" logview-go-to-section-end)
                       ("c n" logview-next-section)
                       ("c p" logview-previous-section)
                       ("c N" logview-next-section-any-thread)
                       ("c P" logview-previous-section-any-thread)
                       ("c ," logview-first-section)
                       ("c ." logview-last-section)
                       ("c <" logview-first-section-any-thread)
                       ("c >" logview-last-section-any-thread)
                       ("c c" logview-narrow-to-section)
                       ("c C" logview-narrow-to-section-keep-threads)
                       ("c h" logview-toggle-narrow-to-section-headers)
                       ("c t" logview-toggle-sections-thread-bound)
                       ;; Explicit entry hiding/showing commands.
                       ("h"   logview-hide-entry)
                       ("H"   logview-hide-region-entries)
                       ("s"   logview-show-entries)
                       ("S"   logview-show-region-entries)
                       ("r h" logview-reset-manual-entry-hiding)
                       ;; Showing/hiding entry details commands.
                       ("d"   logview-toggle-entry-details)
                       ("D"   logview-toggle-region-entry-details)
                       ("e"   logview-toggle-details-globally)
                       ("r d" logview-reset-manual-details-hiding)
                       ;; Entry timestamp commands.
                       ("z a" logview-difference-to-current-entry)
                       ("z t" logview-thread-difference-to-current-entry)
                       ("z c" logview-difference-to-section-headers)
                       ("z z" logview-go-to-difference-base-entry)
                       ("z A" logview-forget-difference-base-entries)
                       ("z T" logview-forget-thread-difference-base-entry)
                       ("z C" logview-cancel-difference-to-section-headers)
                       ("z n" logview-next-timestamp-gap)
                       ("z p" logview-previous-timestamp-gap)
                       ("z N" logview-next-timestamp-gap-in-this-thread)
                       ("z P" logview-previous-timestamp-gap-in-this-thread)
                       ("z g" logview-change-target-gap-length)
                       ;; Option changing commands.
                       ("o r" auto-revert-mode)
                       ("o t" auto-revert-tail-mode)
                       ("o v" logview-toggle-copy-visible-text-only)
                       ("o m" logview-toggle-search-only-in-messages)
                       ("o p" logview-toggle-filter-preview)
                       ("o e" logview-toggle-show-ellipses)
                       ("o g" logview-change-target-gap-length)
                       ("o s" logview-choose-submode)
                       ("o S" logview-customize-submode-options)
                       ;; For compatibility with the inactive keymap.
                       ("C-c C-c" logview-choose-submode)
                       ("C-c C-s" logview-customize-submode-options)
                       ;; Miscellaneous commands.
                       ("SPC" logview-pulse-current-entry)
                       ("?"   logview-mode-help)
                       ("g"   logview-refresh-buffer-as-needed)
                       ("G"   logview-prepare-for-new-contents)
                       ("x"   logview-append-log-file-tail)
                       ("X"   logview-revert-buffer)
                       ("q"   bury-buffer)
                       ;; Simplified universal argument command
                       ;; rebindings.  Digits and minus are set up by
                       ;; 'suppress-keymap' already.
                       ("u"   universal-argument)))
      (define-key map (kbd (car binding)) (cadr binding)))
    (dotimes (k 10)
      (define-key map (kbd (format "M-%d" k)) #'logview-switch-to-view-by-index))
    map))

(defvar logview-mode-inactive-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-c") #'logview-choose-submode)
    (define-key map (kbd "C-c C-s") #'logview-customize-submode-options)
    map)
  "Keymap used by `logview-mode' when the mode is inactive.
Mode is inactive when the buffer is not read-only (to not
interfere with editing) or if submode wasn't guessed
successfully.")


;;;###autoload
(define-derived-mode logview-mode nil "Logview"
  "Major mode for viewing and filtering various log files."
  (let (successful)
    (unwind-protect
        (progn (logview--set-up)
               (setf successful t))
      (unless successful
        ;; Don't leave the mode half-initialized if there is any unhandled error.  This is
        ;; not how Emacs normally works, but standard way is too confusing to users and
        ;; behavior depends on where exactly an error happened.  Also, as function
        ;; `normal-mode' effectively eats the error (at least its backtrace), debugging it
        ;; is nearly impossible anyway.
        (fundamental-mode)))))

(defun logview--set-up ()
  ;; {LOCKED-NARROWING}
  ;; Previously would set `long-line-threshold' to nil on those 29 snapshots that already
  ;; had this locking shit, but no way to unlock at all.  Because of `compat' usage we can
  ;; no longer detect that, even unreliably, so on 29 snapshots things can get ugly.
  ;; Ignore: we simply don't support old Emacs snapshots.
  (logview--update-keymap)
  (add-hook 'read-only-mode-hook #'logview--update-keymap nil t)
  (setq font-lock-defaults (copy-sequence logview-font-lock-defaults))
  (set (make-local-variable 'filter-buffer-substring-function)  #'logview--buffer-substring-filter)
  (set (make-local-variable 'isearch-filter-predicate)          #'logview--isearch-filter-predicate)
  (add-hook 'after-change-functions #'logview--invalidate-region-entries nil t)
  (add-hook 'change-major-mode-hook #'logview--exiting-mode nil t)
  (add-hook 'pre-command-hook #'logview--pre-command nil t)
  (add-hook 'isearch-mode-hook #'logview--starting-isearch nil t)
  (add-hook 'isearch-mode-end-hook #'logview--ending-isearch nil t)
  (logview--guess-submode)
  (logview--update-invisibility-spec)
  (unless (logview-initialized-p)
    (message "Cannot determine log format; press C-c C-c to choose manually or C-c C-s to customize relevant options")))

(defun logview--update-keymap ()
  (use-local-map (if (and buffer-read-only (logview-initialized-p))
                     logview-mode-map
                   logview-mode-inactive-map)))

(defun logview--exiting-mode ()
  (logview--std-temporarily-widening
    (logview--std-altering
      (remove-list-of-text-properties (point-min) (point-max) '(invisible display logview-entry)))))

(defun logview-initialized-p ()
  (not (null logview--entry-regexp)))



;;; 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.

If current entry's timestamp is replaced with its difference to
some base (see e.g.  `logview-difference-to-current-entry'), this
command additionally shows the real timestamp in the echo area."
  (interactive "P")
  (logview--assert)
  (logview--std-temporarily-widening
    (logview--locate-current-entry entry start
      (goto-char (logview--entry-message-start entry start))
      (when select-message
        (push-mark (logview--character-back (logview--entry-end entry start)) t t))
      (when (and (memq 'timestamp logview--submode-features)
                 (get-text-property (logview--entry-group-start entry start logview--timestamp-group) 'display))
        (message "Timestamp here: %s" (logview--entry-group entry start logview--timestamp-group))))
    (unless (and select-message transient-mark-mode)
      (logview--maybe-pulse-current-entry 'message-beginning))))

(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)
  (unless n
    (setq n 1))
  (logview--std-temporarily-widening
    (let ((remaining (logview--forward-entry n nil)))
      (logview--maybe-pulse-current-entry 'movement)
      (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.
Point is positioned at the beginning of the message of the
resulting entry.

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."
  (interactive "p")
  (logview--assert 'level)
  (unless n
    (setq n 1))
  (logview--std-temporarily-widening
    (logview--locate-current-entry entry nil
      (unless (memq last-command '(logview-next-as-important-entry logview-previous-as-important-entry))
        (setq logview--as-important-level (logview--entry-level entry))))
    (let* ((level     (or logview--as-important-level most-positive-fixnum))
           (remaining (logview--forward-entry n (lambda (entry _start) (<= (logview--entry-level entry) level)))))
      (logview--maybe-pulse-current-entry 'movement)
      (logview--maybe-complain-about-movement n remaining 'as-important))))

(defun logview-previous-as-important-entry (&optional n)
  "Move point vertically up N “as important” entries.
Point is positioned at the beginning of the message of the
resulting entry.

See function `logview-next-as-important-entry' for definition of
“as important”."
  (interactive "p")
  (logview-next-as-important-entry (if n (- n) -1)))

(defun logview-next-navigation-view-entry (&optional n set-view-if-needed)
  "Move point vertically down N entries in the navigation view.
Entries that don't conform to the navigation view filters, as
well as hidden due to any reasons, are skipped over.

When called interactively and there is no navigation view yet (or
it got deleted or renamed), ask for the view first.  You can use
command `\\<logview-mode-map>\\[logview-set-navigation-view]' to change that later."
  ;; Second "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p\np")
  (logview--assert)
  (when set-view-if-needed
    (unless (logview--find-view logview--navigation-view-name t)
      (setq logview--navigation-view-name
            (logview--choose-view (substitute-command-keys
                                   "Navigate through view (change with `\\[logview-set-navigation-view]'): ")))))
  (unless n
    (setq n 1))
  (logview--std-temporarily-widening
    (let* ((filters   (plist-get (logview--find-view logview--navigation-view-name) :filters))
           (remaining (logview--forward-entry n (cdr (logview--do-parse-filters filters)))))
      (logview--maybe-pulse-current-entry 'navigation-view)
      (logview--maybe-complain-about-movement n remaining logview--navigation-view-name))))

(defun logview-previous-navigation-view-entry (&optional n set-view-if-needed)
  "Move point vertically up N entries in the navigation view.
Entries that don't conform to the navigation view filters, as
well as hidden due to any reasons, are skipped over.

When called interactively and there is no navigation view yet (or
it got deleted or renamed), ask for the view first.  You can use
command `\\<logview-mode-map>\\[logview-set-navigation-view]' to change that later."
  (interactive "p\np")
  (logview-next-navigation-view-entry (if n (- n) -1) set-view-if-needed))

(defun logview-first-entry (&optional no-mark)
  "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'.

Unless issued with a prefix argument or the mark is active, push
mark at the current position."
  (interactive "P")
  (logview--do-relocate #'logview--point-min no-mark))

(defun logview-last-entry (&optional no-mark)
  "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'.

Unless issued with a prefix argument or the mark is active, push
mark at the current position."
  (interactive "P")
  (logview--do-relocate #'logview--point-max no-mark))

(defun logview--do-relocate (where-provider &optional no-mark)
  (logview--assert)
  (unless (or no-mark (region-active-p))
    (push-mark))
  (logview--std-temporarily-widening
    (goto-char (funcall where-provider))
    (logview--locate-current-entry entry start
      (goto-char (logview--entry-message-start entry start)))
    (logview--maybe-pulse-current-entry 'movement)))



;;; 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.  As an
exception to the standard numeric prefix value conventions, 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.  As an
exception to the standard numeric prefix value conventions, 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)
    (narrow-to-region (if upwards
                          (progn (logview--forward-entry (if n (- n) 0))
                                 (logview--locate-current-entry nil start
                                   (max start from)))
                        from)
                      (if upwards
                          to
                        (logview--forward-entry (or n 0))
                        (logview--locate-current-entry entry start
                          (min (logview--entry-end entry start) to))))))

(defun logview-widen ()
  "Remove narrowing, including thread narrowing, from current buffer.
Narrowing to section headers is canceled as well."
  (interactive)
  (logview--assert)
  (widen)
  (setf logview--thread-narrowing-filter-text ""
        logview--narrow-to-section-headers    nil)
  (logview--parse-filters))

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

(defun logview-narrow-to-thread (&optional thread-name)
  "Narrow to specific thread or widen if already narrowed.
When called interactively, use the thread name of the current
entry.  With a prefix argument, read the name from the minibuffer
instead."
  (interactive (list (when current-prefix-arg
                       (logview--assert 'thread)
                       (read-from-minibuffer "Narrow to thread: " nil nil nil logview--thread-name-history
                                             (logview--std-temporarily-widening
                                               (logview--locate-current-entry entry start
                                                 (logview--entry-group entry start logview--thread-group)))))))
  (logview--assert 'thread)
  (unless (or thread-name (cdar (car logview--effective-filter)))
    (logview--std-temporarily-widening
      (logview--locate-current-entry entry start
        (unless entry
          (user-error "There is no current entry, don't know which thread to narrow to"))
        (setf thread-name (logview--entry-group entry start logview--thread-group)))))
  (logview--do-narrow-to-thread thread-name)
  (if thread-name
      (message "Narrowed to thread `%s'" thread-name)
    (message "All thread-narrowing filters got reset")))

(defun logview-edit-thread-narrowing-filters ()
  "Edit thread narrowing filters in a separate buffer."
  (interactive)
  (logview--do-edit-filters 'thread-narrowing-filters))

(defun logview--do-narrow-to-thread (thread-name)
  (setf logview--thread-narrowing-filter-text (if thread-name
                                                  (format "t+ %s\n" (rx-to-string `(seq bol ,thread-name eol) t))
                                                ""))
  (logview--parse-filters))



;;; Filtering by level commands.

(defun logview-show-only-errors ()
  "Show only error entries."
  (interactive)
  (logview--change-min-level-filter (logview--find-min-level 'error)))

(defun logview-show-errors-and-warnings ()
  "Show only error and warning entries."
  (interactive)
  (logview--change-min-level-filter (logview--find-min-level 'warning)))

(defun logview-show-errors-warnings-and-information ()
  "Show error, warning and information entries."
  (interactive)
  (logview--change-min-level-filter (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--change-min-level-filter (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--change-min-level-filter (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-temporarily-widening
    (logview--locate-current-entry entry nil
      (logview--change-min-level-filter (car (nth (- (length logview--submode-level-data) 1 (logview--entry-level entry)) logview--submode-level-data))))))

(defun logview-always-show-errors ()
  "Always show error entries."
  (interactive)
  (logview--change-min-level-filter (logview--find-min-level 'error) t))

(defun logview-always-show-errors-and-warnings ()
  "Always show error and warning entries."
  (interactive)
  (logview--change-min-level-filter (logview--find-min-level 'warning) t))

(defun logview-always-show-errors-warnings-and-information ()
  "Always show error, warning and information entries."
  (interactive)
  (logview--change-min-level-filter (logview--find-min-level 'information) t))

(defun logview-always-show-errors-warnings-information-and-debug ()
  "Always show error, warning, information and debug entries.
I.e. all entries other than traces are shown with no regard to
text filters."
  (interactive)
  (logview--change-min-level-filter (logview--find-min-level 'debug) t))

(defun logview-disable-unconditional-show ()
  "Disable unconditional display of entries.
All entries, regardless of level, will be shown only if they
match the current text filters."
  (interactive)
  (logview--change-min-level-filter nil t))

(defun logview--find-min-level (final-level)
  "Find minimal submode level that maps to given FINAL-LEVEL or higher."
  (logview--assert 'level)
  (let ((lower-level-faces (mapcar (lambda (final-level) (intern (format "logview-%s-entry" (symbol-name final-level))))
                                   (cdr (memq final-level logview--final-levels)))))
    (catch 'level
      (dolist (level-data logview--submode-level-data)
        (unless (memq (cadr (cdr level-data)) lower-level-faces)
          (throw 'level (car level-data)))))))

(defun logview--change-min-level-filter (min-level &optional always-show)
  (when (and min-level (string= min-level (caar logview--submode-level-data)))
    (setq min-level nil))
  (let ((case-fold-search nil)
        (filter-prefix    (if always-show "LV" "lv")))
    (let* ((level-filter-at       (string-match (format "^%s .*$" filter-prefix) logview--main-filter-text))
           (level-filter-line-end (match-end 0)))
      (if level-filter-at
          (setf logview--main-filter-text
                (concat (substring logview--main-filter-text 0 level-filter-at)
                        (substring logview--main-filter-text
                                   (or (string-match-p "^" logview--main-filter-text level-filter-line-end)
                                       level-filter-line-end))))
        (setq level-filter-at 0))
      (when min-level
        (setf logview--main-filter-text (concat (substring logview--main-filter-text 0 level-filter-at)
                                                filter-prefix " " min-level "\n"
                                                (substring logview--main-filter-text level-filter-at))))))
  (logview--parse-filters))



;;; Filtering by name/thread commands.

(defun logview-edit-filters ()
  "Edit the current filters in a separate buffer."
  (interactive)
  (logview--do-edit-filters 'main-filters))

(defun logview-add-include-name-filter ()
  "Show only entries with name matching 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 "a+"))

(defun logview-add-exclude-name-filter ()
  "Hide entries with name matching entered regular expression.
If this command is invoked multiple times, filter out them all,
i.e. show only entries with name that doesn't match any of
entered expression."
  (interactive)
  (logview--prompt-for-new-filter "Logger name regexp to hide entries" 'name "a-"))

(defun logview-add-include-thread-filter ()
  "Show only entries with thread matching 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 "t+"))

(defun logview-add-exclude-thread-filter ()
  "Hide entries with thread matching entered regular expression.
If this command is invoked multiple times, filter out them all,
i.e. show only entries with thread name that doesn't match any of
entered expression."
  (interactive)
  (logview--prompt-for-new-filter "Thread regexp to hide entries" 'thread "t-"))

(defun logview-add-include-message-filter ()
  "Show only entries with message matching regular expression.
Expression may be multiline.  If this command is invoked multiple
times, show entries with message matching at least one of entered
expression."
  (interactive)
  (logview--prompt-for-new-filter "Message regexp to show entries" 'message "m+"))

(defun logview-add-exclude-message-filter ()
  "Hide entries with message matching entered regular expression.
Expression may be multiline.  If this command is invoked multiple
times, filter out them all, i.e. show only entries with message
that doesn't match any of entered expression."
  (interactive)
  (logview--prompt-for-new-filter "Message regexp to hide entries" 'message "m-"))

(defun logview--prompt-for-new-filter (prompt type filter-line-prefix)
  (logview--assert type)
  (let* ((default-value (unless (eq type 'message)
                          (logview--std-temporarily-widening
                            (logview--locate-current-entry entry start
                              (let ((base (regexp-quote (logview--entry-group entry start (pcase type
                                                                                            (`name   logview--name-group)
                                                                                            (`thread logview--thread-group)
                                                                                            (_       (error "Unhandled type `%s'" type)))))))
                                (list base (format "^%s$" base)))))))
         (regexp        (read-regexp prompt default-value (cdr (assq type '((name    . logview--name-regexp-history)
                                                                            (thread  . logview--thread-regexp-history)
                                                                            (message . logview--message-regexp-history)))))))
    (unless (logview--valid-regexp-p regexp)
      (user-error "Invalid regular expression"))
    (when (and (memq type '(name thread)) (string-match-p "\n" regexp))
      (user-error "Regular expression must not span several lines"))
    (setf logview--main-filter-text (concat logview--main-filter-text
                                            (when (and logview--main-filter-text
                                                       (not (string-suffix-p "\n" logview--main-filter-text)))
                                              "\n")
                                            filter-line-prefix " " (replace-regexp-in-string "\n" "\n.. " regexp) "\n"))
    (logview--parse-filters)))

;; This must have been a standard function.
(defun logview--valid-regexp-p (regexp)
  (ignore-errors
    (string-match-p regexp "")
    t))



;;; Filters resetting commands.

(defun logview-reset-level-filters ()
  "Reset all level filters.
This includes both minimal level to show entries and minimal
level to show entries regardless of text filters."
  (interactive)
  (logview--assert 'level)
  (logview-show-all-levels)
  (logview-disable-unconditional-show))

(defun logview-reset-name-filters ()
  "Reset all name filters."
  (interactive)
  (logview--assert 'name)
  (logview--parse-filters '("a+" "a-")))

(defun logview-reset-thread-filters ()
  "Reset all thread filters."
  (interactive)
  (logview--assert 'thread)
  (logview--parse-filters '("t+" "t-")))

(defun logview-reset-message-filters ()
  "Reset all message filters."
  (interactive)
  (logview--assert)
  (logview--parse-filters '("m+" "m-")))

(defun logview-reset-all-filters ()
  "Reset all filters (level, name, thread).
After this command only entries hidden by the thread-narrowing
filters, hidden explictly, and entries outside narrowing buffer
restrictions remain invisible.  Since narrowing to section
headers doesn't count as a filter, it is also kept intact if
active."
  (interactive)
  (logview--do-reset-all-filters nil nil nil nil))

(defun logview-reset-all-filters-restrictions-and-hidings ()
  "Reset all visibility restrictions.
In other words, reset all filters, show all explictly hidden
entries and cancel any narrowing restrictions, including
narrowing to threads and to section headers."
  (interactive)
  (widen)
  (logview--do-reset-all-filters t t t t))

(defun logview--do-reset-all-filters (also-reset-thread-narrowing also-cancel-section-header-narrowing also-show-details also-cancel-explicit-hiding)
  (logview--assert)
  (when also-show-details
    (setq logview--hide-all-details nil))
  (when also-cancel-explicit-hiding
    (logview--retire-hiding-symbol 'logview--hidden-entry-symbol)
    (logview--retire-hiding-symbol 'logview--hidden-details-symbol))
  (when also-reset-thread-narrowing
    (setf logview--thread-narrowing-filter-text ""))
  (when also-cancel-section-header-narrowing
    (setf logview--narrow-to-section-headers nil))
  (unless (logview--parse-filters logview--valid-filter-prefixes)
    (logview--update-invisibility-spec)))



;;; View commands.

(defun logview-switch-to-view (view)
  "Switch to a previously defined view.
Argument VIEW can either be a string (view name) or a number, in
which case view with that index is activated.

If called interactively with a prefix argument, use its numeric
value as quick access index.  Otherwise, read the view name from
the minibuffer."
  (interactive (list (logview--choose-view "Switch to view: " current-prefix-arg)))
  (setf logview--main-filter-text (plist-get (logview--find-view view) :filters))
  (logview--parse-filters))

(defun logview-switch-to-view-by-index ()
  "Switch to a view by its quick access index.
This command must be bound to a key with a numeric values,
possibly with modifiers, e.g. `3' or `M-3'.

It is only for interactive use.  Non-interactively, use
`logview-switch-to-view' instead."
  (interactive)
  (let* ((char  (if (integerp last-command-event)
                    last-command-event
                  (get last-command-event 'ascii-character)))
     (index (- (logand char #x7f) ?0)))
    (unless (<= 0 index 9)
      (user-error "This command must invoked by a numeric key, possibly with modifiers"))
    (logview-switch-to-view index)))

(defun logview-set-section-view (view)
  "Use given view to split log file into sections.
Argument VIEW can either be a string (view name) or a number, in
which case view with that index is used.  Views can be either
thread-bound or not, use `\\<logview-mode-map>\\[logview-toggle-sections-thread-bound]' to toggle.

If called interactively with a prefix argument, use its numeric
value as quick access index.  Otherwise, read the view name from
the minibuffer."
  (interactive (list (logview--choose-view "View to determine sections: " current-prefix-arg)))
  (logview--do-set-section-view (plist-get (logview--find-view view) :name)))

(defun logview-set-navigation-view (view)
  "Set a view to be used for navigation.
Argument VIEW can either be a string (view name) or a number, in
which case view with that index is activated.

If called interactively with a prefix argument, use its numeric
value as quick access index.  Otherwise, read the view name from
the minibuffer.

Navigation view filters are not active in the normal sense, but
you can use `\\<logview-mode-map>\\[logview-next-navigation-view-entry]' and `\\<logview-mode-map>\\[logview-previous-navigation-view-entry]' keys to move across its entries."
  (interactive (list (logview--choose-view "Navigate through view: " current-prefix-arg)))
  (setq logview--navigation-view-name (plist-get (logview--find-view view) :name)))

(defun logview-highlight-view-entries (view)
  "Set a view to be used for entry highlighting.
Argument VIEW can either be a string (view name) or a number, in
which case view with that index is activated.

If called interactively with a prefix argument, use its numeric
value as quick access index.  Otherwise, read the view name from
the minibuffer."
  (interactive (list (logview--choose-view "Highlight entries of a view: " current-prefix-arg)))
  (logview--do-highlight-view-entries (plist-get (logview--find-view view) :name)))

(defun logview-unhighlight-view-entries ()
  (interactive)
  (setq logview--highlighted-view-name nil)
  (logview--do-highlight-view-entries nil))

(defun logview-save-filters-as-view-for-submode (name)
  "Save the current filter set as a view for the current submode.
Interactively, read the name for the new view from the
minibuffer."
  (interactive (list nil))
  (logview--do-save-filters-as-view name nil))

(defun logview-save-filters-as-global-view (name)
  "Save the current filter set as a global view.
Interactively, read the name for the new view from the
minibuffer."
  (interactive (list nil))
  (logview--do-save-filters-as-view name t))

(defun logview-edit-submode-views ()
  "Edit views for the current submode in a separate buffer."
  (interactive)
  (logview--do-edit-views t))

(defun logview-edit-all-views ()
  "Edit all views in a separate buffer."
  (interactive)
  (logview--do-edit-views nil))

(defun logview-assign-quick-access-index (index)
  (interactive (list (when (logview--current-view)
                       ;; Of course `read-number' insists on the default value being a
                       ;; number and also stuffs it into the prompt.  Have to write our
                       ;; own, wonderful...
                       (let (index)
                         (while (let ((string (read-from-minibuffer "View quick access index (empty for none): ")))
                                  (cond ((equal string "")
                                         nil)
                                        ((integerp (ignore-errors (read string)))
                                         (setq index (string-to-number string))
                                         nil)
                                        (t
                                         (message "Please enter a number")
                                         (sit-for 1)
                                         t))))
                         index))))
  (let ((view (logview--current-view)))
    (unless view
      (user-error "Activate a view first"))
    (plist-put view :index index)
    (setq logview--views-need-saving t)
    (logview--update-mode-name)))

(defun logview-delete-view (view)
  "Delete a view definition.
Interactively, read the view name from the minibuffer.  Views
cannot be deleted using their quick access indices."
  ;; Intentionally not supporting prefix argument here: would be too error-prone.
  (interactive (list (logview--choose-view "Delete view: ")))
  (setq logview--views             (delq (logview--find-view view) (logview--views))
        logview--views-need-saving t)
  (logview--after-updating-view-definitions)
  (logview--update-mode-name))

(defun logview--choose-view (prompt &optional prefix-arg-value)
  (if prefix-arg-value
      (prefix-numeric-value prefix-arg-value)
    (let (defined-names)
      (dolist (view (logview--views))
        (when (or (null (plist-get view :submode)) (string= (plist-get view :submode) logview--submode-name))
          (push (plist-get view :name) defined-names)))
      (unless defined-names
        (user-error "There are no views defined for the current submode"))
      (logview--completing-read prompt defined-names nil t nil 'logview--view-name-history))))

(defun logview--do-save-filters-as-view (name global)
  (unless (caar (car logview--effective-filter))
    (user-error "There are currently no filters"))
  (unless name
    (setq name (read-string "Save as: " nil 'logview--view-name-history)))
  (when (= (length name) 0)
    (user-error "View name may not be empty"))
  (let ((matches (lambda (view)
                   (and (string= (plist-get view :name) name)
                        (or global (null (plist-get view :submode)) (string= (plist-get view :submode) logview--submode-name))))))
    (dolist (view (logview--views))
      (when (funcall matches view)
        (unless (y-or-n-p (format-message (if global
                                              "There is already a view named `%s'. Replace it?"
                                            "There is already a view named `%s' for this submode. Replace it?")
                                          name))
          (user-error "View named `%s' already exists; try a different name" name))))
    (let (new-views)
      (dolist (view (logview--views))
        (unless (funcall matches view)
          (push view new-views)))
      (push (list :name name :filters logview--main-filter-text) new-views)
      (unless global
        (plist-put (car new-views) :submode logview--submode-name))
      (setq logview--views             (nreverse new-views)
            logview--views-need-saving t)
      (logview--after-updating-view-definitions)
      (logview--update-mode-name)
      (message (if global "Saved filters as a global view named `%s'" "Saved filters as a submode view named `%s'") name))))

(defun logview--do-edit-views (submode-only)
  (let ((self    (current-buffer))
        (windows (current-window-configuration))
        (submode (when submode-only logview--submode-name)))
    (if (buffer-live-p logview--view-editing-buffer)
        (when (and (buffer-modified-p logview--view-editing-buffer)
                   (not (eq (with-current-buffer logview--view-editing-buffer
                              logview-filter-edit--editing-views-for-submode)
                            submode)))
          (pop-to-buffer logview--view-editing-buffer)
          (unless (yes-or-no-p "Discard current view editing changes?")
            (user-error "Another view editing is in progress")))
      (setq logview--view-editing-buffer (generate-new-buffer "Logview views")))
    (split-window-vertically)
    (other-window 1)
    (switch-to-buffer logview--view-editing-buffer)
    (unless (eq major-mode 'logview-filter-edit-mode)
      (logview-filter-edit-mode))
    (setf logview-filter-edit--mode                      'views
          logview-filter-edit--editing-views-for-submode submode
          logview-filter-edit--parent-buffer             self
          logview-filter-edit--window-configuration      windows)
    (logview-filter-edit--initialize-text)))



;;; Section commands.

(defun logview-go-to-section-beginning (&optional n set-view-if-needed)
  "Move point to the beginning of current section.
If the actual first section's entry is not visible, go to the
earliest visible one.

If an argument N is specified, move N - 1 sections forward
first (for consistency with standard commands like `C-a').
Sections that consist only of hidden entries (due to any reasons)
are ignored.

When called interactively and there is no section-defining view
yet (or it got deleted or renamed), ask for the view first.  You
can use command `\\<logview-mode-map>\\[logview-set-section-view]' to change that later."
  ;; Second "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p\np")
  (logview--std-temporarily-widening
    (logview--do-forward-section-as-command set-view-if-needed (if n (1- n) 0))))

(defun logview-go-to-section-end (&optional n set-view-if-needed)
  "Move point to the end of current section.
If the actual last section's entry is not visible, go to the
latest visible one.

If an argument N is specified, move N - 1 sections forward
first (for consistency with standard commands like `C-e').
Sections that consist only of hidden entries (due to any reasons)
are ignored.

When called interactively and there is no section-defining view
yet (or it got deleted or renamed), ask for the view first.  You
can use command `\\<logview-mode-map>\\[logview-set-section-view]' to change that later."
  ;; Second "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p\np")
  (logview--std-temporarily-widening
    (logview--do-forward-section-as-command set-view-if-needed (if n (1- n) 0) t)))

(defun logview-next-section (&optional n set-view-if-needed)
  "Move point down N (1 by default) log sections.
Point is positioned at the beginning of the message of the first
visible entry (usually the header) of the resulting section"
  ;; Second "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p\np")
  (unless n
    (setf n 1))
  (logview--std-temporarily-widening
    (logview--maybe-complain-about-movement n (logview--do-forward-section-as-command set-view-if-needed n) 'section)))

(defun logview-previous-section (&optional n set-view-if-needed)
  "Move point up N (1 by default) log sections.
Point is positioned at the beginning of the message of the first
visible entry (usually the header) of the resulting section"
  ;; Second "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p\np")
  (logview-next-section (if n (- n) -1) set-view-if-needed))

(defun logview-next-section-any-thread (&optional n set-view-if-needed)
  "Move point down N (1 by default) log sections regarless of thread.
This is like `logview-next-section', only temporary pretending
that sections are not thread-bound."
  ;; Second "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p\np")
  (let ((logview--sections-thread-bound nil))
    (logview-next-section n set-view-if-needed)))

(defun logview-previous-section-any-thread (&optional n set-view-if-needed)
  "Move point up N (1 by default) log sections regarless of thread.
This is like `logview-previous-section', only temporary
pretending that sections are not thread-bound.  Because of this,
results may be somewhat unexpected, e.g. going to the header of
the current section if there is exactly one intervening section
header in a different thread."
  ;; Second "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p\np")
  (logview-next-section-any-thread (if n (- n) -1) set-view-if-needed))

(defun logview-first-section (&optional set-view-if-needed)
  "Move point to the first (visible) section in this thread.
Point is positioned at the beginning of the message of the
visible entry (usually the header) of the section."
  ;; The "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p")
  (unless (region-active-p)
    (push-mark))
  (logview--std-temporarily-widening
    (logview--do-forward-section-as-command set-view-if-needed most-negative-fixnum)))

(defun logview-last-section (&optional set-view-if-needed)
  "Move point to the last (visible) section in this thread.
Point is positioned at the beginning of the message of the
visible entry (usually the header) of the section."
  ;; The "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p")
  (unless (region-active-p)
    (push-mark))
  (logview--std-temporarily-widening
    (when (> (logview--do-forward-section-as-command set-view-if-needed most-positive-fixnum) 0)
      (logview--forward-section 0))))

(defun logview-first-section-any-thread (&optional set-view-if-needed)
  "Move point to the first (visible) section regardless of thread.
This is like `logview-first-section', only temporary pretending
that sections are not thread-bound."
  ;; The "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p")
  (let ((logview--sections-thread-bound nil))
    (logview-first-section set-view-if-needed)))

(defun logview-last-section-any-thread (&optional set-view-if-needed)
  "Move point to the last (visible) section regardless of thread.
This is like `logview-last-section', only temporary pretending
that sections are not thread-bound."
  ;; The "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive "p")
  (let ((logview--sections-thread-bound nil))
    (logview-last-section set-view-if-needed)))

(defun logview-narrow-to-section (&optional n set-view-if-needed)
  "Narrow the buffer so that only the current section is visible.
If called interactively with a prefix argument, move forward or
backward by that many sections first, thus showing a section
other than the one point was originally in.

When the current submode has a thread concept, also replace
thread narrowing filters so that only the thread of the section
is visible."
  ;; Second "p" is only needed so that SET-VIEW-IF-NEEDED is non-nil when called
  ;; interactively.
  (interactive (list (when current-prefix-arg
                       (prefix-numeric-value current-prefix-arg))
                     t))
  (logview-narrow-to-section-keep-threads n set-view-if-needed)
  (when (memq 'thread logview--submode-features)
    (logview--do-narrow-to-thread (logview--std-temporarily-widening
                                    (logview--locate-current-entry entry start
                                      (logview--entry-group entry start logview--thread-group))))))

(defun logview-narrow-to-section-keep-threads (&optional n set-view-if-needed)
  "Like `logview-narrow-to-section', but preserve thread narrowing."
  (interactive (list (when current-prefix-arg
                       (prefix-numeric-value current-prefix-arg))
                     t))
  (logview--assert)
  (logview--ensure-section-view set-view-if-needed)
  (unless n
    (setf n 0))
  (save-excursion
    (logview--std-temporarily-widening
      ;; When movement fails, just give the error message and abort.
      (logview--maybe-complain-about-movement n (logview--forward-section n t) 'section))
    (logview-narrow-up-to-this-entry)
    (logview--std-temporarily-widening
      (logview--do-forward-section-as-command nil 0)
      (logview--locate-current-entry entry start
        (if (funcall (cdr logview--section-header-filter) entry start)
            (message "Narrowed to section `%s'" (logview--trim-for-display (logview--entry-message entry start)))
          (message (concat "Narrowed to the first, unnamed section" (if (logview-sections-thread-bound-p) " of this thread" ""))))))
    (logview-narrow-from-this-entry)))

(defun logview-toggle-narrow-to-section-headers (&optional arg set-view-if-needed)
  "Toggle whether only sections headers are shown.
Leaving only section headers visible is useful for long-distance
navigation, i.e. when you want to find and switch to a completely
different part of the log.  Section headers, typically, should
provide a good overview of log structure.

If invoked with prefix argument, leave only headers visible if
the argument is positive, show full sections otherwise otherwise.

If this command is used when performing an incremental search
(with `\\<logview-isearch-map>\\[logview-toggle-narrow-to-section-headers]'), the change is temporary and lasts only until the
search is ended.  This is for consistency with e.g. `M-s' or
`M-r' during Isearch."
  (interactive (list (or current-prefix-arg 'toggle)
                     ;; Don't have UI to pick a view if already searching.
                     (not isearch-mode)))
  (logview--ensure-section-view set-view-if-needed)
  (logview--toggle-option-locally 'logview--narrow-to-section-headers arg (called-interactively-p 'interactive)
                                  "Showing only section headers"
                                  "Showing both section headers and contents, as usually"
                                  (lambda ()
                                    (logview--parse-filters)
                                    (logview--isearch-update-if-running))))

(defun logview-toggle-sections-thread-bound (&optional arg)
  "Toggle whether sections are bound to threads.
Section-defining view determines which entries start sections.
If sections are thread-bound (the default), only another match
within the same thread ends a section.  Otherwise, any match ends
the previous section.  If sections are thread-bound, they can
overlap.

If invoked with prefix argument, make sections thread-bound if
the argument is positive, non-bound otherwise."
  (interactive (list (or current-prefix-arg 'toggle)))
  (logview--assert 'thread)
  (when (and (logview--toggle-option-locally 'logview--sections-thread-bound arg (called-interactively-p 'interactive)
                                             "Sections are now thread-bound and can overlap"
                                             "Sections are not thread-bound anymore and are always sequential")
             logview--timestamp-difference-to-section-headers)
    (logview--refontify-buffer)))

(defun logview-sections-thread-bound-p ()
  (and logview--sections-thread-bound (memq 'thread logview--submode-features)))

(defun logview--ensure-section-view (set-view-if-needed)
  (unless (logview--find-view logview--section-view-name t)
    (if set-view-if-needed
        (logview--do-set-section-view (logview--choose-view (substitute-command-keys
                                                             "View that defines sections (change with `\\[logview-set-section-view]'): ")))
      (error "There is no active section view"))))

(defun logview--do-forward-section-as-command (set-view-if-needed n &optional go-to-end)
  (logview--assert)
  (logview--ensure-section-view set-view-if-needed)
  (prog1 (logview--forward-section n go-to-end)
    (logview--maybe-pulse-current-entry 'section-movement)))

(defun logview--forward-section (n &optional go-to-end)
  (logview--locate-current-entry last-valid-entry last-valid-start
    ;; The logic of this code is pretty much impossible to follow.  Just trust it as long
    ;; as it passes the tests.
    (let* ((forward         (or (> n 0) (and (= n 0) go-to-end)))
           (limit           (if forward (logview--point-max) (logview--point-min)))
           (section-thread  (when (logview-sections-thread-bound-p)
                              (logview--entry-group last-valid-entry last-valid-start logview--thread-group)))
           (header-filter   (cdr logview--section-header-filter))
           ;; Current section is always visible.
           (visible-section t)
           (callback        (lambda (entry start)
                              ;; See comment above iterating calls.
                              (and (if forward (<= start limit) (> (logview--entry-end entry start) limit))
                                   ;; Ignore entries belonging to a "wrong" thread and continue.
                                   (or (and section-thread (not (string= (logview--entry-group entry start logview--thread-group) section-thread)))
                                       (let ((header (funcall header-filter entry start)))
                                         (if (and (= n 0) forward go-to-end header)
                                             nil
                                           (let ((invisible (invisible-p start)))
                                             (when header
                                               (unless (or invisible forward)
                                                 (setf visible-section t))
                                               (when visible-section
                                                 (setf n (if forward (1- n) (1+ n))))
                                               (when forward
                                                 (setf visible-section nil)))
                                             (or invisible
                                                 (progn
                                                   (setf last-valid-entry entry
                                                         last-valid-start start)
                                                   (when (or forward (not header))
                                                     (setf visible-section t))
                                                   (or (if forward
                                                           (or (> n 0) go-to-end)
                                                         (when (if go-to-end
                                                                   (or (< n 0) header)
                                                                 (or (<= n 0) (not header)))
                                                           (when header
                                                             (setf visible-section nil))
                                                           t))
                                                       (progn (setf n 0) nil))))))))))))
      ;; We don't skip invisible entries, since we need to detect section bounds even if
      ;; the headers are filtered out.  Therefore, the callback needs to take narrowing
      ;; into account explicitly.
      (if forward
          (logview--iterate-entries-forward last-valid-start callback nil nil t)
        (logview--iterate-entries-backward  last-valid-start callback))
      (goto-char (logview--entry-message-start last-valid-entry last-valid-start))
      (unless visible-section
        (setf n (if forward (1+ n) (1- n))))
      n)))



;;; Explicit entry hiding/showing commands.

(defun logview-hide-entry (&optional n interactive)
  "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)
                     t))
  (if (eq n 'use-region)
      (logview-hide-region-entries (point) (mark) interactive)
    (logview--assert)
    (unless n
      (setq n 1))
    (logview--std-temporarily-widening
      (logview--std-altering
        (logview--maybe-complain-about-movement n (logview--iterate-successive-entries (point) n #'logview--hide-entry-callback t))))))

(defun logview-hide-region-entries (begin end &optional interactive)
  "Explicitly hide all log entries in the region.
Entries that are in the region only partially are hidden as well.

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\np")
  (logview--assert)
  (logview--std-temporarily-widening
    (logview--std-altering
      (logview--iterate-entries-in-region begin end #'logview--hide-entry-callback)
      (when interactive
        (setq deactivate-mark t)))))

(defun logview-show-entries (&optional n interactive)
  "Show explicitly hidden entries.
By default, explicitly hidden entries between the current and the
next visible are shown.  If invoked with prefix argument, entries
between the current entry and N'th after it (or before it if N is
negative) are shown.

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) interactive)
    (logview--assert)
    (unless n
      (setq n 1))
    (logview--std-temporarily-widening
      (logview--std-altering
        ;; Much like 'logview--iterate-successive-entries', but because of
        ;; peculiar semantics, not broken out into its own function.
        (when (/= n 0)
          (let ((direction (cl-signum n)))
            (funcall (if (> n 0) #'logview--iterate-entries-forward #'logview--iterate-entries-backward)
                     (point)
                     (lambda (entry entry-at)
                       (if (invisible-p entry-at)
                           (progn (logview--show-entry-callback entry entry-at)
                                  t)
                         (/= (setq n (- n direction)) 0)))
                     nil nil t))
          (logview--maybe-complain-about-movement n n))))))

(defun logview-show-region-entries (begin end &optional interactive)
  "Explicitly show all log entries in the region.

Note that entries that are currently hidden due to filtering are
also marked as “not explicitly hidden”.  However, you will see
any effect only once you clear or alter the responsible filters."
  (interactive "r\np")
  (logview--assert)
  (logview--std-temporarily-widening
    (logview--std-altering
      (logview--iterate-entries-in-region begin end #'logview--show-entry-callback)
      (when interactive
        (setq deactivate-mark t)))))

(defun logview-reset-manual-entry-hiding ()
  "Show all manually hidden entries in the buffer.

Entries currently not visible due to filtering are also cleared
of “manually hidden” mark.  However, such entries will remain
invisible until filters are removed or changed appropriately."
  (interactive)
  (logview--assert)
  (logview--retire-hiding-symbol 'logview--hidden-entry-symbol)
  (logview--update-invisibility-spec))

(defun logview--hide-entry-callback (entry start)
  (logview--update-entry-invisibility start (logview--entry-details-start entry start) (logview--entry-end entry start)
                                      'propagate t 'propagate))

(defun logview--show-entry-callback (entry start)
  (logview--update-entry-invisibility start (logview--entry-details-start entry start) (logview--entry-end entry start)
                                      'propagate nil 'propagate))



;;; Showing/hiding entry details commands.

(defun logview-toggle-entry-details (&optional arg interactive)
  "Toggle whether details for current entry are shown.
If invoked with prefix argument, show them if the argument is
positive, hide otherwise.

In Transient Mark mode, if the region is active, call
`logview-toggle-region-entry-details'.  See that function help
for how toggling works."
  (interactive (list (if (use-region-p)
                         (list (or current-prefix-arg 'toggle))
                       (or current-prefix-arg 'toggle))
                     t))
  (if (consp arg)
      (logview-toggle-region-entry-details (point) (mark) (car arg) interactive)
    (logview--std-temporarily-widening
      (logview--std-altering
        (logview--locate-current-entry entry start
          (let ((details-start (logview--entry-details-start entry start)))
            (unless details-start
              (user-error "Current entry has no details"))
            (logview--update-entry-invisibility start details-start (logview--entry-end entry start)
                                                'propagate 'propagate (if (eq arg 'toggle)
                                                                          (not (memq logview--hidden-details-symbol (get-text-property details-start 'invisible)))
                                                                        (> (prefix-numeric-value arg) 0)))))))))

(defun logview-toggle-region-entry-details (begin end &optional arg interactive)
  "Toggle whether details in the region are shown.
Toggling works like this: if at least one entry in the region has
visible details, all are hidden.  Otherwise, if all are already
hidden, they are shown.  If invoked with prefix argument, show
details if the argument is positive, hide otherwise.

Entries that are in the region only partially are operated on as
well."
  (interactive (list (point) (mark) (or current-prefix-arg 'toggle) t))
  (logview--std-temporarily-widening
    (logview--std-altering
      (when (eq arg 'toggle)
        (setq arg 1)
        (logview--iterate-entries-in-region begin end (lambda (entry start)
                                                        (let ((details-start (logview--entry-details-start entry start)))
                                                          (if (or (null details-start)
                                                                  (memq logview--hidden-details-symbol (get-text-property details-start 'invisible)))
                                                              t
                                                            (setq arg 0)
                                                            nil)))))
      (let ((hide (<= (prefix-numeric-value arg) 0)))
        (logview--iterate-entries-in-region begin end (lambda (entry start)
                                                        (logview--update-entry-invisibility start (logview--entry-details-start entry start) (logview--entry-end entry start)
                                                                                            'propagate 'propagate hide))))
      (when interactive
        (setq deactivate-mark t)))))

(defun logview-toggle-details-globally (&optional arg)
  "Toggle whether details are shown in the whole buffer.
If invoked with prefix argument, show details if the argument is
positive, hide otherwise.

Note that this is separate from manual detail hiding with `\\<logview-mode-map>\\[logview-toggle-entry-details]' and
`\\<logview-mode-map>\\[logview-toggle-region-entry-details]'.  Global detail hiding takes precedence: when it is active,
all details are hidden in the buffer.  However, when it becomes
inactive (e.g. after toggling two times), only those details
which haven't been manually hidden are visible."
  (interactive (list (or current-prefix-arg 'toggle)))
  (logview--toggle-option-locally 'logview--hide-all-details arg (called-interactively-p 'interactive)
                                  "All entry messages details are now hidden"
                                  "Details of entry messages are now visible unless hidden explicitly")
  (logview--update-invisibility-spec))

(defun logview-reset-manual-details-hiding ()
  "Show all manually hidden entry details in the buffer."
  (interactive)
  (logview--assert)
  (logview--retire-hiding-symbol 'logview--hidden-details-symbol)
  (logview--toggle-option-locally 'logview--hide-all-details 0)
  (logview--update-invisibility-spec))



;;; Timestamp difference commands.

(defun logview-difference-to-current-entry ()
  "Display difference to current entry's timestamp.
Difference is shown for all other entries.  Any thread-specific
difference bases (appointed with `\\<logview-mode-map>\\[logview-thread-difference-to-current-entry]') are removed and displaying
of differences to section headers (see `\\<logview-mode-map>\\[logview-difference-to-section-headers]') is canceled."
  (interactive)
  (logview--assert 'timestamp)
  (logview--std-temporarily-widening
    (logview--locate-current-entry entry start
      ;; Make sure that it is parsed.
      (logview--entry-timestamp entry start)
      (let ((base (cons entry start)))
        (unless (and (equal logview--timestamp-difference-base base)
                     (null logview--timestamp-difference-per-thread-bases)
                     (not logview--timestamp-difference-to-section-headers))
          (setf logview--timestamp-difference-base               base
                logview--timestamp-difference-per-thread-bases   nil
                logview--timestamp-difference-to-section-headers nil)
          (logview--refontify-buffer))))))

(defun logview-thread-difference-to-current-entry ()
  "Display difference to current entry's timestamp in its thread.
In case there is a global difference base (appointed with `\\<logview-mode-map>\\[logview-difference-to-current-entry]'),
it stays in effect for other threads.  Similarly, activated display
of differences to section headers (see `\\<logview-mode-map>\\[logview-difference-to-section-headers]') continues for other
threads."
  (interactive)
  (logview--assert 'timestamp 'thread)
  (logview--std-temporarily-widening
    (logview--locate-current-entry entry start
      ;; Make sure that it is parsed.
      (logview--entry-timestamp entry start)
      (let ((base   (cons entry start))
            (thread (logview--entry-group entry start logview--thread-group)))
        (unless (and logview--timestamp-difference-per-thread-bases
                     (equal (gethash thread logview--timestamp-difference-per-thread-bases) base))
          (unless logview--timestamp-difference-per-thread-bases
            (setq logview--timestamp-difference-per-thread-bases (make-hash-table :test #'equal)))
          (puthash thread base logview--timestamp-difference-per-thread-bases)
          (logview--refontify-buffer))))))

(defun logview-difference-to-section-headers (&optional set-view-if-needed)
  "Display difference to section header timestamp everywhere.
All global (see `\\<logview-mode-map>\\[logview-difference-to-current-entry]') and thread-specific difference bases (`\\<logview-mode-map>\\[logview-thread-difference-to-current-entry]')
are removed."
  (interactive "p")
  (logview--assert 'timestamp)
  (logview--ensure-section-view set-view-if-needed)
  (logview--std-temporarily-widening
    (unless (and (null logview--timestamp-difference-base)
                 (null logview--timestamp-difference-per-thread-bases)
                 logview--timestamp-difference-to-section-headers)
          (setf logview--timestamp-difference-base               nil
                logview--timestamp-difference-per-thread-bases   nil
                logview--timestamp-difference-to-section-headers t)
          (logview--refontify-buffer))))

(defun logview-go-to-difference-base-entry ()
  (interactive)
  (logview--assert 'timestamp)
  (logview--std-temporarily-widening
    (logview--locate-current-entry entry start
      (let* ((thread          (when (memq 'thread logview--submode-features)
                                (logview--entry-group entry start logview--thread-group)))
             (difference-base (or (when logview--timestamp-difference-per-thread-bases
                                    (gethash thread logview--timestamp-difference-per-thread-bases))
                                  (if logview--timestamp-difference-to-section-headers
                                      (save-excursion
                                        (logview--forward-section 0)
                                        (logview--locate-current-entry entry start
                                          (when (funcall (cdr logview--section-header-filter) entry start)
                                            `(,entry . ,start))))
                                    logview--timestamp-difference-base))))
        (unless difference-base
          (user-error "There is no timestamp difference base for the current entry"))
        (when (invisible-p (cdr difference-base))
          (user-error "Timestamp difference base is either hidden or not in the current file contents anymore (e.g. due to log rotation)"))
        (let ((entry (car difference-base))
              (start (cdr difference-base)))
          (unless (and (< start (logview--point-max)) (> (logview--entry-end entry start) (logview--point-min)))
            (user-error "Difference base entry is outside the narrowing region"))
          (goto-char (logview--entry-message-start entry start))
          (logview--maybe-pulse-current-entry 'movement))))))

(defun logview-forget-difference-base-entries ()
  "Don't replace entry timestamps with differences anywhere.
This cancels effects of commands `\\<logview-mode-map>\\[logview-difference-to-current-entry]', `\\<logview-mode-map>\\[logview-thread-difference-to-current-entry]' and `\\<logview-mode-map>\\[logview-difference-to-section-headers]'."
  (interactive)
  (logview--assert 'timestamp)
  (unless (and (null logview--timestamp-difference-base)
               (null logview--timestamp-difference-per-thread-bases)
               (not  logview--timestamp-difference-to-section-headers))
    (setf logview--timestamp-difference-base               nil
          logview--timestamp-difference-per-thread-bases   nil
          logview--timestamp-difference-to-section-headers nil)
    (logview--refontify-buffer)))

(defun logview-forget-thread-difference-base-entry ()
  "Don't replace entry timestamps with differences in this thread.
However, if there is a global replacement in effect (i.e. not
specific to this thread: see commands `\\<logview-mode-map>\\[logview-difference-to-current-entry]' and `\\<logview-mode-map>\\[logview-difference-to-section-headers]'), it will
get activated instead."
  (interactive)
  (logview--assert 'timestamp 'thread)
  (logview--std-temporarily-widening
    (logview--locate-current-entry entry start
      (let* ((thread          (logview--entry-group entry start logview--thread-group))
             (difference-base (when logview--timestamp-difference-per-thread-bases
                                (gethash thread logview--timestamp-difference-per-thread-bases))))
        (unless difference-base
          (user-error "There is no thread-specific timestamp difference base for thread `%s'" thread))
        (remhash thread logview--timestamp-difference-per-thread-bases)
        (logview--refontify-buffer)))))

(defun logview-cancel-difference-to-section-headers ()
  "Don't replace entry timestamps with differences to section headers.
If section headers are not used as timestamp bases (see command `\\<logview-mode-map>\\[logview-difference-to-section-headers]'),
this command doesn't do anything."
  (interactive)
  (logview--assert 'timestamp)
  (if logview--timestamp-difference-to-section-headers
      (progn (setf logview--timestamp-difference-to-section-headers nil)
             (logview--refontify-buffer))
    (user-error "Not showing timestamp differences to section headers, nothing to cancel")))



;;; Timestamp gap commands.

(defun logview-next-timestamp-gap (&optional n)
  "Move to the next large gap in entry timestamps.
If N is specified, use that as a gap count.  Filtered out entries
are ignored.

Point is positioned at the beginning of the message of the first
entry after the gap.  If there is a large enough gap just after
the current entry and N is one, this command just moves one entry
down."
  (interactive "p")
  (logview--do-next-timestamp-gap n nil))

(defun logview-previous-timestamp-gap (&optional n)
  "Move to the previous large gap in entry timestamps.
If N is specified, use that as a gap count.  Filtered out entries
are ignored.

Point is positioned at the beginning of the message of the first
entry after the gap."
  (interactive "p")
  (logview--do-next-timestamp-gap (if n (- n) -1) nil))

(defun logview-next-timestamp-gap-in-this-thread (&optional n)
  "Move to the next large gap in thread's entry timestamps.
If N is specified, use that as a gap count.  Only consider
entries within this thread that are not filtered out.

Point is positioned at the beginning of the message of the first
entry after the gap."
  (interactive "p")
  (logview--do-next-timestamp-gap n t))

(defun logview-previous-timestamp-gap-in-this-thread (&optional n)
  "Move to the previous large gap in thread's entry timestamps.
If N is specified, use that as a gap count.  Only consider
entries within this thread that are not filtered out.

Point is positioned at the beginning of the message of the first
entry after the gap."
  (interactive "p")
  (logview--do-next-timestamp-gap (if n (- n) -1) t))

(defun logview--do-next-timestamp-gap (n same-thread-only)
  (logview--assert 'timestamp)
  (when same-thread-only
    (logview--assert 'thread))
  (unless n
    (setq n 1))
  (logview--std-temporarily-widening
    (logview--locate-current-entry started-at-entry started-at
      (let ((thread (when same-thread-only
                      (logview--entry-group started-at-entry started-at logview--thread-group))))
        (when (< n 0)
          (logview--forward-entry -1 (when thread
                                       (lambda (entry start)
                                         (string-equal (logview--entry-group entry start logview--thread-group) thread)))))
        (logview--locate-current-entry entry start
          (let ((remaining (logview--forward-entry n (logview--entry-timestamp-gap-validator entry start thread)))
                (success   (when logview--last-found-large-gap
                             (format "Gap to the previous entry%s is %s s"
                                     (if same-thread-only " of this thread" "")
                                     (format logview--timestamp-gap-format-string logview--last-found-large-gap))))
                (failure   (format "No more large enough (%s s or more) gaps in timestamps%s"
                                   (logview--target-gap-length) (if same-thread-only " in this thread" ""))))
            (when (< n 0)
              (if (= remaining n)
                  (goto-char (logview--entry-message-start started-at-entry started-at))
                (logview--forward-entry 1)))
            (logview--maybe-pulse-current-entry 'timestamp-gap)
            (cond ((= n 0))
                  ((= remaining n)
                   (user-error "%s" failure))
                  ((= remaining 0)
                   (message "%s" success))
                  (t
                   (user-error "%s; %s" success (downcase failure))))))))))

(defun logview-change-target-gap-length (length)
  (interactive (list (if current-prefix-arg
                         (prefix-numeric-value current-prefix-arg)
                       (string-to-number (read-from-minibuffer (format "Search for gap this long (currently %s s): " (logview--target-gap-length)))))))
  (if (and (numberp length) (>= length 0))
      (setq logview--buffer-target-gap-length (when (/= length 0) length))
    (user-error "Expected a non-negative positive number")))

(defun logview--entry-timestamp-gap-validator (entry start thread)
  (let ((timestamp  (logview--entry-timestamp entry start))
        (target-gap (logview--target-gap-length)))
    (lambda (entry start)
      (when (or (null thread) (string-equal (logview--entry-group entry start logview--thread-group) thread))
        (let* ((new-timestamp (logview--entry-timestamp entry start))
               (gap           (abs (- new-timestamp timestamp))))
          (setq timestamp new-timestamp)
          (when (>= gap target-gap)
            (setq logview--last-found-large-gap gap)))))))

(defun logview--target-gap-length ()
  (let ((target-gap (or logview--buffer-target-gap-length logview-target-gap-length)))
    (if (and (numberp target-gap) (> target-gap 0))
        target-gap
      ;; Might want to issue a user error instead.
      60)))



;;; 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-search-only-in-messages (&optional arg)
  "Toggle `logview-search-only-in-messages' just for this buffer.
If invoked with prefix argument, enable the option if the
argument is positive, disable it otherwise.

When the option is changed when already performing an incremental
search (with `\\<logview-isearch-map>\\[logview-toggle-search-only-in-messages]'), the change is temporary and lasts only until
the search is ended.  This is for consistency with e.g. `M-s' or
`M-r' during Isearch."
  (interactive (list (or current-prefix-arg 'toggle)))
  (logview--toggle-option-locally 'logview-search-only-in-messages arg (called-interactively-p 'interactive)
                                  "Incremental search will find matches only in messages"
                                  "Incremental search will behave normally"
                                  (lambda ()
                                    (logview--refontify-buffer)
                                    (logview--isearch-update-if-running))))

(defun logview-toggle-filter-preview (&optional arg)
  "Toggle `logview-preview-filter-changes' just for this buffer.
If invoked with prefix argument, enable the option if the
argument is positive, disable it otherwise.

This command may be invoked from filter editing buffer too.  In
this case, it affects its associated log buffer."
  (interactive (list (or current-prefix-arg 'toggle)))
  (with-current-buffer (or (when (eq major-mode 'logview-filter-edit-mode) logview-filter-edit--parent-buffer)
                           (current-buffer))
    (logview--toggle-option-locally 'logview-preview-filter-changes arg (called-interactively-p 'interactive)
                                    "Filtering results will be shown on-the-fly when possible"
                                    "New filters will only take effect when pressing `C-c C-c' or `C-c C-a'")
    (logview--parse-filters)))

(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-choose-submode (submode &optional timestamp)
  "Manually choose submode for the current buffer.
SUBMODE must be a name or an alias a supported submode from
`logview-additional-submodes' or `logview-std-submodes' (aliases
are understood too).  Timestamp may be either such a name or
alias from `logview-additional-timestamp-formats' or
`logview-std-timestamp-formats', or just a raw Java pattern.  If
submode doesn't use timestamps, this parameter is ignored.

When called interactively, both parameters are read in the
minibuffer."
  (interactive (list (let (submodes)
                       (logview--iterate-split-alists (lambda (name definition)
                                                        (push name submodes)
                                                        (setq submodes (append (cdr (assq 'aliases definition)) submodes)))
                                                      logview-additional-submodes logview-std-submodes)
                       (logview--completing-read "Submode name: " submodes nil t nil 'logview--submode-name-history))))
  (let ((submode-definition (logview--get-split-alists submode "submode" logview-additional-submodes logview-std-submodes))
        timestamp-definition)
    (when (string-match-p logview--timestamp-entry-part-regexp (cdr (assq 'format submode-definition)))
      (unless timestamp
        (unless (called-interactively-p 'interactive)
          (error "Must specify a timestamp format for submode `%s'" submode))
        (setq timestamp (let (timestamps)
                          (logview--iterate-split-alists (lambda (name definition)
                                                           (push name timestamps)
                                                           (setq timestamps (append (cdr (assq 'aliases definition)) timestamps)))
                                                         logview-additional-timestamp-formats logview-std-timestamp-formats)
                          (dolist (format (logview--all-timestamp-formats))
                            (unless (datetime-pattern-locale-dependent-p 'java (car format))
                              (push (car format) timestamps)))
                          (logview--completing-read "Timestamp format: " timestamps nil nil nil 'logview--timestamp-format-history))))
      (setq timestamp-definition (or (logview--get-split-alists timestamp nil logview-additional-timestamp-formats logview-std-timestamp-formats)
                                     ;; Unlike with submodes, allow unrecognized timestamps.
                                     `(,timestamp (java-pattern . ,timestamp)))))
    (catch 'success
      (logview--initialize-submode submode submode-definition (list timestamp-definition))
      ;; This must not happen.
      (error "Internal error initializing submode `%s'" submode))))

(defun logview-customize-submode-options ()
  "Customize all options that affect submode selection.
These are:
* `logview-additional-submodes'
* `logview-additional-level-mappings'
* `logview-additional-timestamp-formats'"
  (interactive)
  ;; Existing entry point only customizes single option, we need three
  ;; at once (but this hardly warrants a separate group).
  (custom-buffer-create-other-window '((logview-additional-submodes          custom-variable)
                                       (logview-additional-level-mappings    custom-variable)
                                       (logview-additional-timestamp-formats custom-variable))
                                     "*Customize Logview Submodes*"))

(defun logview--toggle-option-locally (variable arg &optional show-message message-if-true message-if-false callback)
  (let ((new-value (if (eq arg 'toggle)
                       (not (symbol-value variable))
                     (> (prefix-numeric-value arg) 0))))
    (unless (eq new-value (symbol-value variable))
      (set (make-local-variable variable) new-value)
      ;; The purpose of `callback' is to be invoked after updating the variable, but before showing the
      ;; message, as e.g. `isearch--momentary-message' sucks in that it effectively freezes Emacs.
      (when callback
        (funcall callback))
      (when show-message
        (let ((message (if (symbol-value variable) message-if-true message-if-false)))
          (if (and isearch-mode
                   ;; Private function, as always.  At least don't die if they rename it.
                   (fboundp 'isearch--momentary-message))
              (isearch--momentary-message message)
            (message message))))
      t)))



;;; Miscellaneous commands.

(defun logview-pulse-current-entry ()
  (interactive)
  (logview--assert)
  (logview--std-temporarily-widening
    (logview--maybe-pulse-current-entry)
    (when logview--section-header-filter
      (save-excursion
        (logview--forward-section 0)
        (logview--locate-current-entry entry start
          (if (funcall (cdr logview--section-header-filter) entry start)
              (message "In section `%s'" (logview--trim-for-display (logview--entry-message entry start)))
            (message (concat "In the first, unnamed section" (if (logview-sections-thread-bound-p) " of this thread" "")))))))))

(defun logview-mode-help ()
  (interactive)
  ;; Just reinitialize the buffer every time to simplify development.
  (with-current-buffer (get-buffer-create "*Logview cheat sheet*")
    (let ((inhibit-read-only t))
      (with-silent-modifications
        (erase-buffer)
        (let ((keys-width 0))
          (dolist (section logview--cheat-sheet)
            (dolist (entry (cdr section))
              ;; Second argument is a hack to prefer 'l 1' to 'l e' and similar.
              (setq keys-width (max keys-width (length (logview--help-format-keys entry "[1-5]"))))))
          (dolist (section logview--cheat-sheet)
            (unless (bobp)
              (insert "\n"))
            (insert (propertize (car section) 'face (if (display-graphic-p) 'bold 'font-lock-keyword-face)) "\n")
            (dolist (entry (cdr section))
              (if (listp entry)
                  (insert "  " (logview--help-format-keys entry "[1-5]" keys-width)
                          "  " (logview--help-substitute-keys (car (last entry))) "\n")
                (insert "\n  " (replace-regexp-in-string "\n" "\n  " (logview--help-substitute-keys entry)) "\n")))))))
    (goto-char (point-min))
    (help-mode)
    (let ((map (make-sparse-keymap)))
      (set-keymap-parent map help-mode-map)
      (substitute-key-definition #'revert-buffer #'undefined map help-mode-map)
      (use-local-map map)))
  (pop-to-buffer "*Logview cheat sheet*"))

(defun logview--help-substitute-keys (text)
  (replace-regexp-in-string (rx "\\["
                                (group (1+ (any alnum ?-)))
                                (? " <" (group (1+ (not (any ?>)))) ">")
                                "]")
                            (lambda (text)
                              (save-match-data
                                (logview--help-format-keys (list (intern (match-string 1 text))) (match-string 2 text))))
                            text t t))

(defun logview--help-format-keys (entry &optional preferred-keys width)
  (if (listp entry)
      (let (strings)
        (dolist (symbol entry)
          (when (symbolp symbol)
            (let ((best-length most-positive-fixnum)
                  best-matches-preferred-keys
                  keys)
              (dolist (alternative (where-is-internal symbol logview-mode-map))
                (setq alternative (key-description alternative))
                (let ((matches-preferred-keys (when preferred-keys (string-match-p preferred-keys alternative))))
                  (when (or (< (length alternative) best-length)
                            (and (= (length alternative) best-length)
                                 matches-preferred-keys
                                 (not best-matches-preferred-keys)))
                    (setq keys                        alternative
                          best-length                 (length keys)
                          best-matches-preferred-keys matches-preferred-keys))))
              (push (if keys (propertize keys 'face 'font-lock-builtin-face) "") strings))))
        (let ((string (mapconcat #'identity (nreverse strings) " / ")))
          (if width
              (format (format "%%%ds" width) string)
            string)))
    ""))

(defun logview-refresh-buffer-as-needed ()
  "Append log file tail or else revert the whole buffer.
This is conceptually the same as typing `\\<logview-mode-map>\\[logview-append-log-file-tail]', followed by `\\<logview-mode-map>\\[logview-revert-buffer]' if the
first command fails.

This command is faster than reloading the whole buffer in the
common case when the log file grows by appending.  Unlike
`logview-append-log-file-tail', it works in all cases, falling
back to full revert if the file appears to have changed in a
different way.

In all cases the current filters are preserved."
  (interactive)
  (unless (and (not (buffer-modified-p)) (logview--do-append-log-file-tail t))
    (logview-revert-buffer)))

(defun logview-prepare-for-new-contents ()
  "Prepare the log buffer for new contents, ignoring existing one.
This refreshes buffer as needed, widens it, goes to the last
entry and then narrows so that all already existing entries are
hidden.  The result is a buffer that appears empty (only one
entry is shown for context) and will be filled with new contents
once you issue command `\\<logview-mode-map>\\[logview-refresh-buffer-as-needed]'.

This seemingly weird sequence of commands is very useful when you
want to concentrate on log contents between issuing the command
and until `\\<logview-mode-map>\\[logview-refresh-buffer-as-needed]'."
  (interactive)
  (logview-refresh-buffer-as-needed)
  (logview-widen)
  (logview-last-entry t)
  (logview-narrow-from-this-entry))

(defun logview-append-log-file-tail ()
  "Load log file tail into the buffer preserving active filters.
This command won't ask for confirmation, but cannot be used if
the buffer is modified.

Before loading the tail it verifies that preceding contents
matches that of the buffer.  The command does that by comparing
`logview-reassurance-chars' immediately before the tail with the
end of the buffer. This is of course not fool-proof, but for log
files almost always good enough, especially if they contain
timestamps.

This can be seen as an alternative to `auto-revert-tail-mode':
instead of automatic reverting you ask for it explicitly.  It
should be as simple as typing `\\<logview-mode-map>\\[logview-append-log-file-tail]', as no confirmations are asked."
  (interactive)
  (when (buffer-modified-p)
    (user-error "Cannot append file tail to a modified buffer"))
  (logview--do-append-log-file-tail))

(defun logview-revert-buffer ()
  "Revert the buffer preserving active filters.
This command won't ask for confirmation unless the buffer is
modified.

This can be seen as an alternative to `auto-revert-mode': instead
of automatic reverting you ask for it explicitly.  It should be
as simple as typing `\\<logview-mode-map>\\[logview-revert-buffer]', as no confirmations are asked.

If the buffer is narrowed and its contents got changed in the
beginning (see `logview-reassurance-chars'), for example due to
log rotation, this function also widens it.  Narrowing
restrictions most likely wouldn't make any sense with new text."
  (interactive)
  (let* ((narrowed             (buffer-narrowed-p))
         (reassurance-chars    (max logview-reassurance-chars 1))
         (first-characters     (when narrowed
                                 (logview--temporarily-widening
                                  (buffer-substring-no-properties
                                   (point-min)
                                   (min (point-max) reassurance-chars)))))
         (revert-without-query (when buffer-file-name (list (regexp-quote buffer-file-name))))
         (was-read-only        buffer-read-only))
    (revert-buffer nil nil t)
    ;; Apparently 'revert-buffer' resets this.
    (read-only-mode (if was-read-only 1 0))
    (if narrowed
        (let ((same-contents (logview--temporarily-widening
                              (string= (buffer-substring-no-properties
                                        (point-min) (min (point-max) reassurance-chars))
                                       first-characters))))
          (if same-contents
              (message "Reverted the buffer; kept the narrowing as the start contents is the same")
            (logview-widen)
            (message "Reverted the buffer; widened it as narrowing is likely obsolete with new contents")))
      (message "Reverted the buffer"))))

(defun logview--do-append-log-file-tail (&optional no-errors)
  "Perform the work of `logview-append-log-file-tail'.
If NO-ERRORS is non-nil and the file has changed in a non-growing
way, returns nil rather than barking.  In case of success, always
returns non-nil."
  (logview--std-temporarily-widening   ;FIXME: `logview--temporarily-widening'?
    (let* ((buffer             (current-buffer))
           (file               buffer-file-name)
           (reassurance-chars  (min (max logview-reassurance-chars 0)
                                    (buffer-size)))
           (compare-from       (- (point-max) reassurance-chars))
           (current-text       (buffer-substring-no-properties
                                compare-from (point-max)))
           (compare-from-bytes (bufferpos-to-filepos compare-from)))
      (with-temp-buffer
        ;; As of Emacs 30 this fails when trying to read past the end of the
        ;; file (in earlier Emacs versions it works, but doesn't insert
        ;; anything).  Don't care to report anything to Emacs-devel (maybe it's
        ;; even intentional in this case, don't know), just work with either
        ;; behavior by suppressing all errors.
        (ignore-errors (insert-file-contents file nil compare-from-bytes nil))
        (let ((temporary      (current-buffer))
              (temporary-boundary (+ reassurance-chars (point-min)))
              (temporary-size (buffer-size)))
          (if (and (>= temporary-size reassurance-chars)
                   (string= (buffer-substring-no-properties
                             (point-min) temporary-boundary)
                            current-text))
              (if (= temporary-size reassurance-chars)
                  (message "Backing file %s hasn't grown" file)
                (with-current-buffer buffer
                  (let ((was-modified      (buffer-modified-p))
                        (inhibit-read-only t)
                        ;; This is to avoid unnecessary confirmation about
                        ;; modifying a buffer with externally changed file.
                        (buffer-file-name  nil))
                    (save-excursion
                      (goto-char (point-max))
                      ;; FIXME: Why `-no-properties'?
                      (insert-buffer-substring-no-properties
                       temporary temporary-boundary nil))
                    (restore-buffer-modified-p was-modified))
                  (message "Appended the tail of file %s" file)))
            (unless no-errors
              (user-error "Buffer contents doesn't match the head of %s anymore" file))))))))



;;; Internal functions (except helpers for specific command groups).

(defmacro logview--internal-log (format-string &rest arguments)
  `(let ((inhibit-message t))
     (message ,format-string ,@arguments)))

(defun logview--guess-submode ()
  (save-excursion
    ;; Need access to the buffer start, regardless of any narrowing.  Also, don't want
    ;; original narrowing to have any effect on anything (see uses of corresponding
    ;; functions that access it; not even sure they are even called from here, but that
    ;; doesn't matter), that's why not `std'.
    (logview--temporarily-widening
      (let ((line-number       0)
            (remaining-attemps (if (and (integerp logview-max-promising-lines) (> logview-max-promising-lines 0))
                                   logview-max-promising-lines
                                 most-positive-fixnum))
            standard-timestamps)
        (logview--iterate-split-alists (lambda (_timestamp-name timestamp) (push timestamp standard-timestamps))
                                       logview-additional-timestamp-formats logview-std-timestamp-formats)
        (dolist (format (logview--all-timestamp-formats))
          (push (cdr format) standard-timestamps))
        (setq standard-timestamps (nreverse standard-timestamps))
        (catch 'success
          (goto-char (point-min))
          (while (and (< line-number (max logview-guess-lines 1)) (> remaining-attemps 0) (not (eobp)))
            (let ((line (buffer-substring-no-properties (point) (progn (end-of-line) (point)))))
              (let (promising)
                (when (> (length line) 0)
                  (logview--iterate-split-alists (lambda (name definition)
                                                   (condition-case error
                                                       (when (logview--initialize-submode name definition standard-timestamps line)
                                                         (setf promising t))
                                                     (error (warn "%s" (error-message-string error)))))
                                                 logview-additional-submodes logview-std-submodes))
                (when promising
                  (setf remaining-attemps (1- remaining-attemps))))
              (forward-line 1)
              (setq line-number (1+ line-number))))))
      ;; This is done regardless of whether guessing has succeeded or not.
      (setf logview--custom-submode-guessed-with logview--custom-submode-revision))))

;; Returns non-nil if TEST-LINE is "promising".
(defun logview--initialize-submode (name definition standard-timestamps &optional test-line)
  (let* ((format            (cdr (assq 'format    definition)))
         (timestamp-names   (when test-line (cdr (assq 'timestamp definition))))
         (timestamp-options (if timestamp-names
                                (mapcar (lambda (name)
                                          (logview--get-split-alists name "timestamp format"
                                                                     logview-additional-timestamp-formats logview-std-timestamp-formats))
                                        ;; Don't be too strict to the definition.  Many if not most users
                                        ;; don't go through customization interface to create it.
                                        (if (listp timestamp-names) timestamp-names (list timestamp-names)))
                              standard-timestamps))
         (search-from       0)
         (parts             (list "^"))
         next
         end
         starter terminator
         levels
         timestamp-at
         cannot-match
         features
         have-explicit-message
         (add-text-part (lambda (from to)
                          (push (replace-regexp-in-string "[ \t]+" "[ \t]+" (regexp-quote (substring format from to))) parts))))
    (unless (and (stringp format) (> (length format) 0))
      (user-error "Invalid submode '%s': no format string" name))
    (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)
            starter    (when (> next 0)
                         (aref format (1- next)))
            terminator (when (< end (length format))
                         (aref format end)))
      (cond ((match-beginning logview--timestamp-group)
             (push nil parts)
             (push 'timestamp features)
             (setq timestamp-at parts))
            ((match-beginning logview--level-group)
             (setq levels (logview--get-split-alists (cdr (assq 'levels definition)) "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))
            ((match-beginning logview--message-group)
             (unless (= (match-end logview--message-group) (length format))
               (user-error "Field `MESSAGE' can only be placed at the very end of format string"))
             (setq have-explicit-message t))
            (t
             (dolist (k (list logview--name-group logview--thread-group logview--ignored-group))
               ;; See definition of `logview--entry-part-regexp' for the meaning of 4 and 10.
               (let ((special-regexp (match-beginning (+ k 4))))
                 (when (or (match-beginning k) special-regexp)
                   (push (format "\\(?%s:%s\\)"
                                 (if (/= k logview--ignored-group)
                                     (number-to-string k)
                                   "")
                                 (cond (special-regexp
                                        (let ((forced-regexp (match-string 10 format)))
                                          (unless (logview--valid-regexp-p forced-regexp)
                                            ;; Ideally would also ensure that there are no catching groups,
                                            ;; but for this we'd need `xr' as dependency.  Not now.
                                            (warn "In format specifier `%s': `%s' is not a valid regexp" format forced-regexp)
                                            (setf cannot-match t))
                                          forced-regexp))
                                       ((and starter terminator
                                             (or (and (= starter ?\() (= terminator ?\)))
                                                 (and (= starter ?\[) (= terminator ?\]))))
                                        ;; See https://github.com/doublep/logview/issues/2 We allow _one_
                                        ;; level of nested parens inside parenthesized THREAD or NAME.
                                        ;; Allowing more would complicate regexp even further.  Unlimited
                                        ;; nesting level is not possible with regexps at all.
                                        ;;
                                        ;; 'rx-to-string' is used to avoid escaping things ourselves.
                                        (rx-to-string `(seq (* (not (any ,starter ,terminator ?\n)))
                                                            (* ,starter (* (not (any ?\n))) ,terminator
                                                               (* (not (any ,starter ,terminator ?\n)))))
                                                      t))
                                       ((and terminator (/= terminator ? ))
                                        (format "[^%c\n]*" terminator))
                                       (terminator
                                        "[^ \t\n]+")
                                       (t
                                        ".+")))
                              parts)
                        (push (if (= k logview--name-group) 'name 'thread) features))))))
      (setq search-from end))
    (unless cannot-match
      (when (< search-from (length format))
        (funcall add-text-part search-from nil))
      ;; Unless `MESSAGE' field is used explicitly, behave as if format string ends with whitespace.
      (unless (or have-explicit-message (string-match-p "[ \t]$" format))
        (push "\\(?:[ \t]+\\|$\\)" parts))
      (setq parts (nreverse parts))
      (when timestamp-at
        ;; Speed optimization: if the submode includes a timestamp, but the test line doesn't have even two
        ;; digits at the expected place, don't even loop through all the timestamp options.
        (setcar timestamp-at ".*[0-9][0-9].*")
        (when (and test-line (not (string-match-p (apply #'concat parts) test-line)))
          (setq cannot-match t))))
    (unless cannot-match
      (dolist (timestamp-option (if timestamp-at timestamp-options '(nil)))
        (let* ((timestamp-pattern (assq 'java-pattern timestamp-option))
               (timestamp-locale  (cdr (assq 'locale timestamp-option)))
               (timestamp-regexp  (if timestamp-pattern
                                      (condition-case error
                                          (apply #'datetime-matching-regexp 'java (cdr timestamp-pattern)
                                                 :locale timestamp-locale
                                                 (append (cdr (assq 'datetime-options timestamp-option)) logview--datetime-matching-options))
                                        ;; 'datetime' doesn't mention the erroneous pattern to keep
                                        ;; the error message concise.  Let's do it ourselves.
                                        (error (warn "In Java timestamp pattern '%s': %s"
                                                     (cdr timestamp-pattern) (error-message-string error))
                                               nil))
                                    (cdr (assq 'regexp timestamp-option)))))
          (when (or timestamp-regexp (null timestamp-at))
            (when timestamp-at
              (setcar timestamp-at (format "\\(?%d:%s\\)" logview--timestamp-group timestamp-regexp)))
            (let ((regexp      (apply #'concat parts))
                  (level-index 0))
              (when (or (null test-line) (string-match-p regexp test-line))
                (setq logview--submode-name           name
                      logview--process-buffer-changes t
                      logview--entry-regexp           regexp
                      logview--submode-features       features
                      logview--submode-level-data     nil)
                (logview--update-mode-name)
                (when (memq 'level features)
                  (dolist (final-level logview--final-levels)
                    (dolist (level (cdr (assoc final-level levels)))
                      (push (cons level (cons level-index (cons (intern (format "logview-%s-entry" (symbol-name final-level)))
                                                                (intern (format "logview-level-%s" (symbol-name final-level))))))
                            logview--submode-level-data)
                      (setq level-index (1+ level-index)))))
                (setq logview--submode-level-faces (make-vector level-index nil))
                (dolist (level-data logview--submode-level-data)
                  (aset logview--submode-level-faces (cadr level-data) (cddr level-data)))
                (when (memq 'timestamp features)
                  (let ((num-fractionals (apply #'datetime-pattern-num-second-fractionals 'java (cdr timestamp-pattern) logview--datetime-parsing-options)))
                    (setf logview--timestamp-difference-format-string (format "%%+.%df" num-fractionals)
                          logview--timestamp-gap-format-string        (format "%%.%df" num-fractionals)))
                  ;; Largely for catching errors in `datetime's determination of system timezone.
                  (setf logview--submode-timestamp-parser
                        (condition-case error
                            (apply #'datetime-parser-to-float 'java (cdr timestamp-pattern) :locale timestamp-locale :timezone 'system
                                   logview--datetime-parsing-options)
                          (error (warn "%s" (error-message-string error))
                                 (let ((utc-parser (ignore-errors (apply #'datetime-parser-to-float 'java (cdr timestamp-pattern) :locale timestamp-locale
                                                                         logview--datetime-parsing-options))))
                                   (if utc-parser
                                       (progn (warn "Using UTC for the log file instead, in hopes it will be good enough")
                                              utc-parser)
                                     ;; Only to avoid errors later.  Results will be incorrect, of course, but
                                     ;; at least the mode everything other than some timestamp-related
                                     ;; commands will work.  The cause is reported as a warning above.
                                     (lambda (_) 0.0)))))))
                (read-only-mode 1)
                (when buffer-file-name
                  (pcase logview-auto-revert-mode
                    (`auto-revert-mode      (auto-revert-mode      1))
                    (`auto-revert-tail-mode (auto-revert-tail-mode 1))))
                (logview--refilter)
                (throw 'success nil))))))
      ;; "Promising" line.
      t)))

(defun logview--all-timestamp-formats ()
  (unless logview--all-timestamp-formats-cache
    ;; Since there are now really lots of locales known by `datetime', cache this value
    ;; not only in memory, but also on disk.  We use `extmap' to create and read the cache
    ;; file.  If `datetime' reports a different locale database version, cache is
    ;; discarded.
    (let ((cache-file              (ignore-errors (extmap-init logview-cache-filename)))
          (locale-database-version (datetime-locale-database-version)))
      (when cache-file
        (let ((cached-externally (extmap-get cache-file 'timestamp-formats t)))
          (when (and cached-externally (equal (extmap-get cache-file 'locale-database-version t) locale-database-version))
            (setq logview--all-timestamp-formats-cache (extmap-get cache-file 'timestamp-formats t)))))
      (if logview--all-timestamp-formats-cache
          (logview--internal-log "Logview: loaded locale timestamp formats from `%s'" logview-cache-filename)
        (let ((start-time (float-time))
              (patterns (make-hash-table :test 'equal :size 1000))
              (uniques  (make-hash-table :test 'equal :size 1000)))
          (dolist (locale (datetime-list-locales t))
            (let ((decimal-separator (char-to-string (datetime-locale-field locale :decimal-separator)))
                  last-time-pattern)
              (dolist (time-variant '(:short :medium :long :full))
                (let ((time-pattern (datetime-locale-time-pattern locale time-variant)))
                  (unless (string= time-pattern last-time-pattern)
                    (setq last-time-pattern time-pattern)
                    (when (and (datetime-pattern-includes-second-p 'java time-pattern)
                               (not (datetime-pattern-includes-timezone-p 'java time-pattern)))
                      (let (variants)
                        (dolist (pattern (cons time-pattern
                                               (mapcar (lambda (date-variant) (datetime-locale-date-time-pattern locale date-variant time-variant))
                                                       '(:short :medium :long :full))))
                          (let ((subvariants (list pattern)))
                            ;; Java 17 (used in `datetime' 0.8+) added commas in a lot of places.  We create
                            ;; variants also without the commas, if only to match older logs.  There are also
                            ;; other changes (e.g. separator "à" is gone in some patterns in French locale),
                            ;; but those we can't handle transparently and generically, oh well.
                            (when (string-match "\\(\\w\\), \\(\\w\\)" pattern)
                              (let ((left  (intern (match-string 1 pattern)))
                                    (right (intern (match-string 2 pattern))))
                                (when (cond ((memq left  '(y d E c))
                                             (memq right '(H h a)))    ; Between date and time.
                                            ((memq left  '(s S))
                                             (memq right '(y d E c)))) ; Between time and date.
                                  (push (replace-match "\\1 \\2" t nil pattern) subvariants))))
                            (dolist (pattern subvariants)
                              (push pattern                                                                                   variants)
                              (push (replace-regexp-in-string "\\<s+\\>" (concat "\\&" decimal-separator "SSS")    pattern t) variants)
                              (push (replace-regexp-in-string "\\<s+\\>" (concat "\\&" decimal-separator "SSSSSS") pattern t) variants))))
                        (dolist (pattern variants)
                          (let* ((parts            (datetime-recode-pattern 'java 'parsed pattern))
                                 (locale-dependent (datetime-pattern-locale-dependent-p 'parsed parts))
                                 (key              (cons pattern (when locale-dependent locale))))
                            (when (or locale-dependent (null (gethash key patterns)))
                              (puthash key
                                       (apply #'datetime-matching-regexp 'parsed parts :locale locale logview--datetime-matching-options)
                                       patterns)))))))))))
          (maphash (lambda (key regexp)
                     (let ((existing (gethash regexp uniques)))
                       (if existing
                           (unless (memq (cdr key) (cdr existing))
                             (push (cdr key) (cdr existing)))
                         (puthash regexp (cons (car key) (list (cdr key))) uniques))))
                   patterns)
          (maphash (lambda (regexp key)
                     (push `(,(car key) (regexp . ,regexp)) logview--all-timestamp-formats-cache))
                   uniques)
          (logview--internal-log "Logview/datetime: built list of %d timestamp regexps in %.3f s" (hash-table-count uniques) (- (float-time) start-time))
          (ignore-errors
            (extmap-from-alist logview-cache-filename `((locale-database-version . ,locale-database-version)
                                                        (timestamp-formats       . ,logview--all-timestamp-formats-cache))
                               :overwrite t))))))
  logview--all-timestamp-formats-cache)

;; Schedule submode reguessing in all Logview buffers that have no submode.  There is some
;; black magic involved to do it one buffer a time and only when Emacs is idle (to avoid
;; making it appear hung) and also handle visible buffers first.
(defun logview--maybe-guess-submodes-again ()
  (let ((state (list logview-additional-submodes logview-additional-level-mappings logview-additional-timestamp-formats)))
    (unless (equal state logview--custom-submode-state)
      (setf logview--custom-submode-state    state
            logview--custom-submode-revision (1+ logview--custom-submode-revision)
            logview--need-submode-guessing   (make-hash-table :test #'eq))
      (dolist (buffer (buffer-list))
        (when (logview--needs-reguessing-p buffer)
          (puthash buffer t logview--need-submode-guessing)))
      (logview--reschedule-submode-guessing))))

(defun logview--needs-reguessing-p (buffer)
  (when (buffer-live-p buffer)
    (with-current-buffer buffer
      (and (eq major-mode 'logview-mode)
           (not (logview-initialized-p))
           (< logview--custom-submode-guessed-with logview--custom-submode-revision)))))

(defun logview--reschedule-submode-guessing ()
  (when logview--submode-guessing-timer
    (cancel-timer logview--submode-guessing-timer)
    (setf logview--submode-guessing-timer nil))
  (when logview--need-submode-guessing
    (if (> (hash-table-count logview--need-submode-guessing) 0)
        (setf logview--submode-guessing-timer (if (current-idle-time)
                                                  (run-with-timer 0.2 nil
                                                                  (lambda ()
                                                                    (if (current-idle-time)
                                                                        (logview--guess-submode-again)
                                                                      (logview--reschedule-submode-guessing))))
                                                (run-with-idle-timer 1 nil #'logview--guess-submode-again)))
      (setf logview--need-submode-guessing nil))))

(defun logview--guess-submode-again ()
  (let* (obsolete-buffers
         (reguessed-in (catch 'processed-buffer
                         (logview--try-to-guess-submode-again (window-buffer (selected-window)))
                         (let ((current-frame (selected-frame)))
                           (dolist (frame (cons current-frame (delq current-frame (frame-list))))
                             (dolist (window (window-list frame))
                               (logview--try-to-guess-submode-again (window-buffer window)))))
                         (maphash (lambda (buffer _)
                                    (logview--try-to-guess-submode-again buffer)
                                    (push buffer obsolete-buffers))
                                  logview--need-submode-guessing))))
    (remhash reguessed-in logview--need-submode-guessing)
    (dolist (obsolete obsolete-buffers)
      (remhash obsolete logview--need-submode-guessing))
    (logview--reschedule-submode-guessing)))

(defun logview--try-to-guess-submode-again (buffer)
  (when (logview--needs-reguessing-p buffer)
    (with-current-buffer buffer
      (logview--guess-submode)
      (throw 'processed-buffer buffer))))


(defun logview--assert (&rest assertions)
  (unless (logview-initialized-p)
    (user-error "Couldn't determine log format; press C-c C-s to customize relevant options"))
  (dolist (assertion assertions)
    (unless (or (eq assertion 'message) (memq assertion logview--submode-features))
      (user-error (cdr (assq assertion '((level     . "Log lacks entry levels")
                                         (name      . "Log lacks logger names")
                                         (thread    . "Log doesn't include thread names")
                                         (timestamp . "Log entries lack timestamps"))))))))


(defun logview--forward-entry (n &optional validator)
  (logview--locate-current-entry last-valid-entry last-valid-start
    (cond ((> n 0)
           (logview--iterate-entries-forward (point)
                                             (lambda (entry start)
                                               (> (setq last-valid-entry entry
                                                        last-valid-start start
                                                        n                (1- n))
                                                  0))
                                             t validator t))
          ((< n 0)
           (logview--iterate-entries-backward (point)
                                              (lambda (entry start)
                                                (< (setq last-valid-entry entry
                                                         last-valid-start start
                                                         n                (1+ n))
                                                   0))
                                              t validator t)))
    (goto-char (logview--entry-message-start last-valid-entry last-valid-start)))
  n)

(defun logview--maybe-complain-about-movement (direction remaining &optional type)
  ;; Using 'equal' since 'remaining' may also be nil.
  (unless (equal remaining 0)
    (user-error (pcase type
                  (`nil          (if (> direction 0)         "No next (visible) entry"              "No previous (visible) entry"))
                  (`as-important (if (> direction 0)         "No next (visible) as important entry" "No previous (visible) as important entry"))
                  (`section      (concat (if (> direction 0) "No next (visible) section"            "No previous (visible) section")
                                         (if (logview-sections-thread-bound-p) " in this thread" "")))
                  (_             (if (> direction 0) "No next (visible) entry in view `%s'" "No previous (visible) entry in view `%s'")))
                type)))


(defun logview--do-locate-current-entry (&optional position)
  "Return the entry around POSITION and its beginning.
If POSITION is nil, take the current value of point as the
position, and also signal a user-level error if no entries can be
located.

This function may misbehave if any narrowing is in effect.  Make
sure to cancel it using `logview--std-temporarily-widening' or
something similar first."
  (let* ((entry-at (or position (point)))
         (entry    (or (get-text-property entry-at 'logview-entry)
                       (progn (logview--find-region-entries entry-at (+ entry-at logview--lazy-region-size))
                              (get-text-property entry-at 'logview-entry)))))
    (if entry
        (when (and (> entry-at (point-min))
                   (eq (get-text-property (1- entry-at) 'logview-entry) entry))
          (setq entry-at (or (previous-single-property-change entry-at 'logview-entry) (point-min))))
      (when (setq entry-at (or (next-single-property-change entry-at 'logview-entry)
                               (when (and (> entry-at (point-min))
                                          (get-text-property (1- entry-at) 'logview-entry))
                                 (previous-single-property-change entry-at 'logview-entry))))
        (setq entry (get-text-property entry-at 'logview-entry))))
    (if entry
        (cons entry entry-at)
      (unless position
        (user-error "Unable to locate any log entries")))))

(defun logview--iterate-entries-forward (position callback &optional only-visible validator skip-current)
  "Invoke CALLBACK for successive valid log entries forward.

Iteration starts at the entry around POSITION (or the next, if
SKIP-CURRENT is non-nil) and continues forward until CALLBACK
returns nil or end of buffer is reached.  This function does not
alter the point, nor is it affected in any way by CALLBACK or
VALIDATOR altering it.

CALLBACK is called with two arguments: value of the
`logview-entry' property and the beginning of the 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.  VALIDATOR is called with the same parameters as
CALLBACK."
  (let ((entry+start (logview--do-locate-current-entry position)))
    (when entry+start
      (let ((entry    (car entry+start))
            (entry-at (cdr entry+start))
            (limit    (if only-visible (logview--point-max) (point-max))))
        (unless (and skip-current (>= (setq entry-at (logview--entry-end entry entry-at)) limit))
          (while (progn (setq entry (or (get-text-property entry-at 'logview-entry)
                                        (progn (logview--find-region-entries entry-at (+ entry-at logview--lazy-region-size))
                                               (get-text-property entry-at 'logview-entry))))
                        (and (or (and only-visible (invisible-p entry-at))
                                 (and validator (not (funcall validator entry entry-at)))
                                 (funcall callback entry entry-at))
                             (< (setq entry-at (logview--entry-end entry entry-at)) limit)))))))))

(defun logview--iterate-entries-backward (position callback &optional only-visible validator skip-current)
  "Invoke CALLBACK for successive valid log entries backward.
See `logview--iterate-entries-forward' for details."
  (let ((entry+start (logview--do-locate-current-entry position)))
    (when entry+start
      (let ((entry    (car entry+start))
            (entry-at (cdr entry+start))
            (limit    (if only-visible (logview--point-min) (point-min))))
        (unless (and skip-current (< (setq entry-at (1- entry-at)) limit))
          (while (and (setq entry (or (get-text-property entry-at 'logview-entry)
                                      (progn (logview--find-region-entries (max (point-min) (- entry-at logview--lazy-region-size)) (1+ entry-at) t)
                                             (get-text-property entry-at 'logview-entry))))
                      (progn (unless (or (= entry-at (point-min))
                                         (not (eq (get-text-property (1- entry-at) 'logview-entry) entry)))
                               (setq entry-at (or (previous-single-property-change entry-at 'logview-entry)
                                                  (point-min))))
                             (when (or (and only-visible (invisible-p entry-at))
                                       (and validator (not (funcall validator entry entry-at)))
                                       (funcall callback entry entry-at))
                               (>= (setq entry-at (1- entry-at)) limit))))))))))

(defun logview--iterate-successive-entries (position 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)
               position
               (lambda (entry entry-at)
                 (funcall callback entry entry-at)
                 (/= (setq n (- n direction)) 0))
               only-visible validator)))
  n)

(defun logview--iterate-entries-in-region (begin end callback &optional only-visible validator)
  (let ((limit (max begin end)))
    (logview--iterate-entries-forward (min begin end)
                                      (lambda (entry entry-at)
                                        (funcall callback entry entry-at)
                                        (< (logview--entry-end entry entry-at) limit))
                                      only-visible validator)))


(defun logview--refilter ()
  (logview--retire-hiding-symbol 'logview--filtered-symbol)
  (logview--update-invisibility-spec)
  (logview--refontify-buffer))

(defun logview--refontify-buffer ()
  (logview--std-temporarily-widening
    (font-lock-flush)))


(defun logview--trim-for-display (message &optional max-length)
  (unless max-length
    (setf max-length 100))
  (setf message (replace-regexp-in-string (rx (1+ whitespace)) " " (string-trim message)))
  (if (<= (length message) max-length)
      message
    (with-temp-buffer
      (insert message)
      (goto-char (point-min))
      (let ((cut-after (point)))
        (while (and (search-forward-regexp (rx word eow) nil t)
                    (when (<= (1- (point)) (- max-length 3))
                      (setf cut-after (point)))))
        ;; If we'd trimming too much because of too long word, better cut it in the middle.
        (when (<= cut-after (/ max-length 2))
          (setf cut-after (- max-length 3)))
        (concat (buffer-substring (point-min) cut-after) "...")))))


(defun logview--maybe-pulse-current-entry (&optional why)
  (when (or (null why) (memq why logview-pulse-entries))
    (eval-and-compile (require 'pulse))
    (save-excursion
      (logview--locate-current-entry entry start
        (pulse-momentary-highlight-region start (logview--entry-end entry start) 'logview-pulse)))))


(defun logview--update-mode-name ()
  (let ((view (logview--current-view)))
    (setf mode-name `("Logview"
                      "/"
                      ;; FIXME: Add mouse keymaps where appropriate.  Not terribly important.
                      (:propertize ,logview--submode-name help-echo "Log submode\nSee `logview-std-submodes' and `logview-additional-submodes'")
                      ,@(when view
                          `("[" (:propertize ,(plist-get view :name) help-echo "Current view\nType `v' to change") "]"
                            ,@(when (plist-get view :index)
                                `(":" (:propertize (number-to-string (plist-get view :index)) help-echo "View index\nType `M-1'...`M-0' to switch between views with index")))))
                      (logview--narrow-to-section-headers
                       `("!"
                         (:propertize "OSH" face warning help-echo "Showing only section headers for easier long-distance navigation\nType `c h' to return to normal mode")))))
    ;; Sometimes it doesn't work automatically (e.g. when using `C-c C-a' in a
    ;; view-editing buffer and thus indirectly changing a different buffer's modeline).
    ;; Just force update at all times, not trying to figure out when it works by itself.
    (force-mode-line-update)))

(defun logview--current-view ()
  (catch 'found
    (dolist (view (logview--views))
      (when (and (or (null (plist-get view :submode)) (string= (plist-get view :submode) logview--submode-name))
                 (string= (plist-get view :filters) logview--main-filter-text))
        (throw 'found view)))
    (let ((canonical-filter-text (logview--canonical-filter-text logview--main-filter-text)))
      (dolist (view (logview--views))
        (when (and (or (null (plist-get view :submode)) (string= (plist-get view :submode) logview--submode-name))
                   (string= (logview--canonical-filter-text (plist-get view :filters)) canonical-filter-text))
          (throw 'found view))))))

(defun logview--update-invisibility-spec ()
  (let ((invisibility-spec (list logview--hidden-details-symbol logview--hidden-entry-symbol logview--filtered-symbol)))
    (when logview--hide-all-details
      (push 'logview-details invisibility-spec))
    (when logview-show-ellipses
      (setq invisibility-spec (mapcar (lambda (x) (cons x t)) invisibility-spec)))
    ;; Try to work nicely with other packages, e.g. minor modes.
    (when (consp buffer-invisibility-spec)
      (let ((case-fold-search nil))
        (dolist (element buffer-invisibility-spec)
          (unless (string-match-p "^logview-" (symbol-name (cond ((symbolp element)            element)
                                                                 ((symbolp (car-safe element)) (car-safe element)))))
            (push element invisibility-spec)))))
    (setq buffer-invisibility-spec (nreverse 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--retire-hiding-symbol (symbol-var)
  (set symbol-var (intern (replace-regexp-in-string "[0-9]+$" (lambda (generation) (number-to-string (1+ (string-to-number generation))))
                                                    (symbol-name (symbol-value symbol-var)) t t))))


;; Return non-nil if filters have changed.
(defun logview--parse-filters (&optional to-reset)
  (let ((main-filters             logview--main-filter-text)
        (thread-narrowing-filters logview--thread-narrowing-filter-text)
        (preview-filters          (when logview-preview-filter-changes logview--preview-filter-text)))
    (when preview-filters
      (setf (if (car preview-filters) main-filters thread-narrowing-filters) (cdr preview-filters)))
    (let ((filters (logview--do-parse-filters main-filters
                                              thread-narrowing-filters
                                              (when logview--narrow-to-section-headers (cdar logview--section-header-filter))
                                              to-reset)))
      (unless (prog1 (equal (cdar logview--effective-filter) (cdar filters))
                (setf logview--effective-filter filters)
                (unless (and preview-filters (car preview-filters))
                  (setf logview--main-filter-text (or (caar (car filters)) "")))
                (unless (and preview-filters (not (car preview-filters)))
                  (setf logview--thread-narrowing-filter-text (or (cdar (car filters)) ""))))
        (logview--refilter)
        (logview--update-mode-name)
        t))))

(defun logview--do-parse-filters (filters &optional thread-narrowing-filters header-filter-key to-reset-in-main-filters)
  "Parse given FILTERS and optional THREAD-NARROWING-FILTERS.
TO-RESET-IN-MAIN-FILTERS may be a list of strings like \"a+\" of
filter types to discard.

Returns

    (((MAIN-FILTER-TEXT . THREAD-NARROWING-FILTER-TEXT) . KEY)
     . VALIDATOR-FN)

or nil if there are no filters."
  (let (non-discarded-lines-main
        non-discarded-lines-narrowing
        min-shown-level
        min-always-shown-level
        include-name-regexps
        exclude-name-regexps
        include-thread-regexps
        exclude-thread-regexps
        include-thread-regexps-narrowing
        exclude-thread-regexps-narrowing
        include-message-regexps
        exclude-message-regexps)
    (dolist (main '(t nil))
      (let ((pass-filters (if main filters thread-narrowing-filters)))
        (when (> (length pass-filters) 0)
          (logview--iterate-filter-text-lines
           pass-filters
           (lambda (type line-begin begin end)
             (let ((filter-line       (not (member type '("#" "" nil))))
                   (reset-this-filter (and main (member type to-reset-in-main-filters))))
               (when reset-this-filter
                 (delete-region begin (point)))
               (when (and (not (and filter-line reset-this-filter)) (or (if main non-discarded-lines-main non-discarded-lines-narrowing) (not (equal type ""))))
                 (push (buffer-substring-no-properties line-begin (point)) (if main non-discarded-lines-main non-discarded-lines-narrowing)))
               (when (and filter-line (not reset-this-filter))
                 (cond ((string= type "lv")
                        (setq min-shown-level (buffer-substring-no-properties begin end)))
                       ((string= type "LV")
                        (setq min-always-shown-level (buffer-substring-no-properties begin end)))
                       (t
                        (let ((regexp (logview--filter-regexp begin end)))
                          (when (logview--valid-regexp-p regexp)
                            (if main
                                (pcase type
                                  ("a+" (push regexp include-name-regexps))
                                  ("a-" (push regexp exclude-name-regexps))
                                  ("t+" (push regexp include-thread-regexps))
                                  ("t-" (push regexp exclude-thread-regexps))
                                  ("m+" (push regexp include-message-regexps))
                                  ("m-" (push regexp exclude-message-regexps)))
                              (pcase type
                                ("t+" (push regexp include-thread-regexps-narrowing))
                                ("t-" (push regexp exclude-thread-regexps-narrowing)))))))))
               t))))))
    (setq min-shown-level                  (unless (equal min-shown-level (caar logview--submode-level-data))
                                             (cadr (assoc min-shown-level logview--submode-level-data)))
          min-always-shown-level           (cadr (assoc min-always-shown-level logview--submode-level-data))
          include-name-regexps             (logview--standardize-regexp-options include-name-regexps)
          exclude-name-regexps             (logview--standardize-regexp-options exclude-name-regexps)
          include-thread-regexps           (logview--standardize-regexp-options include-thread-regexps)
          exclude-thread-regexps           (logview--standardize-regexp-options exclude-thread-regexps)
          include-thread-regexps-narrowing (logview--standardize-regexp-options include-thread-regexps-narrowing)
          exclude-thread-regexps-narrowing (logview--standardize-regexp-options exclude-thread-regexps-narrowing)
          include-message-regexps          (logview--standardize-regexp-options include-message-regexps)
          exclude-message-regexps          (logview--standardize-regexp-options exclude-message-regexps))
    ;; Deliberately not checking `min-always-shown-level': it has no effect without other
    ;; filters.
    (when (or min-shown-level
              include-name-regexps exclude-name-regexps
              include-thread-regexps exclude-thread-regexps include-thread-regexps-narrowing exclude-thread-regexps-narrowing
              include-message-regexps exclude-message-regexps
              header-filter-key)
      (cons (list (cons (when non-discarded-lines-main      (apply #'concat (nreverse non-discarded-lines-main)))
                        (when non-discarded-lines-narrowing (apply #'concat (nreverse non-discarded-lines-narrowing))))
                  min-shown-level min-always-shown-level include-name-regexps exclude-name-regexps
                  include-thread-regexps exclude-thread-regexps include-thread-regexps-narrowing exclude-thread-regexps-narrowing
                  include-message-regexps exclude-message-regexps
                  header-filter-key)
            (let ((level-form (if (and min-shown-level min-always-shown-level) 'level '(logview--entry-level entry)))
                  clauses)
              (when min-shown-level
                (push `(<= ,level-form ,min-shown-level) clauses))
              ;; FIXME: Try to optimize for speed better.  Currently order is fixed like
              ;;        this: thread narrowing, name filters, normal thread filters,
              ;;        message filters.
              (push (logview--build-validator-regexp-clause include-thread-regexps-narrowing exclude-thread-regexps-narrowing logview--thread-group)  clauses)
              (push (logview--build-validator-regexp-clause include-name-regexps             exclude-name-regexps             logview--name-group)    clauses)
              (push (logview--build-validator-regexp-clause include-thread-regexps           exclude-thread-regexps           logview--thread-group)  clauses)
              (push (logview--build-validator-regexp-clause include-message-regexps          exclude-message-regexps          logview--message-group) clauses)
              (when (setf clauses (delq nil clauses))
                (let ((validator (if (cdr clauses) `(and ,@(nreverse clauses)) (car clauses))))
                  (when min-always-shown-level
                    (setq validator `(or (<= ,level-form ,min-always-shown-level) ,validator)))
                  (when (eq level-form 'level)
                    (setq validator `(let ((level (logview--entry-level entry))) ,validator)))
                  ;; Here `eval' is used to translate the lambda into a closure.
                  (byte-compile (eval `(lambda (entry start) (ignore start) ,validator) t)))))))))

;; To prevent refiltering on insignificant changes, we enforce canonical option ordering
;; and drop any duplicates.
(defun logview--standardize-regexp-options (options)
  (delete-consecutive-dups (sort options #'string<)))

(defun logview--build-validator-regexp-clause (include-regexps exclude-regexps entry-group)
  (when (or include-regexps exclude-regexps)
    (let* ((string-fetcher (if (= entry-group logview--message-group)
                               `(logview--entry-message entry start)
                             `(logview--entry-group entry start ,entry-group)))
           (string-form    (if (and include-regexps exclude-regexps) 'string string-fetcher))
           subclauses)
      (when include-regexps
        (push `(string-match-p ,(logview--build-filter-regexp include-regexps) ,string-form) subclauses))
      (when exclude-regexps
        (push `(not (string-match-p ,(logview--build-filter-regexp exclude-regexps) ,string-form)) subclauses))
      (let ((clause (if (cdr subclauses) `(and ,@(nreverse subclauses)) (car subclauses))))
        (when (eq string-form 'string)
          (setq clause `(let ((string ,string-fetcher)) ,clause)))
        clause))))

;; 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)
  (mapconcat #'identity options "\\|"))

(defun logview--iterate-filter-text-lines (filters callback)
  (with-temp-buffer
    (insert filters)
    (unless (bolp)
      (insert "\n"))
    (goto-char (point-min))
    (logview--iterate-filter-buffer-lines callback)))

(defun logview--iterate-filter-buffer-lines (callback)
  "Find successive filter specification in the current buffer.
Buffer must be positioned at the start of a line.  Iteration
continues until CALLBACK returns nil or end of buffer is reached.

CALLBACK is called with four arguments: TYPE, LINE-BEGIN, BEGIN,
and END.  TYPE may be a string: \"a+\", \"a-\", \"t+\", \"t-\", \"m+\",
\"m-\", \"lv\" or \"LV\" for valid filter types, \"#\" for comment line
and \"\" for an empty line, or nil to indicate an erroneous line.
BEGIN and END determine filter text boundaries (may span several
lines for message filters.  LINE-BEGIN is the beginnig of the
line where the entry starts; in case of filters this is a few
characters before BEGIN.  Point is positioned at the start of
next line, which is usually one line beyond END."
  (let ((case-fold-search nil)
        line-begin
        begin
        type)
    (while (and (not (eobp))
                (progn
                  (setq line-begin (point)
                        begin      line-begin
                        type       (when (looking-at "\\(lv\\|LV\\|[atm][-+]\\) \\|\\s-*\\(#\\)\\|\\s-*$")
                                     (if (match-beginning 1)
                                         (progn (setq begin (match-end 0))
                                                (match-string 1))
                                       (or (match-string 2) ""))))
                  (forward-line)
                  (when (member type '("m+" "m-"))
                    (while (looking-at "\\.\\. ")
                      (forward-line)))
                  (funcall callback type line-begin begin (if (bolp) (logview--character-back-checked (point)) (point))))))))

(defun logview--do-edit-filters (mode)
  (let ((self           (current-buffer))
        (windows        (current-window-configuration))
        (filters        (if (eq mode 'main-filters) logview--main-filter-text      logview--thread-narrowing-filter-text))
        (editing-buffer (if (eq mode 'main-filters) logview--filter-editing-buffer logview--thread-narrowing-filter-editing-buffer)))
    (unless (buffer-live-p editing-buffer)
      (setf (if (eq mode 'main-filters) logview--filter-editing-buffer logview--thread-narrowing-filter-editing-buffer)
            (setf editing-buffer (generate-new-buffer (format "%s: %s" (buffer-name) (if (eq mode 'main-filters) "filters" "thread-narrowing filters"))))))
    (split-window-vertically)
    (other-window 1)
    (switch-to-buffer editing-buffer)
    (unless (eq major-mode 'logview-filter-edit-mode)
      (logview-filter-edit-mode))
    (setf logview-filter-edit--mode                 mode
          logview-filter-edit--parent-buffer        self
          logview-filter-edit--window-configuration windows)
    (logview-filter-edit--initialize-text filters)))

(defun logview--canonical-filter-text (filters)
  (let (filter-lines)
    (logview--iterate-filter-text-lines filters
                                        (lambda (type line-begin _begin end)
                                          (unless (member type '(nil "" "#"))
                                            (push (buffer-substring-no-properties line-begin (1+ end)) filter-lines))))
    (apply #'concat (sort filter-lines #'string<))))

(defun logview--filter-regexp (begin end)
  (replace-regexp-in-string "\n\\.\\. " "\n" (buffer-substring-no-properties begin end)))

(defun logview--find-region-entries (region-start region-end &optional dont-stop-early)
  (logview--std-altering
    (save-excursion
      (save-match-data
        (let ((case-fold-search         nil)
              (buffer-invisibility-spec nil))
          (goto-char region-start)
          (forward-line 0)
          (when (or (looking-at logview--entry-regexp)
                    (re-search-backward logview--entry-regexp nil t)
                    (re-search-forward  logview--entry-regexp nil t))
            (setq region-end (min region-end (point-max)))
            (let* ((match-data  (match-data t))
                   ;; The following depends on exact submode format, i.e. on how many
                   ;; groups there are in `logview--entry-regexp'.
                   (num-points  (- (length match-data) 2))
                   (entry-start (car match-data))
                   (have-level  (memq 'level logview--submode-features)))
              (while (and (or dont-stop-early (null (get-text-property entry-start 'logview-entry)))
                          (let* ((details-start   (progn (forward-line 1) (point)))
                                 (have-next-entry (re-search-forward logview--entry-regexp nil t))
                                 (entry-end       (if have-next-entry (match-beginning 0) (point-max)))
                                 ;; See description of `logview-entry' above.
                                 (logview-entry   (make-vector 13 nil)))
                            (aset logview-entry 0 (- entry-end entry-start))
                            (let ((points (cdr match-data)))
                              (dotimes (k num-points)
                                (let ((point (pop points)))
                                  (aset logview-entry (1+ k) (when point (- point entry-start))))))
                            (when (< details-start entry-end)
                              (aset logview-entry 10 (- details-start entry-start)))
                            (when have-level
                              (aset logview-entry 11 (cadr (assoc (logview--entry-group logview-entry entry-start logview--level-group)
                                                                  logview--submode-level-data))))
                            (setf match-data (match-data t match-data))
                            (put-text-property entry-start entry-end 'logview-entry logview-entry)
                            (setq entry-start entry-end)
                            (< entry-start region-end)))))))))))


(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)
  ;; If nothing is found: if TYPE is nil, just return nil, else signal
  ;; a user error with TYPE as missing thing description.
  (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)
    (when type
      (user-error "Unknown %s '%s'" type key))))

(defun logview--views ()
  "Return the list of all defined views.
Each element is a plist with properties :name, :filters and
:submode.  More properties might be defined later.

This list is preserved across Emacs session in
`logview-views-file'."
  (unless logview--views-initialized
    (condition-case error
        (with-temp-buffer
          (insert-file-contents logview-views-file)
          (setq logview--views (logview--parse-view-definitions)))
      (file-missing)
      (file-error
       ;; Pre-26 versions don't have 'file-missing' error.
       (when (or (>= emacs-major-version 26) (file-exists-p logview-views-file))
         (warn "%s" (error-message-string error)))))
    (setq logview--views-initialized t))
  logview--views)

(defun logview--find-view (view &optional internal)
  (if (or (stringp view) (integerp view))
      (let ((all-views (logview--views))
            matches)
        (while all-views
          (let ((candidate (pop all-views)))
            (when (and (if (stringp view)
                           (string= (plist-get candidate :name) view)
                         (equal (plist-get candidate :index) view))
                       (or (null (plist-get candidate :submode)) (string= (plist-get candidate :submode) logview--submode-name)))
              (push candidate matches))))
        (cond ((null matches)
               (funcall (if internal #'error #'user-error)
                        (if (stringp view) "Unknown view `%s'" "There is no view with quick access index %d") view))
              ((cdr matches)
               (apply (if internal #'error #'user-error)
                      (if (stringp view)
                          `("There are at least two views named `%s'" ,view)
                        `("There are at least two views with quick access index %d (`%s' and `%s')"
                          ,view ,(plist-get (car matches) :name) ,(plist-get (cadr matches) :name)))))
              (t
               (car matches))))
    (if (and (listp view) (stringp (plist-get view :name)))
        view
      (unless internal
        (error "Invalid view object `%S'" view)))))

(defun logview--parse-view-definitions (&optional warn-about-garbage)
  (catch 'done
    (let (views
          pending-name
          pending-submode
          pending-index
          filters-from)
      (while t
        (if (or (eobp) (looking-at logview--view-header-regexp))
            (progn (when pending-name
                     (save-excursion
                       (skip-syntax-backward "-" filters-from)
                       (push `(:name    ,pending-name
                               :submode ,pending-submode
                               ,@(when pending-index `(:index ,pending-index))
                               :filters ,(buffer-substring-no-properties filters-from (point)))
                             views)))
                   (when (eobp)
                     (throw 'done (nreverse views)))
                   (setq pending-name    (match-string-no-properties 2)
                         pending-submode nil
                         pending-index   nil)
                   (while (progn (forward-line)
                                 (cond ((looking-at logview--view-submode-regexp)
                                        (setq pending-submode (match-string-no-properties 2)))
                                       ((looking-at logview--view-index-regexp)
                                        (setq pending-index (string-to-number (match-string-no-properties 2)))))))
                   (setq filters-from (point)))
          (when (and warn-about-garbage (null pending-name) (not (looking-at (rx (0+ blank) (opt "#" (0+ nonl)) eol))))
            (if (yes-or-no-p "Non-comment text before the first view will be discarded; continue? ")
                (setq warn-about-garbage nil)
              (keyboard-quit)))
          (forward-line))))))

(defun logview--insert-view-definitions (&optional predicate)
  (dolist (view (logview--views))
    (when (or (null predicate) (funcall predicate view))
      (unless (bobp)
        (insert "\n"))
      (insert "view " (plist-get view :name) "\n")
      (when (plist-get view :submode)
        (insert "submode " (plist-get view :submode) "\n"))
      (when (plist-get view :index)
        (insert "index " (number-to-string (plist-get view :index)) "\n"))
      (insert (plist-get view :filters))
      (unless (bolp)
        (insert "\n")))))

(defun logview--save-views-if-needed ()
  (when logview--views-need-saving
    (with-temp-buffer
      (logview--insert-view-definitions)
      (write-region (point-min) (point-max) logview-views-file nil 'silent)
      (setq logview--views-need-saving nil))))

(defun logview--after-updating-view-definitions ()
  (dolist (buffer (buffer-list))
    (with-current-buffer buffer
      (when (eq major-mode 'logview-mode)
        (when logview--section-view-name
          (logview--do-set-section-view logview--section-view-name))
        (when logview--highlighted-view-name
          (logview--do-highlight-view-entries logview--highlighted-view-name))))))

(defun logview--do-set-section-view (view)
  (setq view (logview--find-view view t))
  (let ((filters (logview--do-parse-filters (plist-get view :filters))))
    (setq logview--section-view-name (plist-get view :name))
    (unless (prog1 (equal (caar (car logview--section-header-filter)) (caar (car filters)))
              (setq logview--section-header-filter filters))
      (unless (logview--parse-filters)
        ;; If the effective filters haven't changed, we still need to refontify.
        (logview--refontify-buffer)))))

(defun logview--do-highlight-view-entries (view)
  (setq view (logview--find-view view t))
  (let ((filters (logview--do-parse-filters (plist-get view :filters))))
    (setq logview--highlighted-view-name (plist-get view :name))
    (unless (prog1 (equal (caar (car logview--highlighted-filter)) (caar (car filters)))
              (setq logview--highlighted-filter filters))
      (logview--refontify-buffer))))


(defun logview--completing-read (&rest arguments)
  (apply (or logview-completing-read-function
             (if (and (boundp 'ido-mode) (fboundp 'ido-completing-read) ido-mode)
                 #'ido-completing-read
               #'completing-read))
         arguments))



;;; Internal commands meant as hooks.

(defun logview--invalidate-region-entries (region-start region-end &optional _old-length)
  (logview--std-temporarily-widening
    (logview--std-altering
      (when (> region-start (point-min))
        ;; Here we need to go to the entry beginning and then one more entry back: it is
        ;; possible that after the change current text has to be merged into the previous
        ;; entry as details.
        (let ((entry (get-text-property region-start 'logview-entry)))
          (when entry
            (when (eq (get-text-property (1- region-start) 'logview-entry) entry)
              (setq region-start (or (previous-single-property-change region-start 'logview-entry) (point-min))))))
        (when (and (> region-start (point-min)) (get-text-property (1- region-start) 'logview-entry))
          (setq region-start (or (previous-single-property-change (1- region-start) 'logview-entry) (point-min)))))
      (when (< region-end (point-max))
        (let ((entry (get-text-property region-end 'logview-entry)))
          (when (and entry (eq (get-text-property (1+ region-end) 'logview-entry) entry))
            (setq region-end (or (next-single-property-change region-end 'logview-entry) (point-max))))))
      (remove-list-of-text-properties region-start region-end '(logview-entry fontified)))))

(defun logview--pre-command ()
  ;; Reset this when fontification alternates with user-level commands.  If the user is
  ;; able to issue those, we can keep fontifying, as this doesn't appear to interfere with
  ;; editing too much.
  (setf logview--num-fontified-in-row 0))

(defun logview--fontify-region (region-start region-end loudly)
  (when (logview-initialized-p)
    ;; We are basically managing narrowing indirectly, by not fontifying further than
    ;; `region-end' (possibly expanded).  Not using `std' here to prevent
    ;; `logview--iterate-entries-forward' from stopping early because of outer narrowing.
    (logview--temporarily-widening
      ;; We are very fast.  Don't fontify too little to avoid overhead.
      ;; FIXME: See `font-lock-extend-region-functions'.  Might want to reuse that instead.
      (when (and (< region-end (point-max)) (not (get-text-property (1+ region-end) 'fontified)))
        (let ((expanded-region-end (min (point-max) (+ region-start logview--lazy-region-size))))
          (when (< region-end expanded-region-end)
            (setq region-end (or (next-single-property-change (1+ region-end) 'fontified nil expanded-region-end) expanded-region-end)))))
      (when (and (> region-start (point-min)) (not (get-text-property (1- region-start) 'fontified)))
        (let ((expanded-region-start (max (point-min) (- region-end logview--lazy-region-size))))
          (when (> region-start expanded-region-start)
            (setq region-start (or (previous-single-property-change (1- region-start) 'fontified nil expanded-region-start) expanded-region-start)))))
      ;; Largely for derived modes.  Logview itself simply replaces all relevant
      ;; properties (e.g. faces) everywhere in the fontified region and that's normally
      ;; enough.
      (font-lock-unfontify-region region-start region-end)
      (logview--std-altering
        (if logview--postpone-fontification
            (progn (add-face-text-property region-start region-end 'logview-unprocessed)
                   (unless logview--pending-refontifications
                     (run-with-idle-timer 0 nil #'logview--schedule-pending-refontification))
                   (push (list (current-buffer) region-start region-end) logview--pending-refontifications))
          (save-match-data
            (let ((region-start (cdr (logview--do-locate-current-entry region-start))))
              (when region-start
                (let* ((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))
                       (validator                     (cdr logview--effective-filter))
                       (difference-to-section-headers logview--timestamp-difference-to-section-headers)
                       (sections-thread-bound         (when difference-to-section-headers (logview-sections-thread-bound-p)))
                       (common-difference-base        (or logview--timestamp-difference-base
                                                          (when (and difference-to-section-headers (not sections-thread-bound)) :unknown)))
                       (difference-bases-per-thread   (or logview--timestamp-difference-per-thread-bases
                                                          (when (and difference-to-section-headers sections-thread-bound)       (make-hash-table :test #'equal))))
                       (displaying-differences        (or common-difference-base difference-bases-per-thread))
                       (difference-format-string      logview--timestamp-difference-format-string)
                       (header-filter                 (cdr logview--section-header-filter))
                       (show-only-headers             (and header-filter logview--narrow-to-section-headers))
                       (highlighter                   (cdr logview--highlighted-filter))
                       (highlighted-part              logview-highlighted-entry-part)
                       (dim-unsearchable              (and logview-search-only-in-messages isearch-mode)))
                  (logview--iterate-entries-forward
                   region-start
                   (lambda (entry start)
                     (let ((end (logview--entry-end entry start))
                           filtered)
                       (if (or (null validator) (funcall validator entry start))
                           (let ((header-entry (and header-filter (funcall header-filter entry start))))
                             (if (and show-only-headers (not header-entry))
                                 (setf filtered t)
                               (when have-level
                                 (let ((entry-faces (aref logview--submode-level-faces (logview--entry-level entry))))
                                   (put-text-property start end 'face (car entry-faces))
                                   (add-face-text-property (logview--entry-group-start entry start logview--level-group)
                                                           (logview--entry-group-end   entry start logview--level-group)
                                                           (cdr entry-faces))))
                               (when have-timestamp
                                 (let ((from   (logview--entry-group-start entry start logview--timestamp-group))
                                       (to     (logview--entry-group-end   entry start logview--timestamp-group))
                                       (thread (when difference-bases-per-thread (logview--entry-group entry start logview--thread-group)))
                                       timestamp-replaced)
                                   (add-face-text-property from to 'logview-timestamp)
                                   (when displaying-differences
                                     (when (and header-entry difference-to-section-headers)
                                       (let ((base `(,entry . ,start)))
                                         (if difference-bases-per-thread
                                             (puthash thread base difference-bases-per-thread)
                                           (setf common-difference-base base))))
                                     (let ((difference-base (or (when difference-bases-per-thread
                                                                  (gethash thread difference-bases-per-thread (when difference-bases-per-thread :unknown)))
                                                                common-difference-base)))
                                       ;; This is possible only when displaying differences to section header.
                                       ;; Means that we don't yet know where the header is.
                                       (when (eq difference-base :unknown)
                                         ;; FIXME: Might need to improve performance here, e.g. using cache.
                                         (save-excursion
                                           (goto-char start)
                                           (logview--forward-section 0)
                                           (logview--locate-current-entry entry start
                                             (setf difference-base (when (funcall header-filter entry start) `(,entry . ,start)))
                                             (if difference-bases-per-thread
                                                 (puthash thread difference-base difference-bases-per-thread)
                                               (setf common-difference-base difference-base)))))
                                       ;; Hide timestamp with time difference if there is a difference
                                       ;; base entry and we are not positioned over it right now.
                                       (when (and difference-base (not (and (= (cdr difference-base) start)
                                                                            (progn (logview--entry-timestamp entry start)  ; Make sure that it is parsed.
                                                                                   (equal (car difference-base) entry)))))
                                         ;; FIXME: It is possible that fractionals are not the last
                                         ;;        thing in the timestamp, in which case it would be
                                         ;;        nicer to add some spaces on the right. However,
                                         ;;        it's not easy to do and is also quite unlikely,
                                         ;;        so ignoring that for now.
                                         (let* ((difference        (- (logview--entry-timestamp entry start)
                                                                      (logview--entry-timestamp (car difference-base) (cdr difference-base))))
                                                (difference-string (format difference-format-string difference))
                                                (length-delta      (- to from (length difference-string))))
                                           (when (> length-delta 0)
                                             (setq difference-string (concat (make-string length-delta ? ) difference-string)))
                                           (put-text-property from to 'display difference-string)
                                           (setq timestamp-replaced t)))))
                                   (unless timestamp-replaced
                                     (remove-list-of-text-properties from to '(display)))))
                               (when have-name
                                 (add-face-text-property (logview--entry-group-start entry start logview--name-group)
                                                         (logview--entry-group-end   entry start logview--name-group)
                                                         'logview-name))
                               (when have-thread
                                 (add-face-text-property (logview--entry-group-start entry start logview--thread-group)
                                                         (logview--entry-group-end   entry start logview--thread-group)
                                                         'logview-thread))
                               (when header-entry
                                 (add-face-text-property start end 'logview-section))
                               (when dim-unsearchable
                                 (add-face-text-property start (logview--entry-message-start entry start) 'logview-unsearchable))
                               (when (and highlighter (funcall highlighter entry start))
                                 (add-face-text-property (if (eq highlighted-part 'message) (logview--entry-message-start entry start) start)
                                                         (if (eq highlighted-part 'header)  (logview--space-back (logview--entry-message-start entry start)) end)
                                                         'logview-highlight))))
                         (setq filtered t))
                       (logview--update-entry-invisibility start (logview--entry-details-start entry start) end filtered 'propagate 'propagate)
                       ;; There appears to be a bug in displaying code for the case that
                       ;; fontifying function hides all the text in the region it has been
                       ;; called for: Emacs still displays an empty line or at least the
                       ;; ellipses to denote hidden text (i.e. not merged with the
                       ;; previous ellipses).  Previously, we'd work around this by
                       ;; continuing past the region.  However, now we stop anyway because
                       ;; of responsiveness improvements: that is more important than
                       ;; minor displaying glitches.
                       (< end region-end)))))))
            (setf logview--num-fontified-in-row (1+ (or logview--num-fontified-in-row 0)))
            (when (or (input-pending-p) (>= logview--num-fontified-in-row logview--max-fontified-in-row))
              (setf logview--postpone-fontification t))
            ;; `font-lock-default-fontify-region' includes some other calls that we simply
            ;; drop for now.  It is unlikely that e.g. a syntax table would be useful here.
            (unless (equal font-lock-keywords '(t nil))
              ;; This is largely for derived modes.  Logview itself doesn't define any keywords.
              (font-lock-fontify-keywords-region region-start region-end loudly)))))))
  `(jit-lock-bounds ,region-start . ,region-end))

;; Returns non-nil if any part of the entry is visible as a result.
(defun logview--update-entry-invisibility (start details-start end filtered entry-manually-hidden details-manually-hidden)
  (let ((first-line-end-lf-back (logview--character-back (or details-start end)))
        (invisible              (get-text-property (or details-start start) 'invisible))
        new-invisible
        fully-invisible)
    (dolist (element (if (listp invisible) invisible (list invisible)))
      (cond ((and (eq filtered                'propagate) (eq element logview--filtered-symbol))
             (setq filtered t))
            ((and (eq entry-manually-hidden   'propagate) (eq element logview--hidden-entry-symbol))
             (setq entry-manually-hidden t))
            ((and (eq details-manually-hidden 'propagate) (eq element logview--hidden-details-symbol))
             (setq details-manually-hidden t))
            ((not (and (symbolp element) (string-match-p "^logview-" (symbol-name element))))
             (push element new-invisible))))
    (when (eq entry-manually-hidden t)
      (push logview--hidden-entry-symbol new-invisible)
      (setq fully-invisible t))
    (when (eq filtered t)
      (push logview--filtered-symbol new-invisible)
      (setq fully-invisible t))
    (if new-invisible
        (put-text-property (logview--character-back-checked start) first-line-end-lf-back 'invisible (setq new-invisible (nreverse new-invisible)))
      (remove-list-of-text-properties (logview--character-back-checked start) first-line-end-lf-back '(invisible)))
    (when details-start
      (when (eq details-manually-hidden t)
        (push logview--hidden-details-symbol new-invisible))
      (push 'logview-details new-invisible)
      (put-text-property first-line-end-lf-back (logview--character-back end) 'invisible new-invisible))
    (not fully-invisible)))

(defun logview--schedule-pending-refontification ()
  (if (input-pending-p)
      ;; To improve responsibility, do nothing this time, but reschedule ourselves with a
      ;; little bit of delay, especially if currently in a non-Logview mode (or editing
      ;; filters etc.).
      (run-with-idle-timer (if (eq major-mode 'logview-mode) 0 1) nil #'logview--schedule-pending-refontification)
    (let ((refontifications logview--pending-refontifications))
      (dolist (entry refontifications)
        (let ((buffer       (nth 0 entry))
              (region-start (nth 1 entry))
              (region-end   (nth 2 entry)))
          (when (buffer-live-p buffer)
            (with-current-buffer buffer
              (setf logview--postpone-fontification nil
                    logview--num-fontified-in-row   0)
              (logview--temporarily-widening
                (logview--std-altering
                  (remove-list-of-text-properties region-start region-end '(fontified))))))))
      (setf logview--pending-refontifications nil))))

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

(defun logview--isearch-filter-predicate (begin end)
  (and (funcall (default-value 'isearch-filter-predicate) begin end)
       (or (not logview-search-only-in-messages)
           (logview--std-temporarily-widening
             (let ((entry+start (logview--do-locate-current-entry begin)))
               (when entry+start
                 (let* ((entry         (car entry+start))
                        (start         (cdr entry+start))
                        (message-start (logview--entry-message-start entry start))
                        (message-end   (logview--entry-end entry start)))
                   (and (>= begin message-start) (<= end message-end)))))))))

;; Exists for potential future expansion.
(defun logview--kill-emacs-hook ()
  (logview--save-views-if-needed))



;;; Advanced integration with isearch.

(defvar logview-isearch-map
  (let ((map (make-sparse-keymap)))
    (dolist (binding '(("M-h" logview-toggle-narrow-to-section-headers)
                       ("M-m" logview-toggle-search-only-in-messages)))
      (define-key map (kbd (car binding)) (cadr binding)))
    map)
  "Keymap used when isearching in a Logview buffer.")

(defvar logview--isearch-mode-map-original-parent t)
(defvar logview--isearch-option-originals nil)

;; According to own source code of Emacs, this snot-glued hack is the standard way of
;; altering isearch behavior, i.e. with hooks.  Oh well.  A lot of code also modifies
;; `isearch-map' bindings directly, but this feels very dirty: e.g. what if this conflicts
;; with a user's private binding?
(defun logview--starting-isearch ()
  (setf logview--isearch-option-originals nil)
  (dolist (option '(logview--narrow-to-section-headers logview-search-only-in-messages))
    (push `(,option . ,(symbol-value option)) logview--isearch-option-originals))
  (when logview-search-only-in-messages
    (logview--refontify-buffer))
  (let ((parent (keymap-parent isearch-mode-map)))
    ;; Need to make commands findable in `isearch-mode-map' (via its parent), else isearch
    ;; will abort on any of our extension commands.
    (set-keymap-parent isearch-mode-map (if parent
                                            (make-composed-keymap logview-isearch-map parent)
                                          logview-isearch-map))
    (setf logview--isearch-mode-map-original-parent parent)))

(defun logview--ending-isearch ()
  (when logview-search-only-in-messages
    (logview--refontify-buffer))
  (let ((narrowed-to-section-headers (and logview--narrow-to-section-headers logview--section-header-filter)))
    ;; Restore original values of certain options, for consistency with `M-c' or `M-r'
    ;; during Isearch that affect only the current search.
    (dolist (entry logview--isearch-option-originals)
      (set (car entry) (cdr entry)))
    (when (and narrowed-to-section-headers (not logview--narrow-to-section-headers))
      (logview--parse-filters)))
  (setf logview--isearch-option-originals nil)
  (unless (eq logview--isearch-mode-map-original-parent t)
    (set-keymap-parent isearch-mode-map logview--isearch-mode-map-original-parent)
    (setf logview--isearch-mode-map-original-parent t)))

(defun logview--isearch-update-if-running ()
  (when isearch-mode
    ;; This is what `isearch-define-mode-toggle' does.  Let's assume it's needed.
    (setf isearch-success  t
          isearch-adjusted 'toggle)
    ;; Otherwise it won't update lazy highlighting.
    (setf isearch-lazy-highlight-last-string nil)
    (isearch-update)))



;;; Logview Filter Edit mode.

(defvar logview-filter-edit-mode-map
  (let ((map (make-sparse-keymap)))
    (dolist (binding '(("C-c C-c" logview-filter-edit-save)
                       ("C-c C-a" logview-filter-edit-apply)
                       ("C-c C-k" logview-filter-edit-cancel)
                       ("C-c C-p" logview-toggle-filter-preview)))
      (define-key map (kbd (car binding)) (cadr binding)))
    map))

(defvar logview-filter-edit-font-lock-keywords
  '(logview-filter-edit--font-lock-region))

(define-derived-mode logview-filter-edit-mode nil "Logview Filters"
  "Major mode for editing filters of a Logview buffer."
  (setq-local font-lock-defaults '(logview-filter-edit-font-lock-keywords t))
  (add-hook 'after-change-functions #'logview-filter-edit--schedule-preview t t))

(defun logview-filter-edit-save ()
  "Save the edited filters or views and quit the buffer and window."
  (interactive)
  (logview-filter-edit--do t t))

(defun logview-filter-edit-apply ()
  "Apply the filters or views, but don't quit the buffer.
In other words, buffer stays for futher editing.  Using `\\<logview-filter-edit-mode-map>\\[logview-filter-edit-cancel]'
afterwards will only reset to the last applied state, not
necessarily to the filters (views) how they were when the buffer
got created."
  (interactive)
  (logview-filter-edit--do t nil)
  ;; Because of possible preview and because the filter buffer is not closed, it may not
  ;; be obvious otherwise that something is done at all.
  (message "Filters have been applied successfully"))

(defun logview-filter-edit-cancel ()
  "Quit the buffer and its window, discarding all edits.
If `\\<logview-filter-edit-mode-map>\\[logview-filter-edit-apply]' has been used from this buffer, its effects remain:
only edits after it get discarded."
  (interactive)
  (logview-filter-edit--do nil t))

(defun logview-filter-edit--do (process-filters quit)
  (let* ((mode    logview-filter-edit--mode)
         (parent  logview-filter-edit--parent-buffer)
         (windows logview-filter-edit--window-configuration)
         filters-changed)
    (when quit
      (with-current-buffer parent
        (when logview--preview-filter-text
          (setf logview--preview-filter-text nil
                filters-changed              t))))
    (when process-filters
      (if (eq mode 'views)
          (let ((new-views (save-excursion
                             (goto-char (point-min))
                             (logview--parse-view-definitions t))))
            (setf logview--views
                  (if logview-filter-edit--editing-views-for-submode
                      (let ((combined-views (nreverse new-views)))
                        (dolist (view (logview--views))
                          (unless (equal (plist-get view :submode) logview-filter-edit--editing-views-for-submode)
                            (push view combined-views)))
                        (nreverse combined-views))
                    new-views))
            (setf logview--views-need-saving t)
            (logview--after-updating-view-definitions)
            (with-current-buffer parent
              (logview--update-mode-name)))
        (let ((filters      (buffer-substring-no-properties
                             (point-min) (point-max)))
              (hint-comment (logview-filter-edit--hint-comment)))
            (when (string-prefix-p hint-comment filters)
              (setf filters (substring filters (length hint-comment))))
            (with-current-buffer parent
              (if (eq process-filters 'preview)
                  (setf logview--preview-filter-text `(,(eq mode 'main-filters) . ,filters))
                (setf (if (eq mode 'main-filters) logview--main-filter-text logview--thread-narrowing-filter-text) filters))
              (setf filters-changed t)))))
    (when filters-changed
      (with-current-buffer parent
        (logview--parse-filters)))
    (when quit
      (kill-buffer)
      (switch-to-buffer parent)
      (set-window-configuration windows))))

(defun logview-filter-edit--initialize-text (&optional filter-text)
  (erase-buffer)
  (unless (and filter-text (string-prefix-p (logview-filter-edit--hint-comment) filter-text))
    (insert (logview-filter-edit--hint-comment)))
  (if (eq logview-filter-edit--mode 'views)
      (logview--insert-view-definitions (when logview-filter-edit--editing-views-for-submode
                                          (lambda (view) (string= (plist-get view :submode) logview-filter-edit--editing-views-for-submode))))
    (insert filter-text)
    (unless (bolp)
      (insert "\n")))
  ;; Put cursor at the first filter beginning if possible.
  (goto-char (point-min))
  (logview--iterate-filter-buffer-lines (lambda (type _line-begin begin _end)
                                          (if (member type logview--valid-filter-prefixes)
                                              (progn (goto-char begin) nil)
                                            t)))
  (set-buffer-modified-p nil))

(defun logview-filter-edit--font-lock-region (region-end)
  (save-excursion
    (save-match-data
      ;; Not even in a Logview mode buffer, not using `std'.
      (logview--temporarily-widening
        (with-silent-modifications
          (forward-line 0)
          ;; Never try to parse from the middle of a multiline filter.
          (while (and (not (bobp))
                      (looking-at "\\.\\. "))
            (forward-line -1))
          (logview--iterate-filter-buffer-lines
           (lambda (type line-begin begin end)
             (unless (pcase type
                       (`nil
                        (when (eq logview-filter-edit--mode 'views)
                          (save-excursion
                            (goto-char line-begin)
                            (cond ((looking-at logview--view-header-regexp)
                                   (put-text-property (match-beginning 1) (match-end 1) 'face 'font-lock-keyword-face)
                                   (put-text-property (match-beginning 2) (match-end 2) 'face 'font-lock-function-name-face)
                                   t)
                                  ((looking-at logview--view-submode-regexp)
                                   (put-text-property (match-beginning 1) (match-end 1) 'face 'font-lock-keyword-face)
                                   (let ((submode-name (match-string-no-properties 2)))
                                     (put-text-property (match-beginning 2) (match-end 2) 'face (if (or (assoc submode-name logview-std-submodes)
                                                                                                        (assoc submode-name logview-additional-submodes))
                                                                                                    'font-lock-variable-name-face
                                                                                                  'error)))
                                   t)
                                  ((looking-at logview--view-index-regexp)
                                   (put-text-property (match-beginning 1) (match-end 1) 'face 'font-lock-keyword-face)
                                   (put-text-property (match-beginning 2) (match-end 2) 'face 'font-lock-constant-face)
                                   t)))))
                       ("#"
                        (put-text-property begin end 'face 'font-lock-comment-face)
                        t)
                       (""
                        (put-text-property begin end 'face nil)
                        t)
                       ((or "lv" "LV")
                        (when (memq logview-filter-edit--mode '(main-filters views))
                          (put-text-property line-begin begin 'face 'logview-edit-filters-type-prefix)
                          (let ((level-string (buffer-substring-no-properties begin end))
                                (known-levels (with-current-buffer logview-filter-edit--parent-buffer
                                                logview--submode-level-data)))
                            (while (and level-string known-levels)
                              (if (string= (caar known-levels) level-string)
                                  (setq level-string nil)
                                (setq known-levels (cdr known-levels))))
                            (put-text-property begin end 'face (if level-string 'error nil)))
                          t))
                       (_
                        (when (or (memq logview-filter-edit--mode '(main-filters views)) (member type '("t+" "t-")))
                          (let* ((valid (logview--valid-regexp-p (logview--filter-regexp begin end))))
                            (goto-char begin)
                            (while (let ((from (point)))
                                     (put-text-property (- from 3) from 'face 'logview-edit-filters-type-prefix)
                                     (forward-line)
                                     (put-text-property from (if (bolp) (logview--character-back (point)) (point))
                                                        'face (unless valid 'error))
                                     (when (< (point) end)
                                       (forward-char 3)
                                       t))))
                          t)))
               (put-text-property begin end 'face 'error))
             (< (point) region-end)))))))
  ;; Tell font-lock that it's not worth calling us back for "further matches".
  nil)

(defun logview-filter-edit--schedule-preview (&rest _ignored)
  (unless (or logview-filter-edit--preview-timer (eq logview-filter-edit--mode 'views))
    (setf logview-filter-edit--preview-timer (run-with-idle-timer 0.2 nil #'logview-filter-edit--trigger-preview (current-buffer)))))

(defun logview-filter-edit--trigger-preview (buffer)
  (let ((debug-on-error t))
    (when (buffer-live-p buffer)
      (with-current-buffer buffer
        (logview-filter-edit--do 'preview nil)
        ;; Clear the timer only now, so if it gets rescheduled above, it gets removed
        ;; instantly.
        (when logview-filter-edit--preview-timer
          (cancel-timer logview-filter-edit--preview-timer)
          (setf logview-filter-edit--preview-timer nil))))))

(defun logview-filter-edit--hint-comment ()
  (pcase logview-filter-edit--mode
    (`main-filters             logview-filter-edit--filters-hint-comment)
    (`thread-narrowing-filters logview-filter-edit--thread-narrowing-filters-hint-comment)
    (`views                    logview-filter-edit--views-hint-comment)))


(add-hook 'kill-emacs-hook #'logview--kill-emacs-hook)
(run-with-idle-timer 30 t #'logview--save-views-if-needed)


(provide 'logview)

;;; logview.el ends here
