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