dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

commit 78cd3caf557b6ac633a5b99d5f26adab228f7f13
parent 5dc866eed67306c4dd522c169a62769d51644e2f
Author: Lukas Henkel <lh@entf.net>
Date:   Thu, 31 Mar 2022 22:39:39 +0200

Add deadgrep

Diffstat:
Aelpa/deadgrep-20220209.719/deadgrep-autoloads.el | 35+++++++++++++++++++++++++++++++++++
Aelpa/deadgrep-20220209.719/deadgrep-pkg.el | 2++
Aelpa/deadgrep-20220209.719/deadgrep.el | 1602+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelpa/deadgrep-20220209.719/deadgrep.elc | 0
Aelpa/spinner-1.7.4.signed | 2++
Aelpa/spinner-1.7.4/README.org | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelpa/spinner-1.7.4/all-spinners.gif | 0
Aelpa/spinner-1.7.4/some-spinners.gif | 0
Aelpa/spinner-1.7.4/spinner-autoloads.el | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelpa/spinner-1.7.4/spinner-pkg.el | 2++
Aelpa/spinner-1.7.4/spinner.el | 340+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelpa/spinner-1.7.4/spinner.elc | 0
Minit.el | 4+++-
13 files changed, 2139 insertions(+), 1 deletion(-)

diff --git a/elpa/deadgrep-20220209.719/deadgrep-autoloads.el b/elpa/deadgrep-20220209.719/deadgrep-autoloads.el @@ -0,0 +1,35 @@ +;;; deadgrep-autoloads.el --- automatically extracted autoloads -*- lexical-binding: t -*- +;; +;;; Code: + +(add-to-list 'load-path (directory-file-name + (or (file-name-directory #$) (car load-path)))) + + +;;;### (autoloads nil "deadgrep" "deadgrep.el" (0 0 0 0)) +;;; Generated autoloads from deadgrep.el + +(autoload 'deadgrep "deadgrep" "\ +Start a ripgrep search for SEARCH-TERM in DIRECTORY. + +If not provided, DIR defaults to the directory as determined by +`deadgrep-project-root-function'. + +See also `deadgrep-project-root-overrides'. + +If called with a prefix argument, create the results buffer but +don't actually start the search. + +\(fn SEARCH-TERM &optional DIRECTORY)" t nil) + +(register-definition-prefixes "deadgrep" '("deadgrep-")) + +;;;*** + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; coding: utf-8-emacs-unix +;; End: +;;; deadgrep-autoloads.el ends here diff --git a/elpa/deadgrep-20220209.719/deadgrep-pkg.el b/elpa/deadgrep-20220209.719/deadgrep-pkg.el @@ -0,0 +1,2 @@ +;;; Generated package description from deadgrep.el -*- no-byte-compile: t -*- +(define-package "deadgrep" "20220209.719" "fast, friendly searching with ripgrep" '((emacs "25.1") (dash "2.12.0") (s "1.11.0") (spinner "1.7.3")) :commit "0a3ba239c458ffc4f63a180b43d0e70b81742a3e" :authors '(("Wilfred Hughes" . "me@wilfred.me.uk")) :maintainer '("Wilfred Hughes" . "me@wilfred.me.uk") :keywords '("tools") :url "https://github.com/Wilfred/deadgrep") diff --git a/elpa/deadgrep-20220209.719/deadgrep.el b/elpa/deadgrep-20220209.719/deadgrep.el @@ -0,0 +1,1602 @@ +;;; deadgrep.el --- fast, friendly searching with ripgrep -*- lexical-binding: t; -*- + +;; Copyright (C) 2018 Wilfred Hughes + +;; Author: Wilfred Hughes <me@wilfred.me.uk> +;; URL: https://github.com/Wilfred/deadgrep +;; Package-Version: 20220209.719 +;; Package-Commit: 0a3ba239c458ffc4f63a180b43d0e70b81742a3e +;; Keywords: tools +;; Version: 0.11 +;; Package-Requires: ((emacs "25.1") (dash "2.12.0") (s "1.11.0") (spinner "1.7.3")) + +;; 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 <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Perform text searches with the speed of ripgrep and the comfort of +;; Emacs. This is a bespoke mode that does not rely on +;; compilation-mode, but tries to be a perfect fit for ripgrep. + +;; Install from MELPA, then `M-x deadgrep' will do a search! + +;;; Code: + +(require 'cl-lib) +(require 's) +(require 'dash) +(require 'spinner) + +(defgroup deadgrep nil + "A powerful text search UI using ripgrep." + :group 'tools + :group 'matching) + +(defcustom deadgrep-executable + "rg" + "The rg executable used by deadgrep. +This will be looked up on `exec-path' if it isn't an absolute +path to the binary." + :type 'string + :group 'deadgrep) + +(defcustom deadgrep-max-buffers + 4 + "Deadgrep will kill the least recently used results buffer +if there are more than this many. + +To disable cleanup entirely, set this variable to nil." + :type '(choice + (number :tag "Maximum of buffers allowed") + (const :tag "Disable cleanup" nil)) + :group 'deadgrep) + +(defcustom deadgrep-project-root-function + #'deadgrep--project-root + "Function called by `deadgrep' to work out the root directory +to search from. + +See also `deadgrep-project-root-overrides'." + :type 'function + :group 'deadgrep) + +(defvar deadgrep-project-root-overrides nil + "An alist associating project directories with the desired +search directory. + +This is useful for large repos where you only want to search a +subdirectory. It's also handy for nested repos where you want to +search from the parent. + +This affects the behaviour of `deadgrep--project-root', so this +variable has no effect if you change +`deadgrep-project-root-function'.") + +(defvar deadgrep-history + nil + "A list of the previous search terms.") + +(defvar deadgrep-max-line-length + 500 + "Truncate lines if they are longer than this. + +Emacs performance can be really poor with long lines, so this +ensures that searching minified files does not slow down movement +in results buffers. + +In extreme cases (100KiB+ single-line files), we can get a stack +overflow on our regexp matchers if we don't apply this.") + +(defcustom deadgrep-display-buffer-function + 'switch-to-buffer-other-window + "Function used to show the deadgrep result buffer. + +This function is called with one argument, the results buffer to +display." + :type 'function + :group 'deadgrep) + +(defface deadgrep-meta-face + '((t :inherit font-lock-comment-face)) + "Face used for deadgrep UI text." + :group 'deadgrep) + +(defface deadgrep-filename-face + '((t :inherit bold)) + "Face used for filename headings in results buffers." + :group 'deadgrep) + +(defface deadgrep-search-term-face + '((t :inherit font-lock-variable-name-face)) + "Face used for the search term in results buffers." + :group 'deadgrep) + +(defface deadgrep-regexp-metachar-face + '((t :inherit + ;; TODO: I've seen a more appropriate face in some themes, + ;; find out what to use instead here. + font-lock-constant-face)) + "Face used for regexp metacharacters in search terms." + :group 'deadgrep) + +(defface deadgrep-match-face + '((t :inherit match)) + "Face used for the portion of a line that matches the search term." + :group 'deadgrep) + +(defvar-local deadgrep--search-term nil) +;; Ensure this variable is ignored by `kill-all-local-variables' when +;; switching between `deadgrep-mode' and `deadgrep-edit-mode'. +(put 'deadgrep--search-term 'permanent-local t) + +(defvar-local deadgrep--search-type 'string) +(put 'deadgrep--search-type 'permanent-local t) +(defvar-local deadgrep--search-case 'smart) +(put 'deadgrep--search-case 'permanent-local t) +(defvar-local deadgrep--file-type 'all) +(put 'deadgrep--file-type 'permanent-local t) + +(defvar-local deadgrep--context nil + "When set, also show context of results. +This is stored as a cons cell of integers (lines-before . lines-after).") +(put 'deadgrep--context 'permanent-local t) +(defvar-local deadgrep--initial-filename nil + "The filename of the buffer that deadgrep was started from. +Used to offer better default values for file options.") +(put 'deadgrep--initial-filename 'permanent-local t) + +(defvar-local deadgrep--current-file nil + "The file we're currently inserting results for.") +(defvar-local deadgrep--spinner nil) +(defvar-local deadgrep--remaining-output nil + "We can't guarantee that our process filter will always receive whole lines. +We save the last line here, in case we need to append more text to it.") +(defvar-local deadgrep--postpone-start nil + "If non-nil, don't (re)start searches.") +(defvar-local deadgrep--running nil + "If non-nil, a search is still running.") + +(defvar-local deadgrep--debug-command nil) +(put 'deadgrep--debug-command 'permanent-local t) +(defvar-local deadgrep--debug-first-output nil) +(put 'deadgrep--debug-first-output 'permanent-local t) + +(defvar-local deadgrep--imenu-alist nil + "Alist that stores filename and position for each matched files. +It is used to create `imenu' index.") + +(defconst deadgrep--position-column-width 5) + +(defconst deadgrep--color-code + (rx "\x1b[" (+ digit) "m") + "Regular expression for an ANSI color code.") + +(defun deadgrep--insert-output (output &optional finished) + "Propertize OUTPUT from rigrep and write to the current buffer." + ;; If we had an unfinished line from our last call, include that. + (when deadgrep--remaining-output + (setq output (concat deadgrep--remaining-output output)) + (setq deadgrep--remaining-output nil)) + + (let ((inhibit-read-only t) + (lines (s-lines output)) + prev-line-num) + ;; Process filters run asynchronously, and don't guarantee that + ;; OUTPUT ends with a complete line. Save the last line for + ;; later processing. + (unless finished + (setq deadgrep--remaining-output (-last-item lines)) + (setq lines (butlast lines))) + + (save-excursion + (goto-char (point-max)) + (dolist (line lines) + (cond + ;; Ignore blank lines. + ((s-blank? line)) + ;; Lines of just -- are used as a context separator when + ;; calling ripgrep with context flags. + ((string= line "--") + (let ((separator "--")) + ;; Try to make the separator length match the previous + ;; line numbers. + (when prev-line-num + (setq separator + (s-repeat (log prev-line-num 10) "-"))) + (insert + (propertize (concat separator "\n") + 'face 'deadgrep-meta-face)))) + ;; If we have a warning or don't have a color code, ripgrep + ;; must be complaining about something (e.g. zero matches for + ;; a glob, or permission denied on some directories). + ((or + (s-starts-with-p "WARNING: " line) + (not (s-matches-p deadgrep--color-code line))) + (when deadgrep--current-file + (setq deadgrep--current-file nil) + (insert "\n")) + (insert line "\n\n")) + (t + (-let* ((truncate-p (> (length line) deadgrep-max-line-length)) + (line + (if truncate-p + (substring line 0 deadgrep-max-line-length) + line)) + ((filename line-num content) (deadgrep--split-line line)) + (formatted-line-num + (s-pad-right deadgrep--position-column-width " " + (number-to-string line-num))) + (pretty-line-num + (propertize formatted-line-num + 'face 'deadgrep-meta-face + 'deadgrep-filename filename + 'deadgrep-line-number line-num + 'read-only t + 'front-sticky t + 'rear-nonsticky t)) + (pretty-filename + (propertize filename + 'face 'deadgrep-filename-face + 'deadgrep-filename filename + 'read-only t + 'front-sticky t))) + (cond + ;; This is the first file we've seen, print the heading. + ((null deadgrep--current-file) + (push (cons filename (point)) deadgrep--imenu-alist) + (insert pretty-filename "\n")) + ;; This is a new file, print the heading with a spacer. + ((not (equal deadgrep--current-file filename)) + (push (cons filename (1+ (point))) deadgrep--imenu-alist) + (insert "\n" pretty-filename "\n"))) + (setq deadgrep--current-file filename) + + ;; TODO: apply the invisible property if the user decided + ;; to hide this filename before we finished finding + ;; results in it. + (insert pretty-line-num content) + (when truncate-p + (insert + (propertize " ... (truncated)" + 'face 'deadgrep-meta-face))) + (insert "\n") + + (setq prev-line-num line-num)))))))) + +(defcustom deadgrep-finished-hook nil + "Hook run when `deadgrep' search is finished." + :type 'hook + :group 'deadgrep) + +(defun deadgrep--process-sentinel (process output) + "Update the deadgrep buffer associated with PROCESS as complete." + (let ((buffer (process-buffer process)) + (finished-p (string= output "finished\n"))) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (setq deadgrep--running nil) + ;; rg has terminated, so stop the spinner. + (spinner-stop deadgrep--spinner) + + (deadgrep--insert-output "" finished-p) + + ;; Report any errors that occurred. + (unless (member output + (list + "exited abnormally with code 1\n" + "interrupt\n" + "finished\n")) + (save-excursion + (let ((inhibit-read-only t)) + (goto-char (point-max)) + (insert output)))) + + (run-hooks 'deadgrep-finished-hook) + (message "Deadgrep finished"))))) + +(defun deadgrep--process-filter (process output) + ;; Searches may see a lot of output, but it's really useful to have + ;; a snippet of output when debugging. Store the first output received. + (unless deadgrep--debug-first-output + (setq deadgrep--debug-first-output output)) + + ;; If we had an unfinished line from our last call, include that. + (when deadgrep--remaining-output + (setq output (concat deadgrep--remaining-output output)) + (setq deadgrep--remaining-output nil)) + + (when (buffer-live-p (process-buffer process)) + (with-current-buffer (process-buffer process) + (deadgrep--insert-output output)))) + +(defun deadgrep--extract-regexp (pattern s) + "Search for PATTERN in S, and return the content of the first group." + (string-match pattern s) + (match-string 1 s)) + +(defconst deadgrep--filename-regexp + (rx bos "\x1b[0m\x1b[3" (or "5" "6") "m" + (? "./") + (group (+? anything)) + "\x1b[") + "Extracts the filename from a ripgrep line with ANSI color sequences. +We use the color sequences to extract the filename exactly, even +if the path contains colons.") + +(defconst deadgrep--line-num-regexp + (rx "\x1b[32m" (group (+ digit))) + "Extracts the line number from a ripgrep line with ANSI color sequences. +Ripgrep uses a unique color for line numbers, so we use that to +extract the linue number exactly.") + +(defconst deadgrep--line-contents-regexp + (rx "\x1b[32m" (+ digit) "\x1b[0m" (or ":" "-") (group (* anything))) + "Extract the line contents from a ripgrep line with ANSI color sequences. +Use the unique color for line numbers to ensure we start at the +correct colon. + +Note that the text in the group will still contain color codes +highlighting which parts matched the user's search term.") + +(defconst deadgrep--hit-regexp + (rx-to-string + `(seq + ;; A reset color code. + "\x1b[0m" + ;; Two color codes, bold and color (any order). + (regexp ,deadgrep--color-code) + (regexp ,deadgrep--color-code) + ;; The actual text. + (group (+? anything)) + ;; A reset color code again. + "\x1b[0m")) + "Extract the portion of a line found by ripgrep that matches the user's input. +This may occur multiple times in one line.") + +(defun deadgrep--split-line (line) + "Split out the components of a raw LINE of output from rg. +Return the filename, line number, and the line content with ANSI +color codes replaced with string properties." + (list + (deadgrep--extract-regexp deadgrep--filename-regexp line) + (string-to-number + (deadgrep--extract-regexp deadgrep--line-num-regexp line)) + (deadgrep--propertize-hits + (deadgrep--extract-regexp deadgrep--line-contents-regexp line)))) + +(defun deadgrep--escape-backslash (s) + "Escape occurrences of backslashes in S. + +This differs from `regexp-quote', which outputs a regexp pattern. +Instead, we provide a string suitable for REP in +`replace-regexp-in-string'." + (s-replace "\\" "\\\\" s)) + +(defun deadgrep--propertize-hits (line-contents) + "Given LINE-CONTENTS from ripgrep, replace ANSI color codes +with a text face property `deadgrep-match-face'." + (replace-regexp-in-string + deadgrep--hit-regexp + (lambda (s) + (propertize + (deadgrep--escape-backslash (match-string 1 s)) + 'face 'deadgrep-match-face)) + line-contents)) + +(define-button-type 'deadgrep-search-term + 'action #'deadgrep--search-term + 'help-echo "Change search term") + +(defun deadgrep--search-prompt (&optional default) + "The prompt shown to the user when starting a deadgrep search." + (let ((kind (if (eq deadgrep--search-type 'regexp) + "by regexp" "for text"))) + (if default + (format "Search %s (default %s): " kind default) + (format "Search %s: " kind)))) + +(defun deadgrep--search-term (_button) + (deadgrep-search-term)) + +(defun deadgrep-search-term () + "Change the current search term and restart the search." + (interactive) + (setq deadgrep--search-term + (read-from-minibuffer + (deadgrep--search-prompt) + deadgrep--search-term)) + (rename-buffer + (deadgrep--buffer-name deadgrep--search-term default-directory) t) + (deadgrep-restart)) + +(define-button-type 'deadgrep-type + 'action #'deadgrep--search-type + 'search-type nil + 'help-echo "Change search type") + +(defun deadgrep--search-type (button) + (setq deadgrep--search-type (button-get button 'search-type)) + (deadgrep-restart)) + +(define-button-type 'deadgrep-case + 'action #'deadgrep--case + 'case nil + 'help-echo "Change case sensitivity") + +(defun deadgrep--case (button) + (setq deadgrep--search-case (button-get button 'case)) + (deadgrep-restart)) + +(define-button-type 'deadgrep-context + 'action #'deadgrep--context + 'context nil + 'help-echo "Show/hide context around match") + +(defun deadgrep--context (button) + ;; deadgrep--context takes the value of (before . after) when set. + (setq deadgrep--context + (cl-case (button-get button 'context) + ((nil) + nil) + (before + (cons + (read-number "Show N lines before: ") + (or (cdr-safe deadgrep--context) 0))) + (after + (cons + (or (car-safe deadgrep--context) 0) + (read-number "Show N lines after: "))) + (t + (error "Unknown context type")))) + + (deadgrep-restart)) + +(defun deadgrep--type-list () + "Query the rg executable for available file types." + (let* ((output (with-output-to-string + (with-current-buffer standard-output + (process-file-shell-command + (format "%s --type-list" deadgrep-executable) + nil '(t nil))))) + (lines (s-lines (s-trim output))) + (types-and-globs + (--map + (s-split (rx ": ") it) + lines))) + (-map + (-lambda ((type globs)) + (list type (s-split (rx ", ") globs))) + types-and-globs))) + +(define-button-type 'deadgrep-file-type + 'action #'deadgrep--file-type + 'case nil + 'help-echo "Change file type") + +(defun deadgrep--format-file-type (file-type extensions) + (let* ((max-exts 4) + (truncated (> (length extensions) max-exts))) + (when truncated + (setq extensions + (append (-take max-exts extensions) + (list "...")))) + (format "%s (%s)" + file-type + (s-join ", " extensions)))) + +(defun deadgrep--glob-regexp (glob) + "Convert GLOB pattern to the equivalent elisp regexp." + (let* ((i 0) + (result "^")) + (while (< i (length glob)) + (let* ((char (elt glob i))) + (cond + ;; . matches a literal . in globs. + ((eq char ?.) + (setq result (concat result "\\.")) + (cl-incf i)) + ;; ? matches a single char in globs. + ((eq char ??) + (setq result (concat result ".")) + (cl-incf i)) + ;; * matches zero or more of anything. + ((eq char ?*) + (setq result (concat result ".*")) + (cl-incf i)) + ;; [ab] matches a literal a or b. + ;; [a-z] matches characters between a and z inclusive. + ;; [?] matches a literal ?. + ((eq char ?\[) + ;; Find the matching ]. + (let ((j (1+ i))) + (while (and (< j (length glob)) + (not (eq (elt glob j) ?\]))) + (cl-incf j)) + (cl-incf j) + (setq result (concat result + (substring glob i j))) + (setq i j))) + (t + (setq result (concat result (char-to-string char))) + (cl-incf i))))) + (concat result "$"))) + +(defun deadgrep--matches-globs-p (filename globs) + "Return non-nil if FILENAME matches any glob pattern in GLOBS." + (when filename + (--any (string-match-p (deadgrep--glob-regexp it) filename) + globs))) + +(defun deadgrep--relevant-file-type (filename types-and-globs) + "Try to find the most relevant item in TYPES-AND-GLOBS for FILENAME." + (let (;; Find all the items in TYPES-AND-GLOBS whose glob match + ;; FILENAME. + (matching (-filter (-lambda ((_type globs)) + (deadgrep--matches-globs-p filename globs)) + types-and-globs))) + (->> matching + ;; Prefer longer names, so "markdown" over "md" for the type + ;; name. + (-sort (-lambda ((type1 _) (type2 _)) + (< (length type1) (length type2)))) + ;; Prefer types with more extensions, as they tend to be more + ;; common languages (e.g. 'ocaml' over 'ml'). + (-sort (-lambda ((_ globs1) (_ globs2)) + (< (length globs1) (length globs2)))) + ;; But prefer elisp over lisp for .el files. + (-sort (-lambda ((type1 _) (type2 _)) + ;; Return t if we're comparing elisp with lisp, nil + ;; otherwise. + (and (equal type1 "lisp") + (equal type2 "elisp")))) + ;; Take the highest scoring matching. + (-last-item)))) + +(defun deadgrep--read-file-type (filename) + "Read a ripgrep file type, defaulting to the type that matches FILENAME." + (let* (;; Get the list of types we can offer. + (types-and-globs (deadgrep--type-list)) + ;; Build a list mapping the formatted types to the type name. + (type-choices + (-map + (-lambda ((type globs)) + (list + (deadgrep--format-file-type type globs) + type)) + types-and-globs)) + ;; Work out the default type name based on the filename. + (default-type-and-globs + (deadgrep--relevant-file-type filename types-and-globs)) + (default + (-when-let ((default-type default-globs) default-type-and-globs) + (deadgrep--format-file-type default-type default-globs))) + ;; Prompt the user for a file type. + (chosen + (completing-read + "File type: " type-choices nil t nil nil default))) + (nth 1 (assoc chosen type-choices)))) + +(defun deadgrep--file-type (button) + (let ((button-type (button-get button 'file-type))) + (cond + ((eq button-type 'all) + (setq deadgrep--file-type 'all)) + ((eq button-type 'type) + (let ((new-file-type + (deadgrep--read-file-type deadgrep--initial-filename))) + (setq deadgrep--file-type (cons 'type new-file-type)))) + ((eq button-type 'glob) + (let* ((initial-value + (cond + ;; If we already have a glob pattern, edit it. + ((eq (car-safe deadgrep--file-type) 'glob) + (cdr deadgrep--file-type)) + ;; If the initial file had a file name of the form + ;; foo.bar, offer *.bar as the initial glob. + ((and deadgrep--initial-filename + (file-name-extension deadgrep--initial-filename)) + (format "*.%s" + (file-name-extension deadgrep--initial-filename))) + (t + "*"))) + (prompt + (if (string= initial-value "*") + ;; Show an example to avoid confusion with regexp syntax. + "Glob (e.g. *.js): " + "Glob: ")) + (glob + (read-from-minibuffer + prompt + initial-value))) + (setq deadgrep--file-type (cons 'glob glob)))) + (t + (error "Unknown button type: %S" button-type)))) + (deadgrep-restart)) + +(define-button-type 'deadgrep-directory + 'action #'deadgrep--directory + 'help-echo "Change base directory") + +(defun deadgrep--directory (_button) + (deadgrep-directory)) + +(defun deadgrep-directory () + "Prompt the user for a new search directory, then restart the search." + (interactive) + (setq default-directory + (expand-file-name + (read-directory-name "Search files in: "))) + (rename-buffer + (deadgrep--buffer-name deadgrep--search-term default-directory)) + (deadgrep-restart)) + +(defun deadgrep--button (text type &rest properties) + ;; `make-text-button' mutates the string to add properties, so copy + ;; TEXT first. + (setq text (substring-no-properties text)) + (apply #'make-text-button text nil :type type properties)) + +(defun deadgrep--arguments (search-term search-type case context) + "Return a list of command line arguments that we can execute in a shell +to obtain ripgrep results." + (let (args) + (push "--color=ansi" args) + (push "--line-number" args) + (push "--no-heading" args) + (push "--no-column" args) + (push "--with-filename" args) + + (cond + ((eq search-type 'string) + (push "--fixed-strings" args)) + ((eq search-type 'words) + (push "--fixed-strings" args) + (push "--word-regexp" args)) + ((eq search-type 'regexp)) + (t + (error "Unknown search type: %s" search-type))) + + (cond + ((eq case 'smart) + (push "--smart-case" args)) + ((eq case 'sensitive) + (push "--case-sensitive" args)) + ((eq case 'ignore) + (push "--ignore-case" args)) + (t + (error "Unknown case: %s" case))) + + (cond + ((eq deadgrep--file-type 'all)) + ((eq (car-safe deadgrep--file-type) 'type) + (push (format "--type=%s" (cdr deadgrep--file-type)) args)) + ((eq (car-safe deadgrep--file-type) 'glob) + (push (format "--type-add=custom:%s" (cdr deadgrep--file-type)) args) + (push "--type=custom" args)) + (t + (error "Unknown file-type: %S" deadgrep--file-type))) + + (when context + (push (format "--before-context=%s" (car context)) args) + (push (format "--after-context=%s" (cdr context)) args)) + + (push "--" args) + (push search-term args) + (push "." args) + + (nreverse args))) + +(defun deadgrep--write-heading () + "Write the deadgrep heading with buttons reflecting the current +search settings." + (let ((start-pos (point)) + (inhibit-read-only t)) + (insert (propertize "Search term: " + 'face 'deadgrep-meta-face) + (if (eq deadgrep--search-type 'regexp) + (deadgrep--propertize-regexp deadgrep--search-term) + (propertize + deadgrep--search-term + 'face 'deadgrep-search-term-face)) + " " + (deadgrep--button "change" 'deadgrep-search-term) + "\n" + (propertize "Search type: " + 'face 'deadgrep-meta-face) + + (if (eq deadgrep--search-type 'string) + "string" + (deadgrep--button "string" 'deadgrep-type + 'search-type 'string)) + " " + (if (eq deadgrep--search-type 'words) + "words" + (deadgrep--button "words" 'deadgrep-type + 'search-type 'words)) + " " + (if (eq deadgrep--search-type 'regexp) + "regexp" + (deadgrep--button "regexp" 'deadgrep-type + 'search-type 'regexp)) + "\n" + (propertize "Case: " + 'face 'deadgrep-meta-face) + (if (eq deadgrep--search-case 'smart) + "smart" + (deadgrep--button "smart" 'deadgrep-case + 'case 'smart)) + " " + (if (eq deadgrep--search-case 'sensitive) + "sensitive" + (deadgrep--button "sensitive" 'deadgrep-case + 'case 'sensitive)) + " " + (if (eq deadgrep--search-case 'ignore) + "ignore" + (deadgrep--button "ignore" 'deadgrep-case + 'case 'ignore)) + "\n" + (propertize "Context: " + 'face 'deadgrep-meta-face) + (if deadgrep--context + (deadgrep--button "none" 'deadgrep-context + 'context nil) + "none") + " " + (deadgrep--button "before" 'deadgrep-context + 'context 'before) + (if deadgrep--context + (format ":%d" (car deadgrep--context)) + "") + " " + (deadgrep--button "after" 'deadgrep-context + 'context 'after) + (if deadgrep--context + (format ":%d" (cdr deadgrep--context)) + "") + + "\n\n" + (propertize "Directory: " + 'face 'deadgrep-meta-face) + (deadgrep--button + (abbreviate-file-name default-directory) + 'deadgrep-directory) + (if (get-text-property 0 'deadgrep-overridden default-directory) + (propertize " (from override)" 'face 'deadgrep-meta-face) + "") + "\n" + (propertize "Files: " + 'face 'deadgrep-meta-face) + (if (eq deadgrep--file-type 'all) + "all" + (deadgrep--button "all" 'deadgrep-file-type + 'file-type 'all)) + " " + (deadgrep--button "type" 'deadgrep-file-type + 'file-type 'type) + (if (eq (car-safe deadgrep--file-type) 'type) + (format ":%s" (cdr deadgrep--file-type)) + "") + " " + (deadgrep--button "glob" 'deadgrep-file-type + 'file-type 'glob) + (if (eq (car-safe deadgrep--file-type) 'glob) + (format ":%s" (cdr deadgrep--file-type)) + "") + "\n\n") + (put-text-property + start-pos (point) + 'read-only t) + (put-text-property + start-pos (point) + 'front-sticky t))) + +;; TODO: could we do this in the minibuffer too? +(defun deadgrep--propertize-regexp (regexp) + "Given a string REGEXP representing a search term with regular +expression syntax, highlight the metacharacters. +Returns a copy of REGEXP with properties set." + (setq regexp (copy-sequence regexp)) + + ;; See https://docs.rs/regex/1.0.0/regex/#syntax + (let ((metachars + ;; Characters that don't match themselves. + '(?\( ?\) ?\[ ?\] ?\{ ?\} ?| ?. ?+ ?* ?? ?^ ?$)) + ;; Characters that have special regexp meaning when preceded + ;; with a backslash. This includes things like \b but not + ;; things like \n. + (escape-metachars + '(?A ?b ?B ?d ?D ?p ?s ?S ?w ?W ?z)) + (prev-char nil)) + ;; Put the standard search term face on every character + ;; individually. + (dotimes (i (length regexp)) + (put-text-property + i (1+ i) + 'face 'deadgrep-search-term-face + regexp)) + ;; Put the metacharacter face on any character that isn't treated + ;; literally. + (--each-indexed (string-to-list regexp) + (cond + ;; Highlight everything between { and }. + ((and (eq it ?\{) (not (equal prev-char ?\\))) + (let ((closing-pos it-index)) + ;; TODO: we have loops like this in several places, factor + ;; out. + (while (and (< closing-pos (length regexp)) + (not (eq (elt regexp closing-pos) + ?\}))) + (cl-incf closing-pos)) + ;; Step over the closing }, if we found one. + (unless (= closing-pos (length regexp)) + (cl-incf closing-pos)) + (put-text-property + it-index closing-pos + 'face + 'deadgrep-regexp-metachar-face + regexp))) + ;; Highlight individual metachars. + ((and (memq it metachars) (not (equal prev-char ?\\))) + (put-text-property + it-index (1+ it-index) + 'face + 'deadgrep-regexp-metachar-face + regexp)) + ((and (memq it escape-metachars) (equal prev-char ?\\)) + (put-text-property + (1- it-index) (1+ it-index) + 'face 'deadgrep-regexp-metachar-face + regexp))) + + (setq prev-char it))) + regexp) + +(defun deadgrep--buffer-name (search-term directory) + ;; TODO: Handle buffers already existing with this name. + (format "*deadgrep %s %s*" + (s-truncate 30 search-term) + (abbreviate-file-name directory))) + +(defun deadgrep--buffers () + "All the current deadgrep results buffers. +Returns a list ordered by the most recently accessed." + (--filter (with-current-buffer it + (eq major-mode 'deadgrep-mode)) + ;; `buffer-list' seems to be ordered by most recently + ;; visited first. + (buffer-list))) + +(defun deadgrep--buffer (search-term directory initial-filename) + "Create and initialise a search results buffer." + (let* ((buf-name (deadgrep--buffer-name search-term directory)) + (buf (get-buffer buf-name))) + (if buf + ;; There was already a buffer with this name. Reset its search + ;; state. + (with-current-buffer buf + (deadgrep--stop-and-reset)) + ;; We need to create the buffer, ensure we don't exceed + ;; `deadgrep-max-buffers' by killing the least recently used. + (progn + (when (numberp deadgrep-max-buffers) + (let* ((excess-buffers (-drop (1- deadgrep-max-buffers) + (deadgrep--buffers)))) + ;; Kill buffers so we have one buffer less than the maximum + ;; before we create a new one. + (-each excess-buffers #'kill-buffer))) + + (setq buf (get-buffer-create buf-name)))) + + (with-current-buffer buf + (setq default-directory directory) + (let ((inhibit-read-only t)) + ;; This needs to happen first, as it clobbers all buffer-local + ;; variables. + (deadgrep-mode) + (erase-buffer) + + (setq deadgrep--search-term search-term) + (setq deadgrep--current-file nil) + (setq deadgrep--initial-filename initial-filename)) + (setq buffer-read-only t)) + buf)) + +(defvar deadgrep-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "RET") #'deadgrep-visit-result) + (define-key map (kbd "o") #'deadgrep-visit-result-other-window) + ;; TODO: we should still be able to click on buttons. + + (define-key map (kbd "S") #'deadgrep-search-term) + (define-key map (kbd "D") #'deadgrep-directory) + (define-key map (kbd "g") #'deadgrep-restart) + + ;; TODO: this should work when point is anywhere in the file, not + ;; just on its heading. + (define-key map (kbd "TAB") #'deadgrep-toggle-file-results) + + ;; Keybinding chosen to match `kill-compilation'. + (define-key map (kbd "C-c C-k") #'deadgrep-kill-process) + + (define-key map (kbd "n") #'deadgrep-forward) + (define-key map (kbd "p") #'deadgrep-backward) + (define-key map (kbd "N") #'deadgrep-forward-match) + (define-key map (kbd "P") #'deadgrep-backward-match) + (define-key map (kbd "M-n") #'deadgrep-forward-filename) + (define-key map (kbd "M-p") #'deadgrep-backward-filename) + + map) + "Keymap for `deadgrep-mode'.") + +(defvar deadgrep-edit-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "RET") #'deadgrep-visit-result) + map) + "Keymap for `deadgrep-edit-mode'.") + +(define-derived-mode deadgrep-mode special-mode + '("Deadgrep" (:eval (spinner-print deadgrep--spinner))) + "Major mode for deadgrep results buffers." + (remove-hook 'after-change-functions #'deadgrep--propagate-change t)) + +(defun deadgrep--find-file (path) + "Open PATH in a buffer, and return a cons cell +\(BUF . OPENED). OPENED is nil if there was aleady a buffer for +this path." + (let* ((initial-buffers (buffer-list)) + (opened nil) + ;; Skip running find-file-hook since it may prompt the user. + (find-file-hook nil) + ;; If we end up opening a buffer, don't bother with file + ;; variables. It prompts the user, and we discard the buffer + ;; afterwards anyway. + (enable-local-variables nil) + ;; Bind `auto-mode-alist' to nil, so we open the buffer in + ;; `fundamental-mode' if it isn't already open. + (auto-mode-alist nil) + ;; Use `find-file-noselect' so we still decode bytes from the + ;; underlying file. + (buf (find-file-noselect path))) + (unless (-contains-p initial-buffers buf) + (setq opened t)) + (cons buf opened))) + +(defun deadgrep--propagate-change (beg end length) + "Repeat the last modification to the results buffer in the +underlying file." + ;; We should never be called outside an edit buffer, but be + ;; defensive. Buggy functions in change hooks are painful. + (when (eq major-mode 'deadgrep-edit-mode) + (save-mark-and-excursion + (goto-char beg) + (-let* ((column (+ (deadgrep--current-column) length)) + (filename (deadgrep--filename)) + (line-number (deadgrep--line-number)) + ((buf . opened) (deadgrep--find-file filename)) + (inserted (buffer-substring beg end))) + (with-current-buffer buf + (save-mark-and-excursion + (save-restriction + (widen) + (goto-char + (deadgrep--buffer-position line-number column)) + (delete-char (- length)) + (insert inserted))) + ;; If we weren't visiting this file before, just save it and + ;; close it. + (when opened + (basic-save-buffer) + (kill-buffer buf))))))) + +(defcustom deadgrep-edit-mode-hook nil + "Called after `deadgrep-edit-mode' is turned on." + :type 'hook + :group 'deadgrep) + +(defun deadgrep-edit-mode () + "Major mode for editing the results files directly from a +deadgrep results buffer. + +\\{deadgrep-edit-mode-map}" + (interactive) + (unless (eq major-mode 'deadgrep-mode) + (user-error "deadgrep-edit-mode only works in deadgrep result buffers")) + (when deadgrep--running + (user-error "Can't edit a results buffer until the search is finished")) + ;; We deliberately don't use `define-derived-mode' here because we + ;; want to check the previous value of `major-mode'. Initialise the + ;; major mode manually. + (delay-mode-hooks + (kill-all-local-variables) + (setq major-mode 'deadgrep-edit-mode) + (setq mode-name + '(:propertize "Deadgrep:Edit" face mode-line-emphasis)) + (use-local-map deadgrep-edit-mode-map) + ;; Done major mode manual initialise (copied from `define-derived-mode'). + + ;; Allow editing, and propagate changes. + (setq buffer-read-only nil) + (add-hook 'after-change-functions #'deadgrep--propagate-change nil t) + + (message "Now editing, use `M-x deadgrep-mode' when finished")) + + (run-mode-hooks 'deadgrep-edit-mode-hook)) + +(defun deadgrep--current-column () + "Get the current column position in char terms. +This treats tabs as 1 and ignores the line numbers in the results +buffer." + (let* ((line-start (line-beginning-position)) + (line-number + (get-text-property line-start 'deadgrep-line-number)) + (line-number-width + (max deadgrep--position-column-width + (length (number-to-string line-number)))) + (char-count 0)) + (save-excursion + (while (not (equal (point) line-start)) + (cl-incf char-count) + (backward-char 1))) + (max + (- char-count line-number-width) + 0))) + +(defun deadgrep--flash-column-offsets (start end) + "Temporarily highlight column offset from START to END." + (let* ((line-start (line-beginning-position)) + (overlay (make-overlay + (+ line-start start) + (+ line-start end)))) + (overlay-put overlay 'face 'highlight) + (run-with-timer 1.0 nil 'delete-overlay overlay))) + +(defun deadgrep--match-face-p (pos) + "Is there a match face at POS?" + (eq (get-text-property pos 'face) 'deadgrep-match-face)) + +(defun deadgrep--match-positions () + "Return a list of indexes of the current line's matches." + (let (positions) + (save-excursion + (beginning-of-line) + + (let* ((line-number + (get-text-property (point) 'deadgrep-line-number)) + (line-number-width + (max deadgrep--position-column-width + (length (number-to-string line-number)))) + (i 0) + (start-pos 0) + (line-end-pos (line-end-position))) + + (forward-char line-number-width) + + (while (<= (point) line-end-pos) + ;; If we've just entered a match, record the start position. + (when (and (deadgrep--match-face-p (point)) + (not (deadgrep--match-face-p (1- (point))))) + (setq start-pos i)) + ;; If we've just left a match, record the match range. + (when (and (not (deadgrep--match-face-p (point))) + (deadgrep--match-face-p (1- (point)))) + (push (list start-pos i) positions)) + + (setq i (1+ i)) + (forward-char 1)))) + + (nreverse positions))) + +(defun deadgrep--buffer-position (line-number column-offset) + "Return the position equivalent to LINE-NUMBER at COLUMN-OFFSET +in the current buffer." + (save-excursion + (save-restriction + (widen) + (goto-char (point-min)) + (forward-line (1- line-number)) + (forward-char column-offset) + + (point)))) + +(defun deadgrep--filename (&optional pos) + "Get the filename of the result at point POS. +If POS is nil, use the beginning position of the current line." + (get-text-property (or pos (line-beginning-position)) 'deadgrep-filename)) + +(defun deadgrep--line-number () + "Get the filename of the result at point." + (get-text-property (line-beginning-position) 'deadgrep-line-number)) + +(defun deadgrep--visit-result (open-fn) + "Goto the search result at point." + (interactive) + (let* ((pos (line-beginning-position)) + (file-name (deadgrep--filename)) + (line-number (deadgrep--line-number)) + (column-offset (when line-number (deadgrep--current-column))) + (match-positions (when line-number (deadgrep--match-positions)))) + (when file-name + (when overlay-arrow-position + (set-marker overlay-arrow-position nil)) + ;; Show an arrow next to the last result viewed. This is + ;; consistent with `compilation-next-error-function' and also + ;; useful with `deadgrep-visit-result-other-window'. + (setq overlay-arrow-position (copy-marker pos)) + + (funcall open-fn file-name) + (goto-char (point-min)) + + (when line-number + (-let [destination-pos (deadgrep--buffer-position + line-number column-offset)] + ;; Put point on the position of the match, widening the + ;; buffer if necessary. + (when (or (< destination-pos (point-min)) + (> destination-pos (point-max))) + (widen)) + (goto-char destination-pos) + + ;; Temporarily highlight the parts of the line that matched + ;; the search term. + (-each match-positions + (-lambda ((start end)) + (deadgrep--flash-column-offsets start end)))))))) + +(defun deadgrep-visit-result-other-window () + "Goto the search result at point, opening in another window." + (interactive) + (deadgrep--visit-result #'find-file-other-window)) + +(defun deadgrep-visit-result () + "Goto the search result at point." + (interactive) + (deadgrep--visit-result #'find-file)) + +(defvar-local deadgrep--hidden-files nil + "An alist recording which files currently have their lines +hidden in this deadgrep results buffer. + +Keys are interned filenames, so they compare with `eq'.") + +(defun deadgrep-toggle-file-results () + "Show/hide the results of the file at point." + (interactive) + (let* ((file-name (deadgrep--filename)) + (line-number (deadgrep--line-number))) + (when (and file-name (not line-number)) + ;; We're on a file heading. + (if (alist-get (intern file-name) deadgrep--hidden-files) + (deadgrep--show) + (deadgrep--hide))))) + +(defun deadgrep--show () + (-let* ((file-name (deadgrep--filename)) + ((start-pos end-pos) (alist-get (intern file-name) deadgrep--hidden-files))) + (remove-overlays start-pos end-pos 'invisible t) + (setf (alist-get (intern file-name) deadgrep--hidden-files) + nil))) + +(defun deadgrep--hide () + "Hide the file results immediately after point." + (save-excursion + (let* ((file-name (deadgrep--filename)) + (start-pos + (progn + (forward-line) + (point))) + (end-pos + (progn + (while (and + (get-text-property (point) 'deadgrep-line-number) + (not (bobp))) + (forward-line)) + ;; Step over the newline. + (1+ (point)))) + (o (make-overlay start-pos end-pos))) + (overlay-put o 'invisible t) + (setf (alist-get (intern file-name) deadgrep--hidden-files) + (list start-pos end-pos))))) + +(defun deadgrep--interrupt-process () + "Gracefully stop the rg process, synchronously." + (-when-let (proc (get-buffer-process (current-buffer))) + ;; Ensure that our process filter is not called again. + (set-process-filter proc #'ignore) + + (interrupt-process proc) + ;; Wait for the process to terminate, so we know that + ;; `deadgrep--process-sentinel' has been called. + (while (process-live-p proc) + ;; `redisplay' can trigger process filters or sentinels. + (redisplay) + (sleep-for 0.1)))) + +(defun deadgrep-kill-process () + "Kill the deadgrep process associated with the current buffer." + (interactive) + (if (get-buffer-process (current-buffer)) + (deadgrep--interrupt-process) + (message "No process running."))) + +(defun deadgrep--item-p (pos) + "Is there something at POS that we can interact with?" + (or (button-at pos) + (deadgrep--filename pos))) + +(defun deadgrep--filename-p (pos) + "Is there a filename at POS that we can interact with?" + (eq (get-text-property pos 'face) 'deadgrep-filename-face)) + +(defun deadgrep--move (forward-p) + "Move to the next item. +This will either be a button, a filename, or a search result." + (interactive) + (let ((pos (point))) + ;; If point is initially on an item, move past it. + (while (and (deadgrep--item-p pos) + (if forward-p + (< pos (point-max)) + (> pos (point-min)))) + (if forward-p + (cl-incf pos) + (cl-decf pos))) + ;; Find the next item. + (while (and (not (deadgrep--item-p pos)) + (if forward-p + (< pos (point-max)) + (> pos (point-min)))) + (if forward-p + (cl-incf pos) + (cl-decf pos))) + ;; Regardless of direction, ensure point is at the beginning of + ;; the item. + (while (and (if forward-p + (< pos (point-max)) + (> pos (point-min))) + (deadgrep--item-p (1- pos))) + (cl-decf pos)) + ;; If we reached an item (we aren't at the first/last item), then + ;; go to it. + (when (deadgrep--item-p pos) + (goto-char pos)))) + +(defun deadgrep-forward () + "Move forward to the next item. +This will either be a button, a filename, or a search result. See +also `deadgrep-forward-match'." + (interactive) + (deadgrep--move t)) + +(defun deadgrep-backward () + "Move backward to the previous item. +This will either be a button, a filename, or a search result. See +also `deadgrep-backward-match'." + (interactive) + (deadgrep--move nil)) + +(defun deadgrep-forward-filename () + "Move forward to the next filename." + (interactive) + (deadgrep--move-match t 'deadgrep-filename-face)) + +(defun deadgrep-backward-filename () + "Move backward to the previous filename." + (interactive) + (deadgrep--move-match nil 'deadgrep-filename-face)) + +(defun deadgrep--move-match (forward-p face) + "Move point to the beginning of the next/previous match." + (interactive) + (let ((start-pos (point))) + ;; Move over the current match, if we were already on one. + (while (eq (get-text-property (point) 'face) + face) + (if forward-p (forward-char) (backward-char))) + (condition-case nil + (progn + ;; Move point to the next match, which may be on the same line. + (while (not (eq (get-text-property (point) 'face) + face)) + (if forward-p (forward-char) (backward-char))) + ;; Ensure point is at the beginning of the match. + (unless forward-p + (while (eq (get-text-property (point) 'face) + face) + (backward-char)) + (forward-char))) + ;; Don't move point beyond the last match. However, it's still + ;; useful to signal that we're at the end, so users can use this + ;; command with macros and terminate when it's done. + (beginning-of-buffer + (goto-char start-pos) + (signal 'beginning-of-buffer nil)) + (end-of-buffer + (goto-char start-pos) + (signal 'end-of-buffer nil))))) + +(defun deadgrep-forward-match () + "Move point forward to the beginning of next match. +Note that a result line may contain more than one match, or zero +matches (if the result line has been truncated)." + (interactive) + (deadgrep--move-match t 'deadgrep-match-face)) + +(defun deadgrep-backward-match () + "Move point backward to the beginning of previous match." + (interactive) + (deadgrep--move-match nil 'deadgrep-match-face)) + +(defun deadgrep--start (search-term search-type case) + "Start a ripgrep search." + (setq deadgrep--spinner (spinner-create 'progress-bar t)) + (setq deadgrep--running t) + (spinner-start deadgrep--spinner) + (let* ((args (deadgrep--arguments + search-term search-type case + deadgrep--context)) + (process + (apply #'start-file-process + (format "rg %s" search-term) + (current-buffer) + deadgrep-executable + args))) + (setq deadgrep--debug-command + (format "%s %s" deadgrep-executable (s-join " " args))) + (set-process-filter process #'deadgrep--process-filter) + (set-process-sentinel process #'deadgrep--process-sentinel))) + +(defun deadgrep--stop-and-reset () + "Terminate the current search and reset any search state." + ;; Stop the old search, so we don't carry on inserting results from + ;; the last thing we searched for. + (deadgrep--interrupt-process) + + (let ((inhibit-read-only t)) + ;; Reset UI: remove results, reset items hidden by TAB, and arrow + ;; position. + (erase-buffer) + (setq deadgrep--hidden-files nil) + (when overlay-arrow-position + (set-marker overlay-arrow-position nil)) + + ;; Reset intermediate search state. + (setq deadgrep--current-file nil) + (setq deadgrep--spinner nil) + (setq deadgrep--remaining-output nil) + (setq deadgrep--current-file nil) + (setq deadgrep--debug-first-output nil) + (setq deadgrep--imenu-alist nil))) + +(defun deadgrep-restart () + "Re-run ripgrep with the current search settings." + (interactive) + ;; If we haven't started yet, start the search if we've been called + ;; by the user. + (when (and deadgrep--postpone-start + (called-interactively-p 'interactive)) + (setq deadgrep--postpone-start nil)) + + (deadgrep--stop-and-reset) + + (let ((start-point (point)) + (inhibit-read-only t)) + (deadgrep--write-heading) + ;; If the point was in the heading, ensure that we restore its + ;; position. + (goto-char (min (point-max) start-point)) + + (if deadgrep--postpone-start + (deadgrep--write-postponed) + (deadgrep--start + deadgrep--search-term + deadgrep--search-type + deadgrep--search-case)))) + +(defun deadgrep--read-search-term () + "Read a search term from the minibuffer. +If region is active, return that immediately. Otherwise, prompt +for a string, offering the current word as a default." + (let (search-term) + (if (use-region-p) + (progn + (setq search-term + (buffer-substring-no-properties (region-beginning) (region-end))) + (deactivate-mark)) + (let* ((sym (symbol-at-point)) + (sym-name (when sym + (substring-no-properties (symbol-name sym)))) + ;; TODO: prompt should say search string or search regexp + ;; as appropriate. + (prompt + (deadgrep--search-prompt sym-name))) + (setq search-term + (read-from-minibuffer + prompt nil nil nil 'deadgrep-history sym-name)) + (when (equal search-term "") + (setq search-term sym-name)))) + (unless (equal (car deadgrep-history) search-term) + (push search-term deadgrep-history)) + search-term)) + +(defun deadgrep--normalise-dirname (path) + "Expand PATH and ensure that it doesn't end with a slash. +If PATH is remote path, it is not expanded." + (directory-file-name (if (file-remote-p path) + path + (let (file-name-handler-alist) + (expand-file-name path))))) + +(defun deadgrep--lookup-override (path) + "If PATH is present in `deadgrep-project-root-overrides', +return the overridden value. +Otherwise, return PATH as is." + (let* ((normalised-path (deadgrep--normalise-dirname path)) + (override + (-first + (-lambda ((original . _)) + (equal (deadgrep--normalise-dirname original) normalised-path)) + deadgrep-project-root-overrides))) + (when override + (setq path (cdr override)) + (unless (stringp path) + (user-error "Bad override: expected a path string, but got: %S" path)) + (setq path (propertize path 'deadgrep-overridden t))) + path)) + +(defun deadgrep--project-root () + "Guess the project root of the given FILE-PATH." + (let ((root default-directory) + (project (project-current))) + (when project + (-when-let (roots (project-roots project)) + (setq root (car roots)))) + (when root + (deadgrep--lookup-override root)))) + +(defun deadgrep--write-postponed () + (let* ((inhibit-read-only t) + (restart-key + (where-is-internal #'deadgrep-restart deadgrep-mode-map t))) + (save-excursion + (goto-char (point-max)) + (insert + (format "Press %s to start the search." + (key-description restart-key)))))) + +(defun deadgrep--create-imenu-index () + "Create `imenu' index for matched files." + (when deadgrep--imenu-alist + (list (cons "Files" (reverse deadgrep--imenu-alist))))) + +;;;###autoload +(defun deadgrep (search-term &optional directory) + "Start a ripgrep search for SEARCH-TERM in DIRECTORY. + +If not provided, DIR defaults to the directory as determined by +`deadgrep-project-root-function'. + +See also `deadgrep-project-root-overrides'. + +If called with a prefix argument, create the results buffer but +don't actually start the search." + (interactive (list (deadgrep--read-search-term))) + (let* ((dir (or directory + (funcall deadgrep-project-root-function))) + (buf (deadgrep--buffer + search-term + dir + (or deadgrep--initial-filename + (buffer-file-name)))) + (last-results-buf (car-safe (deadgrep--buffers))) + prev-search-type + prev-search-case) + ;; Find out what search settings were used last time. + (when last-results-buf + (with-current-buffer last-results-buf + (setq prev-search-type deadgrep--search-type) + (setq prev-search-case deadgrep--search-case))) + + (funcall deadgrep-display-buffer-function buf) + + (with-current-buffer buf + (setq imenu-create-index-function #'deadgrep--create-imenu-index) + (setq next-error-function #'deadgrep-next-error) + + ;; If we have previous search settings, apply them to our new + ;; search results buffer. + (when last-results-buf + (setq deadgrep--search-type prev-search-type) + (setq deadgrep--search-case prev-search-case)) + + (deadgrep--write-heading) + + (if current-prefix-arg + ;; Don't start the search, just create the buffer and inform + ;; the user how to start when they're ready. + (progn + (setq deadgrep--postpone-start t) + (deadgrep--write-postponed)) + ;; Start the search immediately. + (deadgrep--start + search-term + deadgrep--search-type + deadgrep--search-case))))) + +(defun deadgrep-next-error (arg reset) + "Move to the next error. +If ARG is given, move by that many errors. + +This is intended for use with `next-error-function', which see." + (when reset + (goto-char (point-min))) + (beginning-of-line) + (let ((direction (> arg 0))) + (setq arg (abs arg)) + + (while (and + (not (zerop arg)) + (not (eobp))) + (if direction + (forward-line 1) + (forward-line -1)) + ;; If we are on a specific result (not a heading), we have a line + ;; number. + (when (get-text-property (point) 'deadgrep-line-number) + (cl-decf arg)))) + (deadgrep-visit-result-other-window)) + +(defun deadgrep-debug () + "Show a buffer with some debug information about the current search." + (interactive) + (unless (eq major-mode 'deadgrep-mode) + (user-error "deadgrep-debug should be run in a deadgrep results buffer")) + + (let ((command deadgrep--debug-command) + (output deadgrep--debug-first-output) + (buf (get-buffer-create "*deadgrep debug*")) + (inhibit-read-only t)) + (pop-to-buffer buf) + (erase-buffer) + (special-mode) + (setq buffer-read-only t) + + (insert + (propertize + "About your environment:\n" + 'face 'deadgrep-filename-face) + (format "Platform: %s\n" system-type) + (format "Emacs version: %s\n" emacs-version) + (format "Command: %s\n" command) + (format "default-directory: %S\n" default-directory) + (format "exec-path: %S\n" exec-path) + (if (boundp 'tramp-remote-path) + (format "tramp-remote-path: %S\n" tramp-remote-path) + "") + (propertize + "\nInitial output from ripgrep:\n" + 'face 'deadgrep-filename-face) + (format "%S" output) + (propertize + "\n\nPlease file bugs at https://github.com/Wilfred/deadgrep/issues/new" + 'face 'deadgrep-filename-face)))) + +(defun deadgrep-kill-all-buffers () + "Kill all open deadgrep buffers." + (interactive) + (dolist (buffer (deadgrep--buffers)) + (kill-buffer buffer))) + +(provide 'deadgrep) +;;; deadgrep.el ends here diff --git a/elpa/deadgrep-20220209.719/deadgrep.elc b/elpa/deadgrep-20220209.719/deadgrep.elc Binary files differ. diff --git a/elpa/spinner-1.7.4.signed b/elpa/spinner-1.7.4.signed @@ -0,0 +1 @@ +Good signature from 066DAFCB81E42C40 GNU ELPA Signing Agent (2019) <elpasign@elpa.gnu.org> (trust undefined) created at 2021-07-02T11:10:02+0200 using RSA +\ No newline at end of file diff --git a/elpa/spinner-1.7.4/README.org b/elpa/spinner-1.7.4/README.org @@ -0,0 +1,76 @@ +#+TITLE: spinner.el + +Add spinners and progress-bars to the mode-line for ongoing operations. + +[[file:some-spinners.gif]] + +[[file:all-spinners.gif]] + +* Usage + +First of all, don’t forget to add ~(spinner "VERSION")~ to your package’s dependencies. + +** Major-modes +1. Just call ~(spinner-start)~ and a spinner will be added to the mode-line. +2. Call ~(spinner-stop)~ on the same buffer when you want to remove it. + +The default spinner is a line drawing that rotates. You can pass an +argument to ~spinner-start~ to specify which spinner you want. All +possibilities are listed in the ~spinner-types~ variable, but here are +a few examples for you to try: + +- ~(spinner-start 'vertical-breathing 10)~ +- ~(spinner-start 'minibox)~ +- ~(spinner-start 'moon)~ +- ~(spinner-start 'triangle)~ + +You can also define your own as a vector of strings (see the examples +in ~spinner-types~). + +** Minor-modes +Minor-modes can create a spinner with ~spinner-create~ and then add it +to their mode-line lighter. They can then start the spinner by setting +a variable and calling ~spinner-start-timer~. Finally, they can stop +the spinner (and the timer) by just setting the same variable to nil. + +Here’s an example for a minor-mode named ~foo~. Assuming that +~foo--lighter~ is used as the mode-line lighter, the following code +will add an *inactive* global spinner to the mode-line. +#+begin_src emacs-lisp +(defvar foo--spinner (spinner-create 'rotating-line)) +(defconst foo--lighter + '(" foo" (:eval (spinner-print foo--spinner)))) +#+end_src + +1. To activate the spinner, just call ~(spinner-start foo--spinner)~. + It will show up on the mode-line and start animating. +2. To get rid of it, call ~(spinner-stop foo--spinner)~. It will then + disappear again. + +Some minor-modes will need spinners to be buffer-local. To achieve +that, just make the ~foo--spinner~ variable buffer-local and use the +third argument of the ~spinner-create~ function. The snippet below is an example. + +#+begin_src emacs-lisp +(defvar-local foo--spinner nil) +(defconst foo--lighter + '(" foo" (:eval (spinner-print foo--spinner)))) +(defun foo--start-spinner () + "Create and start a spinner on this buffer." + (unless foo--spinner + (setq foo--spinner (spinner-create 'moon t))) + (spinner-start foo--spinner)) +#+end_src + +1. To activate the spinner, just call ~(foo--start-spinner)~. +2. To get rid of it, call ~(spinner-stop foo--spinner)~. + +This will use the ~moon~ spinner, but you can use any of the names +defined in the ~spinner-types~ variable or even define your own. + +* Extra options + +Both ~spinner-start~ and ~spinner-create~ take extra options to configure the spinner, these are: + +- ~FPS~: The number of frames to display per second. Defaults to ~spinner-frames-per-second~. +- ~DELAY~: After starting a spinner, it still won’t be displayed for this many seconds. diff --git a/elpa/spinner-1.7.4/all-spinners.gif b/elpa/spinner-1.7.4/all-spinners.gif Binary files differ. diff --git a/elpa/spinner-1.7.4/some-spinners.gif b/elpa/spinner-1.7.4/some-spinners.gif Binary files differ. diff --git a/elpa/spinner-1.7.4/spinner-autoloads.el b/elpa/spinner-1.7.4/spinner-autoloads.el @@ -0,0 +1,77 @@ +;;; spinner-autoloads.el --- automatically extracted autoloads -*- lexical-binding: t -*- +;; +;;; Code: + +(add-to-list 'load-path (directory-file-name + (or (file-name-directory #$) (car load-path)))) + + +;;;### (autoloads nil "spinner" "spinner.el" (0 0 0 0)) +;;; Generated autoloads from spinner.el + +(autoload 'spinner-create "spinner" "\ +Create a spinner of the given TYPE. +The possible TYPEs are described in `spinner--type-to-frames'. + +FPS, if given, is the number of desired frames per second. +Default is `spinner-frames-per-second'. + +If BUFFER-LOCAL is non-nil, the spinner will be automatically +deactivated if the buffer is killed. If BUFFER-LOCAL is a +buffer, use that instead of current buffer. + +When started, in order to function properly, the spinner runs a +timer which periodically calls `force-mode-line-update' in the +current buffer. If BUFFER-LOCAL was set at creation time, then +`force-mode-line-update' is called in that buffer instead. When +the spinner is stopped, the timer is deactivated. + +DELAY, if given, is the number of seconds to wait after starting +the spinner before actually displaying it. It is safe to cancel +the spinner before this time, in which case it won't display at +all. + +\(fn &optional TYPE BUFFER-LOCAL FPS DELAY)" nil nil) + +(autoload 'spinner-start "spinner" "\ +Start a mode-line spinner of given TYPE-OR-OBJECT. +If TYPE-OR-OBJECT is an object created with `make-spinner', +simply activate it. This method is designed for minor modes, so +they can use the spinner as part of their lighter by doing: + '(:eval (spinner-print THE-SPINNER)) +To stop this spinner, call `spinner-stop' on it. + +If TYPE-OR-OBJECT is anything else, a buffer-local spinner is +created with this type, and it is displayed in the +`mode-line-process' of the buffer it was created it. Both +TYPE-OR-OBJECT and FPS are passed to `make-spinner' (which see). +To stop this spinner, call `spinner-stop' in the same buffer. + +Either way, the return value is a function which can be called +anywhere to stop this spinner. You can also call `spinner-stop' +in the same buffer where the spinner was created. + +FPS, if given, is the number of desired frames per second. +Default is `spinner-frames-per-second'. + +DELAY, if given, is the number of seconds to wait until actually +displaying the spinner. It is safe to cancel the spinner before +this time, in which case it won't display at all. + +\(fn &optional TYPE-OR-OBJECT FPS DELAY)" nil nil) + +(register-definition-prefixes "spinner" '("spinner-")) + +;;;*** + +;;;### (autoloads nil nil ("spinner-pkg.el") (0 0 0 0)) + +;;;*** + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; coding: utf-8-emacs-unix +;; End: +;;; spinner-autoloads.el ends here diff --git a/elpa/spinner-1.7.4/spinner-pkg.el b/elpa/spinner-1.7.4/spinner-pkg.el @@ -0,0 +1,2 @@ +;; Generated package description from spinner.el -*- no-byte-compile: t -*- +(define-package "spinner" "1.7.4" "Add spinners and progress-bars to the mode-line for ongoing operations" '((emacs "24.3")) :keywords '("processes" "mode-line") :authors '(("Artur Malabarba" . "emacs@endlessparentheses.com")) :maintainer '("Artur Malabarba" . "emacs@endlessparentheses.com") :url "https://github.com/Malabarba/spinner.el") diff --git a/elpa/spinner-1.7.4/spinner.el b/elpa/spinner-1.7.4/spinner.el @@ -0,0 +1,340 @@ +;;; spinner.el --- Add spinners and progress-bars to the mode-line for ongoing operations -*- lexical-binding: t; -*- + +;; Copyright (C) 2015 Free Software Foundation, Inc. + +;; Author: Artur Malabarba <emacs@endlessparentheses.com> +;; Version: 1.7.4 +;; Package-Requires: ((emacs "24.3")) +;; URL: https://github.com/Malabarba/spinner.el +;; Keywords: processes mode-line + +;; 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: +;; +;; 1 Usage +;; ═══════ +;; +;; First of all, don’t forget to add `(spinner "VERSION")' to your +;; package’s dependencies. +;; +;; +;; 1.1 Major-modes +;; ─────────────── +;; +;; 1. Just call `(spinner-start)' and a spinner will be added to the +;; mode-line. +;; 2. Call `(spinner-stop)' on the same buffer when you want to remove +;; it. +;; +;; The default spinner is a line drawing that rotates. You can pass an +;; argument to `spinner-start' to specify which spinner you want. All +;; possibilities are listed in the `spinner-types' variable, but here are +;; a few examples for you to try: +;; +;; • `(spinner-start 'vertical-breathing 10)' +;; • `(spinner-start 'minibox)' +;; • `(spinner-start 'moon)' +;; • `(spinner-start 'triangle)' +;; +;; You can also define your own as a vector of strings (see the examples +;; in `spinner-types'). +;; +;; +;; 1.2 Minor-modes +;; ─────────────── +;; +;; Minor-modes can create a spinner with `spinner-create' and then add it +;; to their mode-line lighter. They can then start the spinner by setting +;; a variable and calling `spinner-start-timer'. Finally, they can stop +;; the spinner (and the timer) by just setting the same variable to nil. +;; +;; Here’s an example for a minor-mode named `foo'. Assuming that +;; `foo--lighter' is used as the mode-line lighter, the following code +;; will add an *inactive* global spinner to the mode-line. +;; ┌──── +;; │ (defvar foo--spinner (spinner-create 'rotating-line)) +;; │ (defconst foo--lighter +;; │ '(" foo" (:eval (spinner-print foo--spinner)))) +;; └──── +;; +;; 1. To activate the spinner, just call `(spinner-start foo--spinner)'. +;; It will show up on the mode-line and start animating. +;; 2. To get rid of it, call `(spinner-stop foo--spinner)'. It will then +;; disappear again. +;; +;; Some minor-modes will need spinners to be buffer-local. To achieve +;; that, just make the `foo--spinner' variable buffer-local and use the +;; third argument of the `spinner-create' function. The snippet below is an +;; example. +;; +;; ┌──── +;; │ (defvar-local foo--spinner nil) +;; │ (defconst foo--lighter +;; │ '(" foo" (:eval (spinner-print foo--spinner)))) +;; │ (defun foo--start-spinner () +;; │ "Create and start a spinner on this buffer." +;; │ (unless foo--spinner +;; │ (setq foo--spinner (spinner-create 'moon t))) +;; │ (spinner-start foo--spinner)) +;; └──── +;; +;; 1. To activate the spinner, just call `(foo--start-spinner)'. +;; 2. To get rid of it, call `(spinner-stop foo--spinner)'. +;; +;; This will use the `moon' spinner, but you can use any of the names +;; defined in the `spinner-types' variable or even define your own. + + +;;; Code: +(eval-when-compile + (require 'cl-lib)) + +(defconst spinner-types + '((3-line-clock . ["┤" "┘" "┴" "└" "├" "┌" "┬" "┐"]) + (2-line-clock . ["┘" "└" "┌" "┐"]) + (flipping-line . ["_" "\\" "|" "/"]) + (rotating-line . ["-" "\\" "|" "/"]) + (progress-bar . ["[ ]" "[= ]" "[== ]" "[=== ]" "[====]" "[ ===]" "[ ==]" "[ =]"]) + (progress-bar-filled . ["| |" "|█ |" "|██ |" "|███ |" "|████|" "| ███|" "| ██|" "| █|"]) + (vertical-breathing . ["▁" "▂" "▃" "▄" "▅" "▆" "▇" "█" "▇" "▆" "▅" "▄" "▃" "▂" "▁" " "]) + (vertical-rising . ["▁" "▄" "█" "▀" "▔"]) + (horizontal-breathing . [" " "▏" "▎" "▍" "▌" "▋" "▊" "▉" "▉" "▊" "▋" "▌" "▍" "▎" "▏"]) + (horizontal-breathing-long + . [" " "▎ " "▌ " "▊ " "█ " "█▎" "█▌" "█▊" "██" "█▊" "█▌" "█▎" "█ " "▊ " "▋ " "▌ " "▍ " "▎ " "▏ "]) + (horizontal-moving . [" " "▌ " "█ " "▐▌" " █" " ▐"]) + (minibox . ["▖" "▘" "▝" "▗"]) + (triangle . ["◢" "◣" "◤" "◥"]) + (box-in-box . ["◰" "◳" "◲" "◱"]) + (box-in-circle . ["◴" "◷" "◶" "◵"]) + (half-circle . ["◐" "◓" "◑" "◒"]) + (moon . ["🌑" "🌘" "🌗" "🌖" "🌕" "🌔" "🌓" "🌒"])) + "Predefined alist of spinners. +Each car is a symbol identifying the spinner, and each cdr is a +vector, the spinner itself.") + +(defun spinner-make-progress-bar (width &optional char) + "Return a vector of strings of the given WIDTH. +The vector is a valid spinner type and is similar to the +`progress-bar' spinner, except without the surrounding brackets. +CHAR is the character to use for the moving bar (defaults to =)." + (let ((whole-string (concat (make-string (1- width) ?\s) + (make-string 4 (or char ?=)) + (make-string width ?\s)))) + (apply #'vector (mapcar (lambda (n) (substring whole-string n (+ n width))) + (number-sequence (+ width 3) 0 -1))))) + +(defvar spinner-current nil + "Spinner currently being displayed on the `mode-line-process'.") +(make-variable-buffer-local 'spinner-current) + +(defconst spinner--mode-line-construct + '(:eval (spinner-print spinner-current)) + "Construct used to display a spinner in `mode-line-process'.") +(put 'spinner--mode-line-construct 'risky-local-variable t) + +(defvar spinner-frames-per-second 10 + "Default speed at which spinners spin, in frames per second. +Each spinner can override this value.") + + +;;; The spinner object. +(defun spinner--type-to-frames (type) + "Return a vector of frames corresponding to TYPE. +The list of possible built-in spinner types is given by the +`spinner-types' variable, but you can also use your own (see +below). + +If TYPE is nil, the frames of this spinner are given by the first +element of `spinner-types'. +If TYPE is a symbol, it specifies an element of `spinner-types'. +If TYPE is 'random, use a random element of `spinner-types'. +If TYPE is a list, it should be a list of symbols, and a random +one is chosen as the spinner type. +If TYPE is a vector, it should be a vector of strings and these +are used as the spinner's frames. This allows you to make your +own spinner animations." + (cond + ((vectorp type) type) + ((not type) (cdr (car spinner-types))) + ((eq type 'random) + (cdr (elt spinner-types + (random (length spinner-types))))) + ((listp type) + (cdr (assq (elt type (random (length type))) + spinner-types))) + ((symbolp type) (cdr (assq type spinner-types))) + (t (error "Unknown spinner type: %s" type)))) + +(cl-defstruct (spinner + (:copier nil) + (:conc-name spinner--) + (:constructor make-spinner (&optional type buffer-local frames-per-second delay-before-start))) + (frames (spinner--type-to-frames type)) + (counter 0) + (fps (or frames-per-second spinner-frames-per-second)) + (timer (timer-create)) + (active-p nil) + (buffer (when buffer-local + (if (bufferp buffer-local) + buffer-local + (current-buffer)))) + (delay (or delay-before-start 0))) + +;;;###autoload +(defun spinner-create (&optional type buffer-local fps delay) + "Create a spinner of the given TYPE. +The possible TYPEs are described in `spinner--type-to-frames'. + +FPS, if given, is the number of desired frames per second. +Default is `spinner-frames-per-second'. + +If BUFFER-LOCAL is non-nil, the spinner will be automatically +deactivated if the buffer is killed. If BUFFER-LOCAL is a +buffer, use that instead of current buffer. + +When started, in order to function properly, the spinner runs a +timer which periodically calls `force-mode-line-update' in the +current buffer. If BUFFER-LOCAL was set at creation time, then +`force-mode-line-update' is called in that buffer instead. When +the spinner is stopped, the timer is deactivated. + +DELAY, if given, is the number of seconds to wait after starting +the spinner before actually displaying it. It is safe to cancel +the spinner before this time, in which case it won't display at +all." + (make-spinner type buffer-local fps delay)) + +(defun spinner-print (spinner) + "Return a string of the current frame of SPINNER. +If SPINNER is nil, just return nil. +Designed to be used in the mode-line with: + (:eval (spinner-print some-spinner))" + (when (and spinner (spinner--active-p spinner)) + (let ((frame (spinner--counter spinner))) + (when (>= frame 0) + (elt (spinner--frames spinner) frame))))) + +(defun spinner--timer-function (spinner) + "Function called to update SPINNER. +If SPINNER is no longer active, or if its buffer has been killed, +stop the SPINNER's timer." + (let ((buffer (spinner--buffer spinner))) + (if (or (not (spinner--active-p spinner)) + (and buffer (not (buffer-live-p buffer)))) + (spinner-stop spinner) + ;; Increment + (cl-callf (lambda (x) (if (< x 0) + (1+ x) + (% (1+ x) (length (spinner--frames spinner))))) + (spinner--counter spinner)) + ;; Update mode-line. + (if (buffer-live-p buffer) + (with-current-buffer buffer + (force-mode-line-update)) + (force-mode-line-update))))) + +(defun spinner--start-timer (spinner) + "Start a SPINNER's timer." + (let ((old-timer (spinner--timer spinner))) + (when (timerp old-timer) + (cancel-timer old-timer)) + + (setf (spinner--active-p spinner) t) + + (unless (ignore-errors (> (spinner--fps spinner) 0)) + (error "A spinner's FPS must be a positive number")) + (setf (spinner--counter spinner) + (round (- (* (or (spinner--delay spinner) 0) + (spinner--fps spinner))))) + ;; Create timer. + (let* ((repeat (/ 1.0 (spinner--fps spinner))) + (time (timer-next-integral-multiple-of-time (current-time) repeat)) + ;; Create the timer as a lex variable so it can cancel itself. + (timer (spinner--timer spinner))) + (timer-set-time timer time repeat) + (timer-set-function timer #'spinner--timer-function (list spinner)) + (timer-activate timer) + ;; Return a stopping function. + (lambda () (spinner-stop spinner))))) + + +;;; The main functions +;;;###autoload +(defun spinner-start (&optional type-or-object fps delay) + "Start a mode-line spinner of given TYPE-OR-OBJECT. +If TYPE-OR-OBJECT is an object created with `make-spinner', +simply activate it. This method is designed for minor modes, so +they can use the spinner as part of their lighter by doing: + '(:eval (spinner-print THE-SPINNER)) +To stop this spinner, call `spinner-stop' on it. + +If TYPE-OR-OBJECT is anything else, a buffer-local spinner is +created with this type, and it is displayed in the +`mode-line-process' of the buffer it was created it. Both +TYPE-OR-OBJECT and FPS are passed to `make-spinner' (which see). +To stop this spinner, call `spinner-stop' in the same buffer. + +Either way, the return value is a function which can be called +anywhere to stop this spinner. You can also call `spinner-stop' +in the same buffer where the spinner was created. + +FPS, if given, is the number of desired frames per second. +Default is `spinner-frames-per-second'. + +DELAY, if given, is the number of seconds to wait until actually +displaying the spinner. It is safe to cancel the spinner before +this time, in which case it won't display at all." + (unless (spinner-p type-or-object) + ;; Choose type. + (if (spinner-p spinner-current) + (setf (spinner--frames spinner-current) (spinner--type-to-frames type-or-object)) + (setq spinner-current (make-spinner type-or-object (current-buffer) fps delay))) + (setq type-or-object spinner-current) + ;; Maybe add to mode-line. + (unless (and (listp mode-line-process) + (memq 'spinner--mode-line-construct mode-line-process)) + (setq mode-line-process + (list (or mode-line-process "") + 'spinner--mode-line-construct)))) + + ;; Create timer. + (when fps (setf (spinner--fps type-or-object) fps)) + (when delay (setf (spinner--delay type-or-object) delay)) + (spinner--start-timer type-or-object)) + +(defun spinner-start-print (spinner) + "Like `spinner-print', but also start SPINNER if it's not active." + (unless (spinner--active-p spinner) + (spinner-start spinner)) + (spinner-print spinner)) + +(defun spinner-stop (&optional spinner) + "Stop SPINNER, defaulting to the current buffer's spinner. +It is always safe to call this function, even if there is no +active spinner." + (let ((spinner (or spinner spinner-current))) + (when (spinner-p spinner) + (let ((timer (spinner--timer spinner))) + (when (timerp timer) + (cancel-timer timer))) + (setf (spinner--active-p spinner) nil) + (force-mode-line-update)))) + +(provide 'spinner) + +;; Local Variables: +;; indent-tabs-mode: nil +;; End: +;;; spinner.el ends here diff --git a/elpa/spinner-1.7.4/spinner.elc b/elpa/spinner-1.7.4/spinner.elc Binary files differ. diff --git a/init.el b/init.el @@ -126,6 +126,8 @@ ("C-h v" . helpful-variable) ("C-h k" . helpful-key) ("C-c C-d" . helpful-at-point))) +(lh/global-set-keys + (("M-s d" . deadgrep))) (lh/define-keys isearch-mode-map (("M-e" . consult-isearch) @@ -199,7 +201,7 @@ ("melpa-stable" . "https://stable.melpa.org/packages/") ("melpa" . "https://melpa.org/packages/"))) '(package-selected-packages - '(helpful ob-http pdf-tools paredit-menu paredit vertico-posframe vertico corfu sly eglot aggressive-indent project nov nhexl-mode elfeed magit yaml-mode json-mode lua-mode go-mode geiser-guile geiser org-roam org-contrib org ace-window expand-region consult marginalia uuidgen request diminish which-key)) + '(deadgrep helpful ob-http pdf-tools paredit-menu paredit vertico-posframe vertico corfu sly eglot aggressive-indent project nov nhexl-mode elfeed magit yaml-mode json-mode lua-mode go-mode geiser-guile geiser org-roam org-contrib org ace-window expand-region consult marginalia uuidgen request diminish which-key)) '(pcomplete-ignore-case t t) '(read-buffer-completion-ignore-case t) '(read-file-name-completion-ignore-case t)