-;;; swiper.el --- Isearch with a helm overview. Oh, man! -*- lexical-binding: t -*-
+;;; swiper.el --- Isearch with an overview. Oh, man! -*- lexical-binding: t -*-
-;; Copyright (C) 2015 Oleh Krehel
+;; Copyright (C) 2015 Free Software Foundation, Inc.
;; Author: Oleh Krehel <ohwoeowho@gmail.com>
;; URL: https://github.com/abo-abo/swiper
-;; Version: 0.1.0
-;; Package-Requires: ((helm "1.6.7") (emacs "24.1"))
+;; Version: 0.3.0
+;; Package-Requires: ((emacs "24.1"))
;; Keywords: matching
-;; This file is not part of GNU Emacs
+;; 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
;;; Commentary:
;;
;; This package gives an overview of the current regex search
-;; candidates in a `helm' buffer. The search regex can be split into
-;; groups with a space. Each group is highlighted with a different
-;; face.
+;; candidates. The search regex can be split into groups with a
+;; space. Each group is highlighted with a different face.
+;;
+;; The overview back end is `ivy'.
;;
;; It can double as a quick `regex-builder', although only single
;; lines will be matched.
;;; Code:
-(require 'helm)
+(require 'ivy)
(defgroup swiper nil
- "Interactive `occur' using `helm'."
+ "`isearch' with an overview."
:group 'matching
:prefix "swiper-")
(defface swiper-match-face-1
- '((t (:background "#FEEA89")))
+ '((t (:inherit isearch-lazy-highlight-face)))
"Face for `swiper' matches.")
(defface swiper-match-face-2
- '((t (:background "#F9A35A")))
+ '((t (:inherit isearch)))
"Face for `swiper' matches.")
(defface swiper-match-face-3
- '((t (:background "#fb7905")))
+ '((t (:inherit match)))
"Face for `swiper' matches.")
(defface swiper-match-face-4
- '((t (:background "#F15C79")))
+ '((t (:inherit isearch)))
"Face for `swiper' matches.")
(defface swiper-line-face
- '((t (:background "#f3d3d3")))
+ '((t (:inherit highlight)))
"Face for current `swiper' line.")
(defcustom swiper-faces '(swiper-match-face-1
swiper-match-face-4)
"List of `swiper' faces for group matches.")
-(defvar swiper--buffer nil
- "Store current buffer.")
+(defcustom swiper-min-highlight 2
+ "Only highlight matches for regexps at least this long."
+ :type 'integer)
+
+(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)
+ map)
+ "Keymap for swiper.")
-(defalias 'swiper-font-lock-ensure
- (if (fboundp 'font-lock-ensure)
- 'font-lock-ensure
- 'font-lock-fontify-buffer))
+(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 (query-replace-read-to from "Query replace" t)))
+ (delete-minibuffer-contents)
+ (setq ivy--action
+ (lambda ()
+ (perform-replace from to
+ t t t)))
+ (swiper--cleanup)
+ (exit-minibuffer))))
+
+(defvar swiper--window nil
+ "Store the current window.")
+
+(defun swiper-recenter-top-bottom (&optional arg)
+ "Call (`recenter-top-bottom' ARG) in `swiper--window'."
+ (interactive "P")
+ (with-selected-window swiper--window
+ (recenter-top-bottom arg)))
+
+(defun swiper-font-lock-ensure ()
+ "Ensure the entired buffer is highlighted."
+ (unless (or (derived-mode-p 'magit-mode)
+ (memq major-mode '(package-menu-mode
+ gnus-summary-mode
+ gnus-article-mode
+ gnus-group-mode
+ emms-playlist-mode erc-mode
+ org-agenda-mode
+ dired-mode)))
+ (unless (> (buffer-size) 100000)
+ (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.")
(defun swiper--candidates ()
"Return a list of this buffer lines."
- (let* ((line-width (1+ (floor (log (count-lines
- (point-min) (point-max))
- 10))))
- (fspec (format "%%-%dd %%s" line-width))
- (line-number 0)
- candidates)
- (save-excursion
- (goto-char (point-min))
- (swiper-font-lock-ensure)
- (while (< (point) (point-max))
- (push (format fspec
- (incf line-number)
- (buffer-substring
- (line-beginning-position)
- (line-end-position)))
- candidates)
- (zerop (forward-line 1)))
- (nreverse candidates))))
+ (let ((n-lines (count-lines (point-min) (point-max))))
+ (unless (zerop n-lines)
+ (setq swiper--format-spec
+ (format "%%-%dd %%s" (1+ (floor (log n-lines 10)))))
+ (let ((line-number 0)
+ candidates)
+ (save-excursion
+ (goto-char (point-min))
+ (swiper-font-lock-ensure)
+ (while (< (point) (point-max))
+ (push (format swiper--format-spec
+ (cl-incf line-number)
+ (buffer-substring
+ (line-beginning-position)
+ (line-end-position)))
+ candidates)
+ (forward-line 1))
+ (nreverse candidates))))))
+
+(defvar swiper--opoint 1
+ "The point when `swiper' starts.")
;;;###autoload
-(defun swiper ()
- "Interactive `occur' using `helm'."
+(defun swiper (&optional initial-input)
+ "`isearch' with an overview.
+When non-nil, INITIAL-INPUT is the initial search pattern."
(interactive)
+ (swiper--ivy initial-input))
+
+(defvar swiper--anchor nil
+ "A line number to which the search should be anchored.")
+
+(defvar swiper--len 0
+ "The last length of input for which an anchoring was made.")
+
+(defun swiper--init ()
+ "Perform initialization common to both completion methods."
(deactivate-mark)
+ (setq swiper--opoint (point))
(setq swiper--len 0)
(setq swiper--anchor (line-number-at-pos))
- (unwind-protect
- (let ((helm-display-function
- (lambda (buf)
- (when (one-window-p)
- (split-window-vertically))
- (other-window 1)
- (switch-to-buffer buf)))
- helm-candidate-number-limit)
- (helm :sources
- `((name . ,(buffer-name))
- (init . (lambda ()
- (setq swiper--buffer (current-buffer))
- (add-hook 'helm-move-selection-after-hook
- #'swiper--update-sel)
- (add-hook 'helm-update-hook
- #'swiper--update-input)
- (add-hook 'helm-after-update-hook
- #'swiper--reanchor)))
- (match-strict . (lambda (x) (ignore-errors
- (string-match (swiper--regex) x))))
- (candidates . ,(swiper--candidates))
- (filtered-candidate-transformer
- helm-fuzzy-highlight-matches)
- (action . swiper--action))
- :preselect
- (format "^%d " swiper--anchor)
- :buffer "*swiper*"))
- ;; cleanup
- (remove-hook 'helm-move-selection-after-hook
- #'swiper--update-sel)
- (remove-hook 'helm-update-hook
- #'swiper--update-input)
- (remove-hook 'helm-after-update-hook
- #'swiper--reanchor)
- (while swiper--overlays
- (delete-overlay (pop swiper--overlays)))))
+ (setq swiper--window (selected-window)))
+
+(defun swiper--ivy (&optional initial-input)
+ "`isearch' with an overview using `ivy'.
+When non-nil, INITIAL-INPUT is the initial search pattern."
+ (interactive)
+ (swiper--init)
+ (let ((candidates (swiper--candidates))
+ (preselect (format
+ swiper--format-spec
+ (line-number-at-pos)
+ (regexp-quote
+ (buffer-substring-no-properties
+ (line-beginning-position)
+ (line-end-position)))))
+ res)
+ (unwind-protect
+ (setq res (ivy-read
+ (replace-regexp-in-string
+ "%s" "pattern: " swiper--format-spec)
+ candidates
+ nil
+ initial-input
+ swiper-map
+ preselect
+ #'swiper--update-input-ivy))
+ (swiper--cleanup)
+ (if (null ivy-exit)
+ (goto-char swiper--opoint)
+ (swiper--action res ivy-text)))))
+
+(defun swiper--ensure-visible ()
+ "Remove overlays hiding point."
+ (let ((overlays (overlays-at (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.")
-(defvar swiper--anchor nil
- "A line number to which the search should be anchored.")
+(defun swiper--cleanup ()
+ "Clean up the overlays."
+ (while swiper--overlays
+ (delete-overlay (pop swiper--overlays)))
+ (save-excursion
+ (goto-char (point-min))
+ (isearch-clean-overlays)))
-(defvar swiper--len 0
- "The last length of `helm-input' for which an anchoring was made.")
-
-(defun swiper--update-input ()
- "Update selection."
- (with-current-buffer swiper--buffer
- (let ((re (swiper--regex))
- (we (window-end nil t)))
- (while swiper--overlays
- (delete-overlay (pop swiper--overlays)))
- (when (> (length helm-input) 1)
- (save-excursion
- (goto-char (window-start))
- (while (ignore-errors (re-search-forward re we t))
- (let ((i 0))
- (while (<= i swiper--subexps)
- (when (match-beginning i)
- (let ((overlay (make-overlay (match-beginning i)
- (match-end i)))
- (face
- (cond ((zerop swiper--subexps)
- (caddr swiper-faces))
- ((zerop i)
- (car swiper-faces))
- (t
- (nth (1+ (mod (1- i) (1- (length swiper-faces))))
- swiper-faces)))))
- (push overlay swiper--overlays)
- (overlay-put overlay 'face face)
- (overlay-put overlay 'priority i)
- (incf i))))))))))
- (when (/= (length helm-input) swiper--len)
- (setq swiper--len (length helm-input))
- (swiper--reanchor)))
-
-(defun swiper--binary (beg end)
- "Find anchor between BEG and END."
- (if (<= (- end beg) 10)
- (let ((min 1000)
- n
- ln
- d)
- (goto-char (point-min))
- (forward-line (1- beg))
- (while (< beg end)
- (beginning-of-line)
- (setq n (read (current-buffer)))
- (when (< (setq d (abs (- n swiper--anchor))) min)
- (setq min d)
- (setq ln beg))
- (incf beg)
- (forward-line 1))
- (goto-char (point-min))
- (when ln
- (forward-line (1- ln))))
- (let ((mid (+ beg (/ (- end beg) 2))))
- (goto-char (point-min))
- (forward-line mid)
- (beginning-of-line)
- (let ((n (read (current-buffer))))
- (if (> n swiper--anchor)
- (swiper--binary beg mid)
- (swiper--binary mid end))))))
-
-(defun swiper--update-sel ()
- "Update selection."
- (let* ((re (swiper--regex))
- (str (buffer-substring-no-properties
- (line-beginning-position)
- (line-end-position)))
+(defun swiper--update-input-ivy ()
+ "Called when `ivy' input is updated."
+ (swiper--cleanup)
+ (let* ((re (ivy--regex ivy-text))
+ (str ivy--current)
(num (if (string-match "^[0-9]+" str)
(string-to-number (match-string 0 str))
- 0))
- pt)
- (when (> (length re) 0)
- (with-current-buffer swiper--buffer
+ 0)))
+ (with-selected-window swiper--window
+ (goto-char (point-min))
+ (when (cl-plusp num)
(goto-char (point-min))
(forward-line (1- num))
- (when (re-search-forward re (point-max) t)
- (setq pt (match-beginning 0))))
- (when pt
- (with-selected-window
- (helm-persistent-action-display-window)
- (goto-char pt)
- (recenter)
- (swiper--update-input))))
- (with-current-buffer swiper--buffer
- (let ((ov (make-overlay
- (line-beginning-position)
- (1+ (line-end-position)))))
- (overlay-put ov 'face 'swiper-line-face)
- (push ov swiper--overlays)))))
-
-(defun swiper--reanchor ()
- "Move to a valid match closest to `swiper--anchor'."
- (with-helm-window
+ (isearch-range-invisible (line-beginning-position)
+ (line-end-position))
+ (unless (and (>= (point) (window-start))
+ (<= (point) (window-end swiper--window t)))
+ (recenter)))
+ (swiper--add-overlays re))))
+
+(defun swiper--add-overlays (re &optional beg end)
+ "Add overlays for RE regexp in visible part of the current buffer.
+BEG and END, when specified, are the point bounds."
+ (let ((ov (make-overlay
+ (line-beginning-position)
+ (1+ (line-end-position)))))
+ (overlay-put ov 'face 'swiper-line-face)
+ (overlay-put ov 'window swiper--window)
+ (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)))))
+ (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))
+ (let ((i 0))
+ (while (<= i ivy--subexps)
+ (when (match-beginning i)
+ (let ((overlay (make-overlay (match-beginning i)
+ (match-end i)))
+ (face
+ (cond ((zerop ivy--subexps)
+ (cl-caddr swiper-faces))
+ ((zerop i)
+ (car swiper-faces))
+ (t
+ (nth (1+ (mod (1- i) (1- (length swiper-faces))))
+ swiper-faces)))))
+ (push overlay swiper--overlays)
+ (overlay-put overlay 'face face)
+ (overlay-put overlay 'window swiper--window)
+ (overlay-put overlay 'priority i)))
+ (cl-incf i))))))))
+
+(defun swiper--action (x input)
+ "Goto line X and search for INPUT."
+ (if (null x)
+ (user-error "No candidates")
(goto-char (point-min))
- (if (re-search-forward (format "^%d " swiper--anchor) nil t)
- nil
- (forward-line 1)
- (swiper--binary 2 (1+ (count-lines (point) (point-max)))))
- (when (> (count-lines (point-min) (point-max)) 1)
- (forward-line -1)
- (helm-next-line 1))))
-
-(defvar swiper--subexps 1
- "Number of groups in `(swiper--regex)'.")
-
-(defvar swiper--regex-hash
- (make-hash-table :test 'equal)
- "Store pre-computed regex.")
-
-(defun swiper--regex ()
- "Re-build regex in case it has a space."
- (let ((hashed (gethash helm-input swiper--regex-hash)))
- (if hashed
- (prog1 (cdr hashed)
- (setq swiper--subexps (car hashed)))
- (cdr (puthash helm-input
- (let ((subs (split-string helm-input " +" t)))
- (if (= (length subs) 1)
- (cons
- (setq swiper--subexps 0)
- (car subs))
- (cons
- (setq swiper--subexps (length subs))
- (mapconcat
- (lambda (x) (format "\\(%s\\)" x))
- subs
- ".*"))))
- swiper--regex-hash)))))
-
-(defun swiper--action (x)
- "Goto line X."
- (goto-char (point-min))
- (forward-line (1- (read x)))
- (re-search-forward
- (swiper--regex) (line-end-position) t))
+ (forward-line (1- (read x)))
+ (re-search-forward
+ (ivy--regex input) (line-end-position) t)
+ (swiper--ensure-visible)
+ (when (/= (point) swiper--opoint)
+ (unless (and transient-mark-mode mark-active)
+ (push-mark swiper--opoint t)
+ (message "Mark saved where search started")))))
(provide 'swiper)