1041 lines
38 KiB
EmacsLisp
1041 lines
38 KiB
EmacsLisp
|
;;; swiper.el --- Isearch with an overview. Oh, man! -*- lexical-binding: t -*-
|
||
|
|
||
|
;; Copyright (C) 2015-2018 Free Software Foundation, Inc.
|
||
|
|
||
|
;; Author: Oleh Krehel <ohwoeowho@gmail.com>
|
||
|
;; URL: https://github.com/abo-abo/swiper
|
||
|
;; Package-Version: 20180713.946
|
||
|
;; Version: 0.10.0
|
||
|
;; Package-Requires: ((emacs "24.1") (ivy "0.9.0"))
|
||
|
;; Keywords: matching
|
||
|
|
||
|
;; This file is part of GNU Emacs.
|
||
|
|
||
|
;; This file 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, 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.
|
||
|
|
||
|
;; For a full copy of the GNU General Public License
|
||
|
;; see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
;;; Commentary:
|
||
|
|
||
|
;; This package gives an overview of the current regex search
|
||
|
;; candidates. The search regex can be split into groups with a
|
||
|
;; space. Each group is highlighted with a different face.
|
||
|
;;
|
||
|
;; It can double as a quick `regex-builder', although only single
|
||
|
;; lines will be matched.
|
||
|
|
||
|
;;; Code:
|
||
|
|
||
|
(require 'ivy)
|
||
|
|
||
|
(defgroup swiper nil
|
||
|
"`isearch' with an overview."
|
||
|
:group 'matching
|
||
|
:prefix "swiper-")
|
||
|
|
||
|
(defface swiper-match-face-1
|
||
|
'((t (:inherit lazy-highlight)))
|
||
|
"The background face for `swiper' matches.")
|
||
|
|
||
|
(defface swiper-match-face-2
|
||
|
'((t (:inherit isearch)))
|
||
|
"Face for `swiper' matches modulo 1.")
|
||
|
|
||
|
(defface swiper-match-face-3
|
||
|
'((t (:inherit match)))
|
||
|
"Face for `swiper' matches modulo 2.")
|
||
|
|
||
|
(defface swiper-match-face-4
|
||
|
'((t (:inherit isearch-fail)))
|
||
|
"Face for `swiper' matches modulo 3.")
|
||
|
|
||
|
(defface swiper-line-face
|
||
|
'((t (:inherit highlight)))
|
||
|
"Face for current `swiper' line.")
|
||
|
|
||
|
(defcustom swiper-faces '(swiper-match-face-1
|
||
|
swiper-match-face-2
|
||
|
swiper-match-face-3
|
||
|
swiper-match-face-4)
|
||
|
"List of `swiper' faces for group matches."
|
||
|
:group 'ivy-faces
|
||
|
:type '(repeat face))
|
||
|
|
||
|
(defcustom swiper-min-highlight 2
|
||
|
"Only highlight matches for regexps at least this long."
|
||
|
:type 'integer)
|
||
|
|
||
|
(defcustom swiper-include-line-number-in-search nil
|
||
|
"Include line number in text of search candidates."
|
||
|
:type 'boolean
|
||
|
:group 'swiper)
|
||
|
|
||
|
(defcustom swiper-goto-start-of-match nil
|
||
|
"When non-nil, go to the start of the match, not its end."
|
||
|
:type 'boolean
|
||
|
:group 'swiper)
|
||
|
|
||
|
(defvar swiper-map
|
||
|
(let ((map (make-sparse-keymap)))
|
||
|
(define-key map (kbd "M-q") 'swiper-query-replace)
|
||
|
(define-key map (kbd "C-l") 'swiper-recenter-top-bottom)
|
||
|
(define-key map (kbd "C-'") 'swiper-avy)
|
||
|
(define-key map (kbd "C-7") 'swiper-mc)
|
||
|
(define-key map (kbd "C-c C-f") 'swiper-toggle-face-matching)
|
||
|
map)
|
||
|
"Keymap for swiper.")
|
||
|
|
||
|
(defun swiper-query-replace ()
|
||
|
"Start `query-replace' with string to replace from last search string."
|
||
|
(interactive)
|
||
|
(if (null (window-minibuffer-p))
|
||
|
(user-error "Should only be called in the minibuffer through `swiper-map'")
|
||
|
(let* ((enable-recursive-minibuffers t)
|
||
|
(from (ivy--regex ivy-text))
|
||
|
(to (minibuffer-with-setup-hook
|
||
|
(lambda ()
|
||
|
(setq minibuffer-default
|
||
|
(if (string-match "\\`\\\\_<\\(.*\\)\\\\_>\\'" ivy-text)
|
||
|
(match-string 1 ivy-text)
|
||
|
ivy-text)))
|
||
|
(read-from-minibuffer (format "Query replace %s with: " from)))))
|
||
|
(swiper--cleanup)
|
||
|
(ivy-exit-with-action
|
||
|
(lambda (_)
|
||
|
(with-ivy-window
|
||
|
(move-beginning-of-line 1)
|
||
|
(let ((inhibit-read-only t))
|
||
|
(perform-replace from to
|
||
|
t t nil))))))))
|
||
|
|
||
|
(defun swiper-all-query-replace ()
|
||
|
"Start `query-replace' with string to replace from last search string."
|
||
|
(interactive)
|
||
|
(if (null (window-minibuffer-p))
|
||
|
(user-error
|
||
|
"Should only be called in the minibuffer through `swiper-all-map'")
|
||
|
(let* ((enable-recursive-minibuffers t)
|
||
|
(from (ivy--regex ivy-text))
|
||
|
(to (query-replace-read-to from "Query replace" t)))
|
||
|
(swiper--cleanup)
|
||
|
(ivy-exit-with-action
|
||
|
(lambda (_)
|
||
|
(let ((wnd-conf (current-window-configuration))
|
||
|
(inhibit-message t))
|
||
|
(unwind-protect
|
||
|
(dolist (cand ivy--old-cands)
|
||
|
(let ((buffer (get-text-property 0 'buffer cand)))
|
||
|
(switch-to-buffer buffer)
|
||
|
(goto-char (point-min))
|
||
|
(perform-replace from to t t nil)))
|
||
|
(set-window-configuration wnd-conf))))))))
|
||
|
|
||
|
(defvar avy-background)
|
||
|
(defvar avy-all-windows)
|
||
|
(defvar avy-style)
|
||
|
(defvar avy-keys)
|
||
|
(declare-function avy--regex-candidates "ext:avy")
|
||
|
(declare-function avy--process "ext:avy")
|
||
|
(declare-function avy--overlay-post "ext:avy")
|
||
|
(declare-function avy-action-goto "ext:avy")
|
||
|
(declare-function avy-candidate-beg "ext:avy")
|
||
|
(declare-function avy--done "ext:avy")
|
||
|
(declare-function avy--make-backgrounds "ext:avy")
|
||
|
(declare-function avy-window-list "ext:avy")
|
||
|
(declare-function avy-read "ext:avy")
|
||
|
(declare-function avy-read-de-bruijn "ext:avy")
|
||
|
(declare-function avy-tree "ext:avy")
|
||
|
(declare-function avy-push-mark "ext:avy")
|
||
|
(declare-function avy--remove-leading-chars "ext:avy")
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun swiper-avy ()
|
||
|
"Jump to one of the current swiper candidates."
|
||
|
(interactive)
|
||
|
(unless (require 'avy nil 'noerror)
|
||
|
(error "Package avy isn't installed"))
|
||
|
(unless (string= ivy-text "")
|
||
|
(let* ((avy-all-windows nil)
|
||
|
;; We'll have overlapping overlays, so we sort all the
|
||
|
;; overlays in the visible region by their start, and then
|
||
|
;; throw out non-Swiper overlays or overlapping Swiper
|
||
|
;; overlays.
|
||
|
(visible-overlays (cl-sort (with-ivy-window
|
||
|
(overlays-in (window-start)
|
||
|
(window-end)))
|
||
|
#'< :key #'overlay-start))
|
||
|
(min-overlay-start 0)
|
||
|
(overlays-for-avy (cl-remove-if-not
|
||
|
(lambda (ov)
|
||
|
(when (and (>= (overlay-start ov)
|
||
|
min-overlay-start)
|
||
|
(memq (overlay-get ov 'face)
|
||
|
swiper-faces))
|
||
|
(setq min-overlay-start (overlay-start ov))))
|
||
|
visible-overlays))
|
||
|
(candidates (append
|
||
|
(mapcar (lambda (ov)
|
||
|
(cons (overlay-start ov)
|
||
|
(overlay-get ov 'window)))
|
||
|
overlays-for-avy)
|
||
|
(save-excursion
|
||
|
(save-restriction
|
||
|
(narrow-to-region (window-start) (window-end))
|
||
|
(goto-char (point-min))
|
||
|
(forward-line)
|
||
|
(let ((cands))
|
||
|
(while (< (point) (point-max))
|
||
|
(push (cons (1+ (point))
|
||
|
(selected-window))
|
||
|
cands)
|
||
|
(forward-line))
|
||
|
cands)))))
|
||
|
(candidate (unwind-protect
|
||
|
(prog2
|
||
|
(avy--make-backgrounds
|
||
|
(append (avy-window-list)
|
||
|
(list (ivy-state-window ivy-last))))
|
||
|
(if (eq avy-style 'de-bruijn)
|
||
|
(avy-read-de-bruijn
|
||
|
candidates avy-keys)
|
||
|
(avy-read (avy-tree candidates avy-keys)
|
||
|
#'avy--overlay-post
|
||
|
#'avy--remove-leading-chars))
|
||
|
(avy-push-mark))
|
||
|
(avy--done))))
|
||
|
(if (window-minibuffer-p (cdr candidate))
|
||
|
(progn
|
||
|
(ivy-set-index (- (line-number-at-pos (car candidate)) 2))
|
||
|
(ivy--exhibit)
|
||
|
(ivy-done)
|
||
|
(ivy-call))
|
||
|
(ivy-quit-and-run
|
||
|
(avy-action-goto (avy-candidate-beg candidate)))))))
|
||
|
|
||
|
(declare-function mc/create-fake-cursor-at-point "ext:multiple-cursors-core")
|
||
|
(declare-function multiple-cursors-mode "ext:multiple-cursors-core")
|
||
|
|
||
|
(defun swiper-mc ()
|
||
|
"Create a fake cursor for each `swiper' candidate."
|
||
|
(interactive)
|
||
|
(unless (require 'multiple-cursors nil t)
|
||
|
(error "Multiple-cursors isn't installed"))
|
||
|
(unless (window-minibuffer-p)
|
||
|
(error "Call me only from `swiper'"))
|
||
|
(let ((cands (nreverse ivy--old-cands)))
|
||
|
(unless (string= ivy-text "")
|
||
|
(ivy-exit-with-action
|
||
|
(lambda (_)
|
||
|
(let (cand)
|
||
|
(while (setq cand (pop cands))
|
||
|
(swiper--action cand)
|
||
|
(when cands
|
||
|
(mc/create-fake-cursor-at-point))))
|
||
|
(multiple-cursors-mode 1))))))
|
||
|
|
||
|
(defun swiper-recenter-top-bottom (&optional arg)
|
||
|
"Call (`recenter-top-bottom' ARG)."
|
||
|
(interactive "P")
|
||
|
(with-ivy-window
|
||
|
(recenter-top-bottom arg)))
|
||
|
|
||
|
(defvar swiper-font-lock-exclude
|
||
|
'(bbdb-mode
|
||
|
bookmark-bmenu-mode
|
||
|
package-menu-mode
|
||
|
gnus-summary-mode
|
||
|
gnus-article-mode
|
||
|
gnus-group-mode
|
||
|
emms-playlist-mode
|
||
|
emms-stream-mode
|
||
|
eshell-mode
|
||
|
erc-mode
|
||
|
forth-mode
|
||
|
forth-block-mode
|
||
|
helpful-mode
|
||
|
nix-mode
|
||
|
org-agenda-mode
|
||
|
dired-mode
|
||
|
jabber-chat-mode
|
||
|
elfeed-search-mode
|
||
|
elfeed-show-mode
|
||
|
fundamental-mode
|
||
|
Man-mode
|
||
|
woman-mode
|
||
|
mu4e-view-mode
|
||
|
mu4e-headers-mode
|
||
|
notmuch-tree-mode
|
||
|
notmuch-search-mode
|
||
|
help-mode
|
||
|
debbugs-gnu-mode
|
||
|
occur-mode
|
||
|
occur-edit-mode
|
||
|
bongo-mode
|
||
|
bongo-library-mode
|
||
|
magit-popup-mode
|
||
|
adoc-mode
|
||
|
bongo-playlist-mode
|
||
|
eww-mode
|
||
|
treemacs-mode
|
||
|
twittering-mode
|
||
|
vc-dir-mode
|
||
|
rcirc-mode
|
||
|
circe-channel-mode
|
||
|
circe-server-mode
|
||
|
circe-query-mode
|
||
|
sauron-mode
|
||
|
w3m-mode)
|
||
|
"List of major-modes that are incompatible with `font-lock-ensure'.")
|
||
|
|
||
|
(defun swiper-font-lock-ensure-p ()
|
||
|
"Return non-nil if we should `font-lock-ensure'."
|
||
|
(or (derived-mode-p 'magit-mode)
|
||
|
(bound-and-true-p magit-blame-mode)
|
||
|
(memq major-mode swiper-font-lock-exclude)))
|
||
|
|
||
|
(defun swiper-font-lock-ensure ()
|
||
|
"Ensure the entired buffer is highlighted."
|
||
|
(unless (swiper-font-lock-ensure-p)
|
||
|
(unless (or (> (buffer-size) 100000) (null font-lock-mode))
|
||
|
(if (fboundp 'font-lock-ensure)
|
||
|
(font-lock-ensure)
|
||
|
(with-no-warnings (font-lock-fontify-buffer))))))
|
||
|
|
||
|
(defvar swiper--format-spec ""
|
||
|
"Store the current candidates format spec.")
|
||
|
|
||
|
(defvar swiper--width nil
|
||
|
"Store the number of digits needed for the longest line nubmer.")
|
||
|
|
||
|
(defvar swiper-use-visual-line nil
|
||
|
"When non-nil, use `line-move' instead of `forward-line'.")
|
||
|
|
||
|
(defvar dired-isearch-filenames)
|
||
|
(declare-function dired-move-to-filename "dired")
|
||
|
|
||
|
(defun swiper--line ()
|
||
|
(let* ((beg (cond ((and (eq major-mode 'dired-mode)
|
||
|
(bound-and-true-p dired-isearch-filenames))
|
||
|
(dired-move-to-filename)
|
||
|
(point))
|
||
|
(swiper-use-visual-line
|
||
|
(save-excursion
|
||
|
(beginning-of-visual-line)
|
||
|
(point)))
|
||
|
(t
|
||
|
(point))))
|
||
|
(end (if swiper-use-visual-line
|
||
|
(save-excursion
|
||
|
(end-of-visual-line)
|
||
|
(point))
|
||
|
(line-end-position))))
|
||
|
|
||
|
(concat
|
||
|
" "
|
||
|
(buffer-substring beg end))))
|
||
|
|
||
|
(declare-function outline-show-all "outline")
|
||
|
|
||
|
(defun swiper--candidates (&optional numbers-width)
|
||
|
"Return a list of this buffer lines.
|
||
|
|
||
|
NUMBERS-WIDTH, when specified, is used for width spec of line
|
||
|
numbers; replaces calculating the width from buffer line count."
|
||
|
(if (and visual-line-mode
|
||
|
;; super-slow otherwise
|
||
|
(< (buffer-size) 20000))
|
||
|
(progn
|
||
|
(when (eq major-mode 'org-mode)
|
||
|
(require 'outline)
|
||
|
(if (fboundp 'outline-show-all)
|
||
|
(outline-show-all)
|
||
|
(with-no-warnings
|
||
|
(show-all))))
|
||
|
(setq swiper-use-visual-line t))
|
||
|
(setq swiper-use-visual-line nil))
|
||
|
(let ((n-lines (count-lines (point-min) (point-max))))
|
||
|
(unless (zerop n-lines)
|
||
|
(setq swiper--width (or numbers-width
|
||
|
(1+ (floor (log n-lines 10)))))
|
||
|
(setq swiper--format-spec
|
||
|
(format "%%-%dd " swiper--width))
|
||
|
(let ((line-number 0)
|
||
|
(advancer (if swiper-use-visual-line
|
||
|
(lambda (arg) (line-move arg t))
|
||
|
#'forward-line))
|
||
|
candidates)
|
||
|
(save-excursion
|
||
|
(goto-char (point-min))
|
||
|
(swiper-font-lock-ensure)
|
||
|
(while (< (point) (point-max))
|
||
|
(let ((str (swiper--line)))
|
||
|
(setq str (ivy-cleanup-string str))
|
||
|
(let ((line-number-str
|
||
|
(format swiper--format-spec (cl-incf line-number))))
|
||
|
(if swiper-include-line-number-in-search
|
||
|
(setq str (concat line-number-str str))
|
||
|
(put-text-property
|
||
|
0 1 'display line-number-str str))
|
||
|
(put-text-property
|
||
|
0 1 'swiper-line-number line-number-str str))
|
||
|
(push str candidates))
|
||
|
(funcall advancer 1))
|
||
|
(nreverse candidates))))))
|
||
|
|
||
|
(defvar swiper--opoint 1
|
||
|
"The point when `swiper' starts.")
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun swiper (&optional initial-input)
|
||
|
"`isearch' with an overview.
|
||
|
When non-nil, INITIAL-INPUT is the initial search pattern."
|
||
|
(interactive)
|
||
|
(swiper--ivy (swiper--candidates) initial-input))
|
||
|
|
||
|
(defvar swiper--current-window-start nil)
|
||
|
|
||
|
(defun swiper--extract-matches (regex cands)
|
||
|
"Extract captured REGEX groups from CANDS."
|
||
|
(let (res)
|
||
|
(dolist (cand cands)
|
||
|
(setq cand (substring cand 1))
|
||
|
(when (string-match regex cand)
|
||
|
(push (mapconcat (lambda (n) (match-string-no-properties n cand))
|
||
|
(number-sequence
|
||
|
1
|
||
|
(/ (- (length (match-data)) 2) 2))
|
||
|
" ")
|
||
|
res)))
|
||
|
(nreverse res)))
|
||
|
|
||
|
(defun swiper-occur (&optional revert)
|
||
|
"Generate a custom occur buffer for `swiper'.
|
||
|
When REVERT is non-nil, regenerate the current *ivy-occur* buffer.
|
||
|
When capture groups are present in the input, print them instead of lines."
|
||
|
(let* ((buffer (ivy-state-buffer ivy-last))
|
||
|
(fname (propertize
|
||
|
(with-ivy-window
|
||
|
(if (buffer-file-name buffer)
|
||
|
(file-name-nondirectory
|
||
|
(buffer-file-name buffer))
|
||
|
(buffer-name buffer)))
|
||
|
'face
|
||
|
'compilation-info))
|
||
|
(re (progn (string-match "\"\\(.*\\)\"" (buffer-name))
|
||
|
(match-string 1 (buffer-name))))
|
||
|
(re (mapconcat #'identity (ivy--split re) ".*?"))
|
||
|
(cands
|
||
|
(mapcar
|
||
|
(lambda (s)
|
||
|
(let* ((n (get-text-property 0 'swiper-line-number s))
|
||
|
(i (string-match-p "[ \t\n\r]+\\'" n)))
|
||
|
(when i (setq n (substring n 0 i)))
|
||
|
(put-text-property 0 (length n) 'face 'compilation-line-number n)
|
||
|
(format "%s:%s:%s" fname n (substring s 1))))
|
||
|
(if (not revert)
|
||
|
ivy--old-cands
|
||
|
(setq ivy--old-re nil)
|
||
|
(let ((ivy--regex-function 'swiper--re-builder))
|
||
|
(ivy--filter re (with-current-buffer buffer
|
||
|
(swiper--candidates))))))))
|
||
|
(if (string-match-p "\\\\(" re)
|
||
|
(insert
|
||
|
(mapconcat #'identity
|
||
|
(swiper--extract-matches
|
||
|
re (with-current-buffer buffer
|
||
|
(swiper--candidates)))
|
||
|
"\n"))
|
||
|
(unless (eq major-mode 'ivy-occur-grep-mode)
|
||
|
(ivy-occur-grep-mode)
|
||
|
(font-lock-mode -1))
|
||
|
(setq swiper--current-window-start nil)
|
||
|
(insert (format "-*- mode:grep; default-directory: %S -*-\n\n\n"
|
||
|
default-directory))
|
||
|
(insert (format "%d candidates:\n" (length cands)))
|
||
|
(ivy--occur-insert-lines
|
||
|
(mapcar
|
||
|
(lambda (cand) (concat "./" cand))
|
||
|
cands))
|
||
|
(goto-char (point-min))
|
||
|
(forward-line 4))))
|
||
|
|
||
|
(ivy-set-occur 'swiper 'swiper-occur)
|
||
|
|
||
|
(declare-function evil-set-jump "ext:evil-jumps")
|
||
|
|
||
|
(defvar swiper--current-line nil)
|
||
|
(defvar swiper--current-match-start nil)
|
||
|
(defvar swiper--point-min nil)
|
||
|
(defvar swiper--point-max nil)
|
||
|
(defvar swiper--reveal-mode nil)
|
||
|
|
||
|
(defun swiper--init ()
|
||
|
"Perform initialization common to both completion methods."
|
||
|
(setq swiper--current-line nil)
|
||
|
(setq swiper--current-match-start nil)
|
||
|
(setq swiper--current-window-start nil)
|
||
|
(setq swiper--opoint (point))
|
||
|
(setq swiper--point-min (point-min))
|
||
|
(setq swiper--point-max (point-max))
|
||
|
(when (setq swiper--reveal-mode
|
||
|
(bound-and-true-p reveal-mode))
|
||
|
(reveal-mode -1))
|
||
|
(when (bound-and-true-p evil-mode)
|
||
|
(evil-set-jump)))
|
||
|
|
||
|
(declare-function char-fold-to-regexp "char-fold")
|
||
|
|
||
|
(defun swiper--re-builder (str)
|
||
|
"Transform STR into a swiper regex.
|
||
|
This is the regex used in the minibuffer where candidates have
|
||
|
line numbers. For the buffer, use `ivy--regex' instead."
|
||
|
(let* ((re-builder (ivy-alist-setting ivy-re-builders-alist))
|
||
|
(re (cond
|
||
|
((equal str "")
|
||
|
"")
|
||
|
((equal str "^")
|
||
|
(setq ivy--subexps 0)
|
||
|
".")
|
||
|
((string-match "^\\^" str)
|
||
|
(let ((re (funcall re-builder (substring str 1))))
|
||
|
(if (zerop ivy--subexps)
|
||
|
(prog1 (format "^ ?\\(%s\\)" re)
|
||
|
(setq ivy--subexps 1))
|
||
|
(format "^ %s" re))))
|
||
|
((eq (bound-and-true-p search-default-mode) 'char-fold-to-regexp)
|
||
|
(mapconcat #'char-fold-to-regexp (ivy--split str) ".*"))
|
||
|
(t
|
||
|
(funcall re-builder str)))))
|
||
|
re))
|
||
|
|
||
|
(defvar swiper-history nil
|
||
|
"History for `swiper'.")
|
||
|
|
||
|
(defvar swiper-invocation-face nil
|
||
|
"The face at the point of invocation of `swiper'.")
|
||
|
|
||
|
(defcustom swiper-stay-on-quit nil
|
||
|
"When non-nil don't go back to search start on abort."
|
||
|
:type 'boolean)
|
||
|
|
||
|
(defun swiper--ivy (candidates &optional initial-input)
|
||
|
"Select one of CANDIDATES and move there.
|
||
|
When non-nil, INITIAL-INPUT is the initial search pattern."
|
||
|
(swiper--init)
|
||
|
(setq swiper-invocation-face
|
||
|
(plist-get (text-properties-at (point)) 'face))
|
||
|
(let ((preselect
|
||
|
(if swiper-use-visual-line
|
||
|
(count-screen-lines
|
||
|
(point-min)
|
||
|
(save-excursion (beginning-of-visual-line) (point)))
|
||
|
(1- (line-number-at-pos))))
|
||
|
(minibuffer-allow-text-properties t)
|
||
|
res)
|
||
|
(unwind-protect
|
||
|
(and
|
||
|
(setq res
|
||
|
(ivy-read
|
||
|
"Swiper: "
|
||
|
candidates
|
||
|
:initial-input initial-input
|
||
|
:keymap swiper-map
|
||
|
:preselect preselect
|
||
|
:require-match t
|
||
|
:update-fn #'swiper--update-input-ivy
|
||
|
:unwind #'swiper--cleanup
|
||
|
:action #'swiper--action
|
||
|
:re-builder #'swiper--re-builder
|
||
|
:history 'swiper-history
|
||
|
:caller 'swiper))
|
||
|
(point))
|
||
|
(unless (or res swiper-stay-on-quit)
|
||
|
(goto-char swiper--opoint))
|
||
|
(when (and (null res) (> (length ivy-text) 0))
|
||
|
(cl-pushnew ivy-text swiper-history))
|
||
|
(when swiper--reveal-mode
|
||
|
(reveal-mode 1)))))
|
||
|
|
||
|
(defun swiper-toggle-face-matching ()
|
||
|
"Toggle matching only the candidates with `swiper-invocation-face'."
|
||
|
(interactive)
|
||
|
(setf (ivy-state-matcher ivy-last)
|
||
|
(if (ivy-state-matcher ivy-last)
|
||
|
nil
|
||
|
#'swiper--face-matcher))
|
||
|
(setq ivy--old-re nil))
|
||
|
|
||
|
(defun swiper--face-matcher (regexp candidates)
|
||
|
"Return REGEXP matching CANDIDATES.
|
||
|
Matched candidates should have `swiper-invocation-face'."
|
||
|
(cl-remove-if-not
|
||
|
(lambda (x)
|
||
|
(and
|
||
|
(string-match regexp x)
|
||
|
(let ((s (match-string 0 x))
|
||
|
(i 0))
|
||
|
(while (and (< i (length s))
|
||
|
(text-property-any
|
||
|
i (1+ i)
|
||
|
'face swiper-invocation-face
|
||
|
s))
|
||
|
(cl-incf i))
|
||
|
(eq i (length s)))))
|
||
|
candidates))
|
||
|
|
||
|
(defun swiper--ensure-visible ()
|
||
|
"Remove overlays hiding point."
|
||
|
(let ((overlays (overlays-at (1- (point))))
|
||
|
ov expose)
|
||
|
(while (setq ov (pop overlays))
|
||
|
(if (and (invisible-p (overlay-get ov 'invisible))
|
||
|
(setq expose (overlay-get ov 'isearch-open-invisible)))
|
||
|
(funcall expose ov)))))
|
||
|
|
||
|
(defvar swiper--overlays nil
|
||
|
"Store overlays.")
|
||
|
|
||
|
(defun swiper--cleanup ()
|
||
|
"Clean up the overlays."
|
||
|
(while swiper--overlays
|
||
|
(delete-overlay (pop swiper--overlays)))
|
||
|
(save-excursion
|
||
|
(goto-char (point-min))
|
||
|
(isearch-clean-overlays)))
|
||
|
|
||
|
(defun swiper--update-input-ivy ()
|
||
|
"Called when `ivy' input is updated."
|
||
|
(with-ivy-window
|
||
|
(swiper--cleanup)
|
||
|
(when (> (length (ivy-state-current ivy-last)) 0)
|
||
|
(let* ((regexp-or-regexps (funcall ivy--regex-function ivy-text))
|
||
|
(regexps
|
||
|
(if (listp regexp-or-regexps)
|
||
|
(mapcar #'car (cl-remove-if-not #'cdr regexp-or-regexps))
|
||
|
(list regexp-or-regexps))))
|
||
|
(dolist (re regexps)
|
||
|
(let* ((re (replace-regexp-in-string
|
||
|
" " "\t"
|
||
|
re))
|
||
|
(str (get-text-property 0 'swiper-line-number (ivy-state-current ivy-last)))
|
||
|
(num (if (string-match "^[0-9]+" str)
|
||
|
(string-to-number (match-string 0 str))
|
||
|
0)))
|
||
|
(unless (memq this-command '(ivy-yank-word
|
||
|
ivy-yank-symbol
|
||
|
ivy-yank-char
|
||
|
scroll-other-window))
|
||
|
(when (cl-plusp num)
|
||
|
(unless (if swiper--current-line
|
||
|
(eq swiper--current-line num)
|
||
|
(eq (line-number-at-pos) num))
|
||
|
(goto-char swiper--point-min)
|
||
|
(if swiper-use-visual-line
|
||
|
(line-move (1- num))
|
||
|
(forward-line (1- num))))
|
||
|
(if (and (equal ivy-text "")
|
||
|
(>= swiper--opoint (line-beginning-position))
|
||
|
(<= swiper--opoint (line-end-position)))
|
||
|
(goto-char swiper--opoint)
|
||
|
(if (eq swiper--current-line num)
|
||
|
(when swiper--current-match-start
|
||
|
(goto-char swiper--current-match-start))
|
||
|
(setq swiper--current-line num))
|
||
|
(when (re-search-forward re (line-end-position) t)
|
||
|
(setq swiper--current-match-start (match-beginning 0))))
|
||
|
(isearch-range-invisible (line-beginning-position)
|
||
|
(line-end-position))
|
||
|
(unless (and (>= (point) (window-start))
|
||
|
(<= (point) (window-end (ivy-state-window ivy-last) t)))
|
||
|
(recenter))
|
||
|
(setq swiper--current-window-start (window-start))))
|
||
|
(swiper--add-overlays
|
||
|
re
|
||
|
(max (window-start) swiper--point-min)
|
||
|
(min (window-end (selected-window) t) swiper--point-max))))))))
|
||
|
|
||
|
(defun swiper--add-overlays (re &optional beg end wnd)
|
||
|
"Add overlays for RE regexp in visible part of the current buffer.
|
||
|
BEG and END, when specified, are the point bounds.
|
||
|
WND, when specified is the window."
|
||
|
(setq wnd (or wnd (ivy-state-window ivy-last)))
|
||
|
(let ((ov (if visual-line-mode
|
||
|
(make-overlay
|
||
|
(save-excursion
|
||
|
(beginning-of-visual-line)
|
||
|
(point))
|
||
|
(save-excursion
|
||
|
(end-of-visual-line)
|
||
|
(point)))
|
||
|
(make-overlay
|
||
|
(line-beginning-position)
|
||
|
(1+ (line-end-position))))))
|
||
|
(overlay-put ov 'face 'swiper-line-face)
|
||
|
(overlay-put ov 'window wnd)
|
||
|
(push ov swiper--overlays)
|
||
|
(let* ((wh (window-height))
|
||
|
(beg (or beg (save-excursion
|
||
|
(forward-line (- wh))
|
||
|
(point))))
|
||
|
(end (or end (save-excursion
|
||
|
(forward-line wh)
|
||
|
(point))))
|
||
|
(case-fold-search (ivy--case-fold-p re)))
|
||
|
(when (>= (length re) swiper-min-highlight)
|
||
|
(save-excursion
|
||
|
(goto-char beg)
|
||
|
;; RE can become an invalid regexp
|
||
|
(while (and (ignore-errors (re-search-forward re end t))
|
||
|
(> (- (match-end 0) (match-beginning 0)) 0))
|
||
|
;; Don't highlight a match if it spans multiple
|
||
|
;; lines. `count-lines' returns 1 if the match is within a
|
||
|
;; single line, even if it includes the newline, and 2 or
|
||
|
;; greater otherwise. We hope that the inclusion of the
|
||
|
;; newline will not ever be a problem in practice.
|
||
|
(when (< (count-lines (match-beginning 0) (match-end 0)) 2)
|
||
|
(unless (and (consp ivy--old-re)
|
||
|
(null
|
||
|
(save-match-data
|
||
|
(ivy--re-filter ivy--old-re
|
||
|
(list
|
||
|
(buffer-substring-no-properties
|
||
|
(line-beginning-position)
|
||
|
(line-end-position)))))))
|
||
|
(let ((mb (match-beginning 0))
|
||
|
(me (match-end 0)))
|
||
|
(unless (> (- me mb) 2017)
|
||
|
(swiper--add-overlay mb me
|
||
|
(if (zerop ivy--subexps)
|
||
|
(cadr swiper-faces)
|
||
|
(car swiper-faces))
|
||
|
wnd 0))))
|
||
|
(let ((i 1)
|
||
|
(j 0))
|
||
|
(while (<= (cl-incf j) ivy--subexps)
|
||
|
(let ((bm (match-beginning j))
|
||
|
(em (match-end j)))
|
||
|
(when (and (integerp em)
|
||
|
(integerp bm))
|
||
|
(while (and (< j ivy--subexps)
|
||
|
(integerp (match-beginning (+ j 1)))
|
||
|
(= em (match-beginning (+ j 1))))
|
||
|
(setq em (match-end (cl-incf j))))
|
||
|
(swiper--add-overlay
|
||
|
bm em
|
||
|
(nth (1+ (mod (+ i 2) (1- (length swiper-faces))))
|
||
|
swiper-faces)
|
||
|
wnd i)
|
||
|
(cl-incf i))))))))))))
|
||
|
|
||
|
(defun swiper--add-overlay (beg end face wnd priority)
|
||
|
"Add overlay bound by BEG and END to `swiper--overlays'.
|
||
|
FACE, WND and PRIORITY are properties corresponding to
|
||
|
the face, window and priority of the overlay."
|
||
|
(let ((overlay (make-overlay beg end)))
|
||
|
(push overlay swiper--overlays)
|
||
|
(overlay-put overlay 'face face)
|
||
|
(overlay-put overlay 'window wnd)
|
||
|
(overlay-put overlay 'priority priority)))
|
||
|
|
||
|
(defcustom swiper-action-recenter nil
|
||
|
"When non-nil, recenter after exiting `swiper'."
|
||
|
:type 'boolean)
|
||
|
(defvar evil-search-module)
|
||
|
(defvar evil-ex-search-pattern)
|
||
|
(defvar evil-ex-search-persistent-highlight)
|
||
|
(defvar evil-ex-search-direction)
|
||
|
(declare-function evil-ex-search-activate-highlight "evil-ex")
|
||
|
|
||
|
|
||
|
(defun swiper--action (x)
|
||
|
"Goto line X."
|
||
|
(let ((ln (1- (read (or (get-text-property 0 'swiper-line-number x)
|
||
|
(and (string-match ":\\([0-9]+\\):.*\\'" x)
|
||
|
(match-string-no-properties 1 x))))))
|
||
|
(re (ivy--regex ivy-text)))
|
||
|
(if (null x)
|
||
|
(user-error "No candidates")
|
||
|
(with-ivy-window
|
||
|
(unless (equal (current-buffer)
|
||
|
(ivy-state-buffer ivy-last))
|
||
|
(switch-to-buffer (ivy-state-buffer ivy-last)))
|
||
|
(goto-char swiper--point-min)
|
||
|
(funcall (if swiper-use-visual-line
|
||
|
#'line-move
|
||
|
#'forward-line)
|
||
|
ln)
|
||
|
(when (and (re-search-forward re (line-end-position) t) swiper-goto-start-of-match)
|
||
|
(goto-char (match-beginning 0)))
|
||
|
(swiper--ensure-visible)
|
||
|
(cond (swiper-action-recenter
|
||
|
(recenter))
|
||
|
(swiper--current-window-start
|
||
|
(set-window-start (selected-window) swiper--current-window-start)))
|
||
|
(when (/= (point) swiper--opoint)
|
||
|
(unless (and transient-mark-mode mark-active)
|
||
|
(when (eq ivy-exit 'done)
|
||
|
(push-mark swiper--opoint t)
|
||
|
(message "Mark saved where search started"))))
|
||
|
(add-to-history
|
||
|
'regexp-search-ring
|
||
|
re
|
||
|
regexp-search-ring-max)
|
||
|
;; integration with evil-mode's search
|
||
|
(when (bound-and-true-p evil-mode)
|
||
|
(when (eq evil-search-module 'isearch)
|
||
|
(setq isearch-string ivy-text))
|
||
|
(when (eq evil-search-module 'evil-search)
|
||
|
(add-to-history 'evil-ex-search-history re)
|
||
|
(setq evil-ex-search-pattern (list re t t))
|
||
|
(setq evil-ex-search-direction 'forward)
|
||
|
(when evil-ex-search-persistent-highlight
|
||
|
(evil-ex-search-activate-highlight evil-ex-search-pattern))))))))
|
||
|
|
||
|
(defun swiper-from-isearch ()
|
||
|
"Invoke `swiper' from isearch."
|
||
|
(interactive)
|
||
|
(let ((query (if isearch-regexp
|
||
|
isearch-string
|
||
|
(regexp-quote isearch-string))))
|
||
|
(isearch-exit)
|
||
|
(swiper query)))
|
||
|
|
||
|
(defvar swiper-multi-buffers nil
|
||
|
"Store the current list of buffers.")
|
||
|
|
||
|
(defvar swiper-multi-candidates nil
|
||
|
"Store the list of candidates for `swiper-multi'.")
|
||
|
|
||
|
(defun swiper-multi-prompt ()
|
||
|
"Return prompt for `swiper-multi'."
|
||
|
(format "Buffers (%s): "
|
||
|
(mapconcat #'identity swiper-multi-buffers ", ")))
|
||
|
|
||
|
(defun swiper-multi ()
|
||
|
"Select one or more buffers.
|
||
|
Run `swiper' for those buffers."
|
||
|
(interactive)
|
||
|
(setq swiper-multi-buffers nil)
|
||
|
(let ((ivy-use-virtual-buffers nil))
|
||
|
(ivy-read (swiper-multi-prompt)
|
||
|
'internal-complete-buffer
|
||
|
:action 'swiper-multi-action-1))
|
||
|
(ivy-read "Swiper: " swiper-multi-candidates
|
||
|
:action 'swiper-multi-action-2
|
||
|
:unwind #'swiper--cleanup
|
||
|
:caller 'swiper-multi))
|
||
|
|
||
|
(defun swiper-multi-action-1 (x)
|
||
|
"Add X to list of selected buffers `swiper-multi-buffers'.
|
||
|
If X is already part of the list, remove it instead. Quit the selection if
|
||
|
X is selected by either `ivy-done', `ivy-alt-done' or `ivy-immediate-done',
|
||
|
otherwise continue prompting for buffers."
|
||
|
(if (member x swiper-multi-buffers)
|
||
|
(progn
|
||
|
(setq swiper-multi-buffers (delete x swiper-multi-buffers)))
|
||
|
(unless (equal x "")
|
||
|
(setq swiper-multi-buffers (append swiper-multi-buffers (list x)))))
|
||
|
(let ((prompt (swiper-multi-prompt)))
|
||
|
(setf (ivy-state-prompt ivy-last) prompt)
|
||
|
(setq ivy--prompt (concat "%-4d " prompt)))
|
||
|
(cond ((memq this-command '(ivy-done
|
||
|
ivy-alt-done
|
||
|
ivy-immediate-done))
|
||
|
(setq swiper-multi-candidates
|
||
|
(swiper--multi-candidates
|
||
|
(mapcar #'get-buffer swiper-multi-buffers))))
|
||
|
((eq this-command 'ivy-call)
|
||
|
(with-selected-window (active-minibuffer-window)
|
||
|
(delete-minibuffer-contents)))))
|
||
|
|
||
|
(defun swiper-multi-action-2 (x)
|
||
|
"Move to candidate X from `swiper-multi'."
|
||
|
(when (> (length x) 0)
|
||
|
(let ((buffer-name (get-text-property 0 'buffer x)))
|
||
|
(when buffer-name
|
||
|
(with-ivy-window
|
||
|
(switch-to-buffer buffer-name)
|
||
|
(goto-char (point-min))
|
||
|
(forward-line (1- (read (get-text-property 0 'swiper-line-number x))))
|
||
|
(re-search-forward
|
||
|
(ivy--regex ivy-text)
|
||
|
(line-end-position) t)
|
||
|
(isearch-range-invisible (line-beginning-position)
|
||
|
(line-end-position))
|
||
|
(unless (eq ivy-exit 'done)
|
||
|
(swiper--cleanup)
|
||
|
(swiper--add-overlays (ivy--regex ivy-text))))))))
|
||
|
|
||
|
(defun swiper-all-buffer-p (buffer)
|
||
|
"Return non-nil if BUFFER should be considered by `swiper-all'."
|
||
|
(let ((mode (buffer-local-value 'major-mode (get-buffer buffer))))
|
||
|
(cond
|
||
|
;; Ignore TAGS buffers, they tend to add duplicate results.
|
||
|
((eq mode #'tags-table-mode) nil)
|
||
|
;; Always consider dired buffers, even though they're not backed
|
||
|
;; by a file.
|
||
|
((eq mode #'dired-mode) t)
|
||
|
;; Always consider stash buffers too, as they may have
|
||
|
;; interesting content not present in any buffers. We don't #'
|
||
|
;; quote to satisfy the byte-compiler.
|
||
|
((eq mode 'magit-stash-mode) t)
|
||
|
;; Email buffers have no file, but are useful to search
|
||
|
((eq mode 'gnus-article-mode) t)
|
||
|
;; Otherwise, only consider the file if it's backed by a file.
|
||
|
(t (buffer-file-name buffer)))))
|
||
|
|
||
|
;;* `swiper-all'
|
||
|
(defun swiper-all-function (str)
|
||
|
"Search in all open buffers for STR."
|
||
|
(if (and (< (length str) 3))
|
||
|
(list "" (format "%d chars more" (- 3 (length ivy-text))))
|
||
|
(let* ((buffers (cl-remove-if-not #'swiper-all-buffer-p (buffer-list)))
|
||
|
(re-full (funcall ivy--regex-function str))
|
||
|
re re-tail
|
||
|
cands match
|
||
|
(case-fold-search (ivy--case-fold-p str)))
|
||
|
(if (stringp re-full)
|
||
|
(setq re re-full)
|
||
|
(setq re (caar re-full))
|
||
|
(setq re-tail (cdr re-full)))
|
||
|
(dolist (buffer buffers)
|
||
|
(with-current-buffer buffer
|
||
|
(save-excursion
|
||
|
(goto-char (point-min))
|
||
|
(while (re-search-forward re nil t)
|
||
|
(setq match (if (memq major-mode '(org-mode dired-mode))
|
||
|
(buffer-substring-no-properties
|
||
|
(line-beginning-position)
|
||
|
(line-end-position))
|
||
|
(buffer-substring
|
||
|
(line-beginning-position)
|
||
|
(line-end-position))))
|
||
|
(put-text-property
|
||
|
0 1 'buffer
|
||
|
(buffer-name)
|
||
|
match)
|
||
|
(put-text-property 0 1 'point (point) match)
|
||
|
(when (or (null re-tail) (ivy-re-match re-tail match))
|
||
|
(push match cands))))))
|
||
|
(setq ivy--old-re re-full)
|
||
|
(if (null cands)
|
||
|
(list "")
|
||
|
(setq ivy--old-cands (nreverse cands))))))
|
||
|
|
||
|
(defvar swiper-window-width 80)
|
||
|
|
||
|
(defun swiper--all-format-function (cands)
|
||
|
"Format CANDS for `swiper-all'.
|
||
|
See `ivy-format-function' for further information."
|
||
|
(let* ((ww swiper-window-width)
|
||
|
(col2 1)
|
||
|
(cands-with-buffer
|
||
|
(mapcar (lambda (s)
|
||
|
(let ((buffer (get-text-property 0 'buffer s)))
|
||
|
(setq col2 (max col2 (length buffer)))
|
||
|
(cons s buffer))) cands))
|
||
|
(col1 (- ww 4 col2)))
|
||
|
(setq cands
|
||
|
(mapcar (lambda (x)
|
||
|
(if (cdr x)
|
||
|
(let ((s (ivy--truncate-string (car x) col1)))
|
||
|
(concat
|
||
|
s
|
||
|
(make-string
|
||
|
(max 0
|
||
|
(- ww (string-width s) (length (cdr x))))
|
||
|
?\ )
|
||
|
(cdr x)))
|
||
|
(car x)))
|
||
|
cands-with-buffer))
|
||
|
(ivy--format-function-generic
|
||
|
(lambda (str)
|
||
|
(ivy--add-face str 'ivy-current-match))
|
||
|
(lambda (str)
|
||
|
str)
|
||
|
cands
|
||
|
"\n")))
|
||
|
|
||
|
(defvar swiper-all-map
|
||
|
(let ((map (make-sparse-keymap)))
|
||
|
(define-key map (kbd "M-q") 'swiper-all-query-replace)
|
||
|
map)
|
||
|
"Keymap for `swiper-all'.")
|
||
|
|
||
|
;;;###autoload
|
||
|
(defun swiper-all (&optional initial-input)
|
||
|
"Run `swiper' for all open buffers."
|
||
|
(interactive)
|
||
|
(let* ((swiper-window-width (- (frame-width) (if (display-graphic-p) 0 1)))
|
||
|
(ivy-format-function #'swiper--all-format-function))
|
||
|
(ivy-read "swiper-all: " 'swiper-all-function
|
||
|
:action 'swiper-all-action
|
||
|
:unwind #'swiper--cleanup
|
||
|
:update-fn (lambda ()
|
||
|
(swiper-all-action (ivy-state-current ivy-last)))
|
||
|
:dynamic-collection t
|
||
|
:keymap swiper-all-map
|
||
|
:initial-input initial-input
|
||
|
:caller 'swiper-multi)))
|
||
|
|
||
|
(defun swiper-all-action (x)
|
||
|
"Move to candidate X from `swiper-all'."
|
||
|
(when (> (length x) 0)
|
||
|
(let ((buffer-name (get-text-property 0 'buffer x)))
|
||
|
(when buffer-name
|
||
|
(with-ivy-window
|
||
|
(switch-to-buffer buffer-name)
|
||
|
(goto-char (get-text-property 0 'point x))
|
||
|
(isearch-range-invisible (line-beginning-position)
|
||
|
(line-end-position))
|
||
|
(unless (eq ivy-exit 'done)
|
||
|
(swiper--cleanup)
|
||
|
(swiper--add-overlays (ivy--regex ivy-text))))))))
|
||
|
|
||
|
(defun swiper--multi-candidates (buffers)
|
||
|
"Extract candidates from BUFFERS."
|
||
|
(let* ((ww (window-width))
|
||
|
(res nil)
|
||
|
(column-2 (apply #'max
|
||
|
(mapcar
|
||
|
(lambda (b)
|
||
|
(length (buffer-name b)))
|
||
|
buffers)))
|
||
|
(column-1 (- ww 4 column-2 1)))
|
||
|
(dolist (buf buffers)
|
||
|
(with-current-buffer buf
|
||
|
(setq res
|
||
|
(append
|
||
|
(mapcar
|
||
|
(lambda (s)
|
||
|
(setq s (concat (ivy--truncate-string s column-1) " "))
|
||
|
(let ((len (length s)))
|
||
|
(put-text-property
|
||
|
(1- len) len 'display
|
||
|
(concat
|
||
|
(make-string
|
||
|
(max 0
|
||
|
(- ww (string-width s) (length (buffer-name)) 3))
|
||
|
?\ )
|
||
|
(buffer-name))
|
||
|
s)
|
||
|
(put-text-property 0 len 'buffer buf s)
|
||
|
s))
|
||
|
(swiper--candidates 4))
|
||
|
res))
|
||
|
nil))
|
||
|
res))
|
||
|
|
||
|
(provide 'swiper)
|
||
|
|
||
|
;;; swiper.el ends here
|