branch: master commit 6a874a4ed341920d2bcfcd78ebda885b66d89908 Author: Oleh Krehel <ohwoeo...@gmail.com> Commit: Oleh Krehel <ohwoeo...@gmail.com>
Add `ivy' back end * ivy.el: New completion back end. * swiper.el: Package doesn't depend on `helm'. (ivy): Depend on `ivy'. (swiper-completion-method): New defcustom. (swiper--window): New var. (swiper--helm-keymap): Rename from `swiper--keymap'. (swiper): Change to a dispatch. (swiper--init): New defun. (swiper--ivy): New command. (swiper--helm): New command. (swiper--cleanup): New defun. (swiper--update-input-helm): Rename from `swiper--update-input'. (swiper--update-input-ivy): New defun. (swiper--add-overlays): New defun. (swiper--update-sel): Update. (swiper--subexps): (swiper--regex-hash): (swiper--regex): Move to ivy. (swiper--action): Update. --- ivy.el | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ swiper.el | 220 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 396 insertions(+), 82 deletions(-) diff --git a/ivy.el b/ivy.el new file mode 100644 index 0000000..63ccbc1 --- /dev/null +++ b/ivy.el @@ -0,0 +1,258 @@ +;;; ivy.el --- Incremental Vertical completYon -*- lexical-binding: t -*- + +;; Copyright (C) 2015 Oleh Krehel + +;; Author: Oleh Krehel <ohwoeo...@gmail.com> +;; URL: https://github.com/abo-abo/ivy +;; Version: 0.1.0 + +;; This file is not 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 provides `ivy-read' as an alternative to +;; `completing-read' and similar functions. +;; +;; There's no intricate code to determine the best candidate. +;; Instead, the user can navigate to it with `ivy-next-line' and +;; `ivy-previous-line'. +;; +;; The matching is done by splitting the input text by spaces and +;; re-building it into a regex. +;; So "for example" is transformed into "\\(for\\).*\\(example\\)". + +;;; Code: +;;* Customization +(defgroup ivy nil + "Incremental vertical completion." + :group 'convenience) + +(defface ivy-current-match + '((t (:background "#e5b7c0"))) + "Face used by Ivy for highlighting first match.") + +(defcustom ivy-height 10 + "Number of lines for the minibuffer window." + :type 'integer) + +;;* User Visible +;;** Keymap +(defvar ivy-minibuffer-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-m") 'ivy-done) + (define-key map (kbd "C-n") 'ivy-next-line) + (define-key map (kbd "C-p") 'ivy-previous-line) + (define-key map (kbd "C-s") 'ivy-next-line) + (define-key map (kbd "C-r") 'ivy-previous-line) + (define-key map (kbd "SPC") 'self-insert-command) + (define-key map (kbd "DEL") 'ivy-backward-delete-char) + (define-key map (kbd "M-<") 'ivy-beginning-of-buffer) + (define-key map (kbd "M->") 'ivy-end-of-buffer) + map) + "Keymap used in the minibuffer.") + +;;** Commands +(defun ivy-done () + "Exit the minibuffer with the selected candidate." + (interactive) + (delete-minibuffer-contents) + (insert ivy--current) + (exit-minibuffer)) + +(defun ivy-next-line () + "Select the next completion candidate." + (interactive) + (unless (>= ivy--index (1- ivy--length)) + (incf ivy--index))) + +(defun ivy-beginning-of-buffer () + "Select the first completion candidate." + (interactive) + (setq ivy--index 0)) + +(defun ivy-end-of-buffer () + "Select the last completion candidate." + (interactive) + (setq ivy--index (1- ivy--length))) + +(defun ivy-previous-line () + "Select the previous completion candidate." + (interactive) + (unless (zerop ivy--index) + (decf ivy--index))) + +(defun ivy-backward-delete-char () + "Forward to `backward-delete-char'. +On error (read-only), quit without selecting." + (interactive) + (condition-case nil + (backward-delete-char 1) + (error + (minibuffer-keyboard-quit)))) + +;;** Entry Point +(defun ivy-read (prompt collection &optional update-fn) + "Read a string in the minibuffer, with completion. +PROMPT is a string to prompt with; normally it ends in a colon and a space. +COLLECTION is a list of strings. +UPDATE-FN is called each time the current candidate(s) is changed." + (setq ivy--index 0) + (setq ivy--old-re nil) + (setq ivy-text "") + (setq ivy--all-candidates collection) + (setq ivy--update-fn update-fn) + (unwind-protect + (minibuffer-with-setup-hook + #'ivy--minibuffer-setup + (read-from-minibuffer prompt)) + (remove-hook 'post-command-hook #'ivy--exhibit))) + +(defvar ivy-text "" + "Stores the user's string as it is typed in.") + +;;* Implementation +;;** Regex +(defvar ivy--subexps 0 + "Number of groups in the current `ivy--regex'.") + +(defvar ivy--regex-hash + (make-hash-table :test 'equal) + "Store pre-computed regex.") + +(defun ivy--regex (str) + "Re-build regex from STR in case it has a space." + (let ((hashed (gethash str ivy--regex-hash))) + (if hashed + (prog1 (cdr hashed) + (setq ivy--subexps (car hashed))) + (cdr (puthash str + (let ((subs (split-string str " +" t))) + (if (= (length subs) 1) + (cons + (setq ivy--subexps 0) + (car subs)) + (cons + (setq ivy--subexps (length subs)) + (mapconcat + (lambda (x) (format "\\(%s\\)" x)) + subs + ".*")))) + ivy--regex-hash))))) + +;;** Rest +(defun ivy--minibuffer-setup () + "Setup ivy completion in the minibuffer." + (set (make-local-variable 'completion-show-inline-help) nil) + (use-local-map (make-composed-keymap ivy-minibuffer-map + (current-local-map))) + (setq-local max-mini-window-height ivy-height) + (add-hook 'post-command-hook #'ivy--exhibit nil t) + ;; show completions with empty input + (ivy--exhibit)) + +(defvar ivy--all-candidates nil + "Store the candidates passed to `ivy-read'.") + +(defvar ivy--index 0 + "Store the index of the current candidate.") + +(defvar ivy--length 0 + "Store the amount of viable candidates.") + +(defvar ivy--current "" + "Current candidate.") + +(defvar ivy--update-fn nil + "Current function to call when current candidate(s) update.") + +(defun ivy--input () + "Return the current minibuffer input." + ;; assume one-line minibuffer input + (buffer-substring-no-properties + (minibuffer-prompt-end) + (line-end-position))) + +(defun ivy--cleanup () + "Delete the displayed completion candidates." + (save-excursion + (goto-char (minibuffer-prompt-end)) + (delete-region (line-end-position) (point-max)))) + +(defun ivy--exhibit () + "Insert Ivy completions display. +Should be run via minibuffer `post-command-hook'." + (setq ivy-text (ivy--input)) + (ivy--cleanup) + (let ((text (while-no-input + (ivy-completions + ivy-text + ivy--all-candidates))) + (buffer-undo-list t) + deactivate-mark) + (when ivy--update-fn + (funcall ivy--update-fn)) + ;; Do nothing if while-no-input was aborted. + (when (stringp text) + (save-excursion + (forward-line 1) + (insert text))))) + +(defvar ivy--old-re nil + "Store the old regexp.") + +(defvar ivy--old-cands nil + "Store the candidates matched by `ivy--old-re'.") + +(defun ivy-completions (name candidates) + "Return as text the current completions. +NAME is a string of words separated by spaces that is used to +build a regex. +CANDIDATES is a list of strings." + (let* ((re (ivy--regex name)) + (cands (if (equal re ivy--old-re) + ivy--old-cands + (setq ivy--old-re re) + (setq ivy--old-cands + (ignore-errors + (cl-remove-if-not + (lambda (x) (string-match re x)) + candidates)))))) + (setq ivy--length (length cands)) + ;; should do a re-anchor here + (when (>= ivy--index ivy--length) + (setq ivy--index (1- ivy--length))) + (if (null cands) + "" + (let ((index ivy--index)) + (if (< index (/ ivy-height 2)) + (setq cands + (cl-subseq cands 0 (min (1- ivy-height) ivy--length))) + (setq cands + (cl-subseq cands + (- index (/ ivy-height 2)) + (min (+ index (/ ivy-height 2)) + ivy--length))) + (setq index (min (/ ivy-height 2) + (1- (length cands))))) + (setq ivy--current (copy-sequence + (nth index cands))) + (setf (nth index cands) + (propertize ivy--current 'face 'ivy-current-match)) + (concat "\n" (mapconcat #'identity cands "\n")))))) + +(provide 'ivy) + +;;; ivy.el ends here diff --git a/swiper.el b/swiper.el index 3243069..6bc53d0 100644 --- a/swiper.el +++ b/swiper.el @@ -1,11 +1,11 @@ -;;; 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 ;; Author: Oleh Krehel <ohwoeo...@gmail.com> ;; URL: https://github.com/abo-abo/swiper ;; Version: 0.1.0 -;; Package-Requires: ((helm "1.6.7") (emacs "24.1")) +;; Package-Requires: ((emacs "24.1")) ;; Keywords: matching ;; This file is not part of GNU Emacs @@ -26,23 +26,30 @@ ;;; 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 can be either `helm' or `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-") +(defcustom swiper-completion-method 'helm + "Method to select a candidate from a list of strings." + :type '(choice + (const :tag "Helm" helm) + (const :tag "Ivy" ivy))) + (defface swiper-match-face-1 - '((t (:background "#FEEA89"))) + '((t (:background "#FEEA89"))) "Face for `swiper' matches.") (defface swiper-match-face-2 @@ -70,6 +77,10 @@ (defvar swiper--buffer nil "Store current buffer.") +(defvar swiper--window nil + "Store current window. +This is necessary for `window-start' while in minibuffer.") + (defalias 'swiper-font-lock-ensure (if (fboundp 'font-lock-ensure) 'font-lock-ensure @@ -96,20 +107,53 @@ (zerop (forward-line 1))) (nreverse candidates)))) -(defvar swiper--keymap - (let ((map (copy-keymap helm-map))) +(defvar swiper-helm-keymap + (let ((map (make-sparse-keymap))) (define-key map (kbd "C-s") 'helm-next-line) (define-key map (kbd "C-r") 'helm-previous-line) map) - "Allows you to go to next and previous hit isearch-style") + "Allows you to go to next and previous hit isearch-style.") ;;;###autoload (defun swiper () - "Interactive `occur' using `helm'." + "`isearch' with an overview." (interactive) + (if (and (eq 'swiper-completion-method 'helm) + (featurep 'helm)) + (swiper--helm) + (swiper--ivy))) + +(defun swiper--init () + "Perform initialization common to both completion methods." (deactivate-mark) (setq swiper--len 0) (setq swiper--anchor (line-number-at-pos)) + (setq swiper--buffer (current-buffer)) + (setq swiper--window (selected-window))) + +(defun swiper--ivy () + "`isearch' with an overview using `ivy'." + (interactive) + (ido-mode -1) + (swiper--init) + (unwind-protect + (let ((res (ivy-read "pattern: " + (swiper--candidates) + #'swiper--update-input-ivy))) + (goto-char (point-min)) + (forward-line (1- (read res))) + (re-search-forward + (ivy--regex ivy-text) + (line-end-position) + t)) + (ido-mode 1) + (swiper--cleanup))) + +(defun swiper--helm () + "`isearch' with an overview using `helm'." + (interactive) + (require 'helm) + (swiper--init) (unwind-protect (let ((helm-display-function (lambda (buf) @@ -121,32 +165,35 @@ (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) + #'swiper--update-input-helm) (add-hook 'helm-after-update-hook #'swiper--reanchor))) - (match-strict . (lambda (x) (ignore-errors - (string-match (swiper--regex helm-input) x)))) + (match-strict . (lambda (x) + (ignore-errors + (string-match (ivy--regex helm-input) x)))) (candidates . ,(swiper--candidates)) (filtered-candidate-transformer helm-fuzzy-highlight-matches) (action . swiper--action)) - :keymap swiper--keymap + :keymap (make-composed-keymap + swiper-helm-keymap + helm-map) :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))))) + (remove-hook 'helm-move-selection-after-hook #'swiper--update-sel) + (remove-hook 'helm-update-hook #'swiper--update-input-helm) + (remove-hook 'helm-after-update-hook #'swiper--reanchor) + (swiper--cleanup))) + +(defun swiper--cleanup () + "Clean up the overlays." + (while swiper--overlays + (delete-overlay (pop swiper--overlays)))) (defvar swiper--overlays nil "Store overlays.") @@ -155,40 +202,76 @@ "A line number to which the search should be anchored.") (defvar swiper--len 0 - "The last length of `helm-input' for which an anchoring was made.") + "The last length of input for which an anchoring was made.") -(defun swiper--update-input () +(defun swiper--update-input-helm () "Update selection." + (swiper--cleanup) (with-current-buffer swiper--buffer - (let ((re (swiper--regex helm-input)) - (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) - (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 'priority i) - (cl-incf i)))))))))) + (swiper--add-overlays + (ivy--regex helm-input) + (window-start swiper--window) + (window-end swiper--window t))) (when (/= (length helm-input) swiper--len) (setq swiper--len (length helm-input)) (swiper--reanchor))) +(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))) + (with-current-buffer swiper--buffer + (goto-char (point-min)) + (when (plusp num) + (goto-char (point-min)) + (forward-line (1- num)) + (setf (window-point swiper--window) + (point))) + (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)) + (swiper--add-overlays + re + (save-excursion + (forward-line (- (window-height swiper--window))) + (point)) + (save-excursion + (forward-line (window-height swiper--window)) + (point)))))) + +(defun swiper--add-overlays (re beg end) + "Add overlays for RE regexp in current buffer between BEG and END." + (when (> (length re) 1) + (save-excursion + (goto-char beg) + ;; RE can become an invalid regexp + (while (ignore-errors (re-search-forward re end t)) + (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--binary (beg end) "Find anchor between BEG and END." (if (<= (- end beg) 10) @@ -220,7 +303,7 @@ (defun swiper--update-sel () "Update selection." - (let* ((re (swiper--regex helm-input)) + (let* ((re (ivy--regex helm-input)) (str (buffer-substring-no-properties (line-beginning-position) (line-end-position))) @@ -239,7 +322,7 @@ (helm-persistent-action-display-window) (goto-char pt) (recenter) - (swiper--update-input)))) + (swiper--update-input-helm)))) (with-current-buffer swiper--buffer (let ((ov (make-overlay (line-beginning-position) @@ -259,39 +342,12 @@ (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 (str) - "Re-build regex from STR in case it has a space." - (let ((hashed (gethash str swiper--regex-hash))) - (if hashed - (prog1 (cdr hashed) - (setq swiper--subexps (car hashed))) - (cdr (puthash str - (let ((subs (split-string str " +" 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 helm-input) (line-end-position) t)) + (ivy--regex helm-input) (line-end-position) t)) (provide 'swiper)