X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/00810d6cd77f1aea0b7f291b49779120c000d9ae..70c8c8adf750b71c304647696350b44a18162144:/ivy.el diff --git a/ivy.el b/ivy.el index a58df5694..1a92d0d15 100644 --- a/ivy.el +++ b/ivy.el @@ -57,7 +57,7 @@ "Face used by Ivy to issue a match required prompt.") (defface ivy-subdir - '((t (:weight bold))) + '((t (:inherit 'dired-directory))) "Face used by Ivy for highlighting subdirs in the alternatives.") (defface ivy-remote @@ -87,13 +87,17 @@ This is usually meant as a quick exit out of the minibuffer." Only \"./\" and \"../\" apply here. They appear in reverse order." :type 'list) -;;* User Visible -;;** Keymap +(defcustom ivy-use-virtual-buffers nil + "When non-nil, add `recentf-mode' and bookmarks to the list of buffers." + :type 'boolean) + +;;* Keymap (require 'delsel) (defvar ivy-minibuffer-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-m") 'ivy-done) (define-key map (kbd "C-j") 'ivy-alt-done) + (define-key map (kbd "TAB") 'ivy-partial-or-done) (define-key map (kbd "C-n") 'ivy-next-line) (define-key map (kbd "C-p") 'ivy-previous-line) (define-key map (kbd "") 'ivy-next-line) @@ -102,6 +106,7 @@ Only \"./\" and \"../\" apply here. They appear in reverse order." (define-key map (kbd "C-r") 'ivy-previous-line-or-history) (define-key map (kbd "SPC") 'self-insert-command) (define-key map (kbd "DEL") 'ivy-backward-delete-char) + (define-key map (kbd "M-DEL") 'ivy-backward-kill-word) (define-key map (kbd "M-<") 'ivy-beginning-of-buffer) (define-key map (kbd "M->") 'ivy-end-of-buffer) (define-key map (kbd "") 'ivy-beginning-of-buffer) @@ -117,18 +122,38 @@ Only \"./\" and \"../\" apply here. They appear in reverse order." map) "Keymap used in the minibuffer.") +(defvar ivy-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [remap switch-to-buffer] 'ivy-switch-buffer) + map) + "Keymap for `ivy-mode'.") + +;;* Globals +(cl-defstruct ivy-state + prompt collection + predicate require-match initial-input + history preselect keymap update-fn sort + ;; The window in which `ivy-read' was called + window + action + unwind + re-builder + matcher + ;; When this is non-nil, call it for each input change to get new candidates + dynamic-collection) + +(defvar ivy-last nil + "The last parameters passed to `ivy-read'.") + +(defsubst ivy-set-action (action) + (setf (ivy-state-action ivy-last) action)) + (defvar ivy-history nil "History list of candidates entered in the minibuffer. Maximum length of the history list is determined by the value of `history-length', which see.") -(defvar ivy-require-match t - "Store require-match. See `completing-read'.") - -(defvar ivy-def nil - "Store the default completion value. See `completing-read'.") - (defvar ivy--directory nil "Current directory when completing file names.") @@ -138,9 +163,6 @@ of `history-length', which see.") (defvar ivy-text "" "Store the user's string as it is typed in.") -(defvar ivy-window nil - "Store the window in which `ivy-read' was called.") - (defvar ivy--current "" "Current candidate.") @@ -151,27 +173,18 @@ of `history-length', which see.") "Store 'done if the completion was successfully selected. Otherwise, store nil.") -(defvar ivy--action nil - "Store a function to call at the end of `ivy--read'.") - -(defvar ivy--persistent-action nil - "Store a function to call for current candidate without exiting.") - (defvar ivy--all-candidates nil "Store the candidates passed to `ivy-read'.") (defvar ivy--default nil "Default initial input.") -(defvar ivy--update-fn nil - "Current function to call when current candidate(s) update.") - (defvar ivy--prompt nil "Store the format-style prompt. When non-nil, it should contain one %d.") (defvar ivy--prompt-extra "" - "Temporary modifications to the prompt") + "Temporary modifications to the prompt.") (defvar ivy--old-re nil "Store the old regexp.") @@ -184,41 +197,59 @@ When non-nil, it should contain one %d.") (defvar Info-current-file) -;;** Commands +(defmacro ivy-quit-and-run (&rest body) + "Quit the minibuffer and run BODY afterwards." + `(progn + (put 'quit 'error-message "") + (run-at-time nil nil + (lambda () + (put 'quit 'error-message "Quit") + ,@body)) + (minibuffer-keyboard-quit))) + +(defun ivy--done (text) + "Insert TEXT and exit minibuffer." + (if (and ivy--directory + (not (eq (ivy-state-history ivy-last) 'grep-files-history))) + (insert (expand-file-name + text ivy--directory)) + (insert text)) + (setq ivy-exit 'done) + (exit-minibuffer)) + +;;* Commands (defun ivy-done () "Exit the minibuffer with the selected candidate." (interactive) (delete-minibuffer-contents) - (when (cond (ivy--directory - (if (zerop ivy--length) - (if (or (not (eq confirm-nonexistent-file-or-buffer t)) - (equal " (confirm)" ivy--prompt-extra)) - (progn - (insert - (expand-file-name ivy-text ivy--directory)) - (setq ivy-exit 'done)) - (setq ivy--prompt-extra " (confirm)") - (insert ivy-text) - (ivy--exhibit) - nil) - (insert - (expand-file-name - ivy--current ivy--directory)) - (setq ivy-exit 'done))) - ((zerop ivy--length) - (if (memq ivy-require-match - '(nil confirm confirm-after-completion)) - (progn - (insert ivy-text) - (setq ivy-exit 'done)) - (setq ivy--prompt-extra " (match required)") - (insert ivy-text) - (ivy--exhibit) - nil)) - (t - (insert ivy--current) - (setq ivy-exit 'done))) - (exit-minibuffer))) + (cond ((> ivy--length 0) + (ivy--done ivy--current)) + ((memq (ivy-state-collection ivy-last) + '(read-file-name-internal internal-complete-buffer)) + (if (or (not (eq confirm-nonexistent-file-or-buffer t)) + (equal " (confirm)" ivy--prompt-extra)) + (ivy--done ivy-text) + (setq ivy--prompt-extra " (confirm)") + (insert ivy-text) + (ivy--exhibit))) + ((memq (ivy-state-require-match ivy-last) + '(nil confirm confirm-after-completion)) + (ivy--done ivy-text)) + (t + (setq ivy--prompt-extra " (match required)") + (insert ivy-text) + (ivy--exhibit)))) + +(defun ivy-build-tramp-name (x) + "Reconstruct X into a path. +Is is a cons cell, related to `tramp-get-completion-function'." + (let ((user (car x)) + (domain (cadr x))) + (if user + (concat user "@" domain) + domain))) + +(declare-function tramp-get-completion-function "tramp") (defun ivy-alt-done (&optional arg) "Exit the minibuffer with the selected candidate. @@ -227,20 +258,74 @@ When ARG is t, exit with current text, ignoring the candidates." (if arg (ivy-immediate-done) (let (dir) - (if (and ivy--directory - (or - (and - (not (string= ivy--current "./")) - (cl-plusp ivy--length) - (file-directory-p - (setq dir (expand-file-name - ivy--current ivy--directory)))) - (prog1 (string-match ":" ivy-text) - (setq dir ivy-text)))) - (progn - (ivy--cd dir) - (ivy--exhibit)) - (ivy-done))))) + (cond ((and ivy--directory + (or + (and + (not (string= ivy--current "./")) + (cl-plusp ivy--length) + (file-directory-p + (setq dir (expand-file-name + ivy--current ivy--directory)))))) + (ivy--cd dir) + (ivy--exhibit)) + ((string-match "^/\\([^/]+?\\):\\(?:\\(.*\\)@\\)?" ivy-text) + (let ((method (match-string 1 ivy-text)) + (user (match-string 2 ivy-text)) + res) + (dolist (x (tramp-get-completion-function method)) + (setq res (append res (funcall (car x) (cadr x))))) + (setq res (delq nil res)) + (when user + (dolist (x res) + (setcar x user))) + (setq res (cl-delete-duplicates res :test 'equal)) + (let ((host (ivy-read "Find File: " + (mapcar #'ivy-build-tramp-name res)))) + (when host + (setq ivy--directory "/") + (ivy--cd (concat "/" method ":" host ":")))))) + (t + (ivy-done)))))) + +(defcustom ivy-tab-space nil + "When non-nil, `ivy-partial-or-done' should insert a space." + :type 'boolean) + +(defun ivy-partial-or-done () + "Complete the minibuffer text as much as possible. +When called twice in a row, exit the minibuffer with the current +candidate." + (interactive) + (if (and (eq (ivy-state-collection ivy-last) 'read-file-name-internal) + (string-match "^/" ivy-text)) + (let ((default-directory ivy--directory)) + (minibuffer-complete) + (setq ivy-text (ivy--input)) + (when (and (file-directory-p ivy-text) + (= ivy--length 1)) + (ivy--cd (expand-file-name ivy-text)))) + (or (ivy-partial) + (if (eq this-command last-command) + (ivy-done) + (ivy-alt-done))))) + +(defun ivy-partial () + "Complete the minibuffer text as much as possible." + (interactive) + (let* ((parts (or (split-string ivy-text " " t) (list ""))) + (postfix (car (last parts))) + (completion-ignore-case t) + (new (try-completion postfix + (mapcar (lambda (str) (substring str (string-match postfix str))) + ivy--old-cands)))) + (cond ((eq new t) nil) + ((string= new ivy-text) nil) + (new + (delete-region (minibuffer-prompt-end) (point-max)) + (setcar (last parts) new) + (insert (mapconcat #'identity parts " ") + (if ivy-tab-space " " "")) + t)))) (defun ivy-immediate-done () "Exit the minibuffer with the current input." @@ -250,6 +335,26 @@ When ARG is t, exit with current text, ignoring the candidates." (setq ivy-exit 'done) (exit-minibuffer)) +(defun ivy-resume () + "Resume the last completion session." + (interactive) + (ivy-read + (ivy-state-prompt ivy-last) + (ivy-state-collection ivy-last) + :predicate (ivy-state-predicate ivy-last) + :require-match (ivy-state-require-match ivy-last) + :initial-input ivy-text + :history (ivy-state-history ivy-last) + :preselect (regexp-quote ivy--current) + :keymap (ivy-state-keymap ivy-last) + :update-fn (ivy-state-update-fn ivy-last) + :sort (ivy-state-sort ivy-last) + :action (ivy-state-action ivy-last) + :unwind (ivy-state-unwind ivy-last) + :re-builder (ivy-state-re-builder ivy-last) + :matcher (ivy-state-matcher ivy-last) + :dynamic-collection (ivy-state-dynamic-collection ivy-last))) + (defun ivy-beginning-of-buffer () "Select the first completion candidate." (interactive) @@ -313,18 +418,16 @@ If the input is empty, select the previous history element instead." (interactive "p") (ivy-next-line arg) (ivy--exhibit) - (when ivy--persistent-action - (with-selected-window ivy-window - (funcall ivy--persistent-action ivy--current)))) + (with-selected-window (ivy-state-window ivy-last) + (funcall (ivy-state-action ivy-last)))) (defun ivy-previous-line-and-call (&optional arg) "Move cursor vertically down ARG candidates." (interactive "p") (ivy-previous-line arg) (ivy--exhibit) - (when ivy--persistent-action - (with-selected-window ivy-window - (funcall ivy--persistent-action ivy--current)))) + (with-selected-window (ivy-state-window ivy-last) + (funcall (ivy-state-action ivy-last)))) (defun ivy-previous-history-element (arg) "Forward to `previous-history-element' with ARG." @@ -341,7 +444,7 @@ If the input is empty, select the previous history element instead." (ivy--maybe-scroll-history)) (defun ivy--maybe-scroll-history () - "If the selected history element holds an index, scroll there." + "If the selected history element has an index, scroll there." (let ((idx (ignore-errors (get-text-property (minibuffer-prompt-end) @@ -379,6 +482,19 @@ On error (read-only), call `ivy-on-del-error-function'." (when ivy-on-del-error-function (funcall ivy-on-del-error-function)))))) +(defun ivy-backward-kill-word () + "Forward to `backward-kill-word'." + (interactive) + (if (and ivy--directory (= (minibuffer-prompt-end) (point))) + (progn + (ivy--cd (file-name-directory + (directory-file-name + (expand-file-name + ivy--directory)))) + (ivy--exhibit)) + (ignore-errors + (backward-kill-word 1)))) + (defvar ivy--regexp-quote 'regexp-quote "Store the regexp quoting state.") @@ -409,11 +525,20 @@ For each entry, nil means no sorting. The entry associated to t is used for all fall-through cases.") (defvar ivy-re-builders-alist - '((t . ivy--regex)) + '((t . ivy--regex-plus)) "An alist of regex building functions for each collection function. -Each function should take a string and return a valid regex. +Each function should take a string and return a valid regex or a +regex sequence (see below). + The entry associated to t is used for all fall-through cases. -Possible choices: `ivy--regex', `regexp-quote'.") +Possible choices: `ivy--regex', `regexp-quote', `ivy--regex-plus'. + +In case a function returns a list, it should look like this: +'((\"matching-regexp\" . t) (\"non-matching-regexp\") ...). + +The matches will be filtered in a sequence, you can mix the +regexps that should match and that should not match as you +like.") (defcustom ivy-sort-max-size 30000 "Sorting won't be done for collections larger than this." @@ -443,7 +568,8 @@ Directories come first." ;;** Entry Point (cl-defun ivy-read (prompt collection &key predicate require-match initial-input - history preselect keymap update-fn sort) + history preselect keymap update-fn sort + action unwind re-builder matcher dynamic-collection) "Read a string in the minibuffer, with completion. PROMPT is a string to prompt with; normally it ends in a colon @@ -462,18 +588,47 @@ the ones that match INITIAL-INPUT. UPDATE-FN is called each time the current candidate(s) is changed. -When SORT is t, refer to `ivy-sort-functions-alist' for sorting." +When SORT is t, refer to `ivy-sort-functions-alist' for sorting. + +ACTION is a lambda to call after a result was selected. + +UNWIND is a lambda to call before exiting. + +RE-BUILDER is a lambda that transforms text into a regex. + +MATCHER can completely override matching. + +DYNAMIC-COLLECTION is a function to call to update the list of +candidates with each input." + (setq ivy-last + (make-ivy-state + :prompt prompt + :collection collection + :predicate predicate + :require-match require-match + :initial-input initial-input + :history history + :preselect preselect + :keymap keymap + :update-fn update-fn + :sort sort + :action action + :window (selected-window) + :unwind unwind + :re-builder re-builder + :matcher matcher + :dynamic-collection dynamic-collection)) (setq ivy--directory nil) - (setq ivy-require-match require-match) - (setq ivy-def preselect) - (setq ivy-window (selected-window)) (setq ivy--regex-function - (or (and (functionp collection) + (or re-builder + (and (functionp collection) (cdr (assoc collection ivy-re-builders-alist))) (cdr (assoc t ivy-re-builders-alist)) 'ivy--regex)) (setq ivy--subexps 0) (setq ivy--regexp-quote 'regexp-quote) + (setq ivy--old-text "") + (setq ivy-text "") (let (coll sort-fn) (cond ((eq collection 'Info-read-node-name-1) (if (equal Info-current-file "dir") @@ -485,6 +640,7 @@ When SORT is t, refer to `ivy-sort-functions-alist' for sorting." (setq coll (all-completions "" collection predicate)))) ((eq collection 'read-file-name-internal) (setq ivy--directory default-directory) + (require 'dired) (setq coll (ivy--sorted-files default-directory)) (when initial-input @@ -493,14 +649,7 @@ When SORT is t, refer to `ivy-sort-functions-alist' for sorting." (setq coll (cons initial-input coll))) (setq initial-input nil))) ((eq collection 'internal-complete-buffer) - (setq coll - (mapcar (lambda (x) - (if (with-current-buffer x - (file-remote-p - (abbreviate-file-name default-directory))) - (propertize x 'face 'ivy-remote) - x)) - (all-completions "" collection predicate)))) + (setq coll (ivy--buffer-list "" ivy-use-virtual-buffers))) ((or (functionp collection) (vectorp collection) (listp (car collection))) @@ -526,15 +675,15 @@ When SORT is t, refer to `ivy-sort-functions-alist' for sorting." coll)) (setq coll (cons preselect coll)))) (setq ivy--index (or + (and dynamic-collection + ivy--index) (and preselect (ivy--preselect-index coll initial-input preselect)) 0)) (setq ivy--old-re nil) (setq ivy--old-cands nil) - (setq ivy-text "") (setq ivy--all-candidates coll) - (setq ivy--update-fn update-fn) (setq ivy-exit nil) (setq ivy--default (or (thing-at-point 'symbol) "")) (setq ivy--prompt @@ -546,12 +695,13 @@ When SORT is t, refer to `ivy-sort-functions-alist' for sorting." prompt) (t nil))) - (setq ivy--action nil) (prog1 (unwind-protect (minibuffer-with-setup-hook #'ivy--minibuffer-setup (let* ((hist (or history 'ivy-history)) + (minibuffer-completion-table collection) + (minibuffer-completion-predicate predicate) (res (read-from-minibuffer prompt initial-input @@ -563,9 +713,11 @@ When SORT is t, refer to `ivy-sort-functions-alist' for sorting." (delete ivy-text (cdr (symbol-value hist))))) res))) - (remove-hook 'post-command-hook #'ivy--exhibit)) - (when ivy--action - (funcall ivy--action))))) + (remove-hook 'post-command-hook #'ivy--exhibit) + (when (setq unwind (ivy-state-unwind ivy-last)) + (funcall unwind))) + (when (setq action (ivy-state-action ivy-last)) + (funcall action))))) (defun ivy-completing-read (prompt collection &optional predicate require-match initial-input @@ -578,8 +730,7 @@ it can be used for `completing-read-function'. PROMPT is a string to prompt with; normally it ends in a colon and a space. COLLECTION can be a list of strings, an alist, an obarray or a hash table. PREDICATE limits completion to a subset of COLLECTION. - -REQUIRE-MATCH is stored into `ivy-require-match'. See `completing-read'. +REQUIRE-MATCH is considered boolean. See `completing-read'. INITIAL-INPUT is a string that can be inserted into the minibuffer initially. _HISTORY is ignored for now. DEF is the default value. @@ -597,14 +748,19 @@ The history, defaults and input-method arguments are ignored for now." ;;;###autoload (define-minor-mode ivy-mode - "Toggle Ivy mode on or off. + "Toggle Ivy mode on or off. With ARG, turn Ivy mode on if arg is positive, off otherwise. Turning on Ivy mode will set `completing-read-function' to `ivy-completing-read'. +Global bindings: +\\{ivy-mode-map} + +Minibuffer bindings: \\{ivy-minibuffer-map}" :group 'ivy :global t + :keymap ivy-mode-map :lighter " ivy" (if ivy-mode (setq completing-read-function 'ivy-completing-read) @@ -613,6 +769,7 @@ Turning on Ivy mode will set `completing-read-function' to (defun ivy--preselect-index (candidates initial-input preselect) "Return the index in CANDIDATES filtered by INITIAL-INPUT for PRESELECT." (when initial-input + (setq initial-input (ivy--regex-plus initial-input)) (setq candidates (cl-remove-if-not (lambda (x) @@ -633,6 +790,24 @@ Turning on Ivy mode will set `completing-read-function' to (make-hash-table :test 'equal) "Store pre-computed regex.") +(defun ivy--split (str) + "Split STR into a list by single spaces. +The remaining spaces stick to their left. +This allows to \"quote\" N spaces by inputting N+1 spaces." + (let ((len (length str)) + (start 0) + res s) + (while (and (string-match " +" str start) + (< start len)) + (setq s (substring str start (1- (match-end 0)))) + (unless (= (length s) 0) + (push s res)) + (setq start (match-end 0))) + (setq s (substring str start)) + (unless (= (length s) 0) + (push s res)) + (nreverse res))) + (defun ivy--regex (str &optional greedy) "Re-build regex from STR in case it has a space. When GREEDY is non-nil, join words in a greedy way." @@ -642,7 +817,7 @@ When GREEDY is non-nil, join words in a greedy way." (prog1 (cdr hashed) (setq ivy--subexps (car hashed))) (cdr (puthash str - (let ((subs (split-string str " +" t))) + (let ((subs (ivy--split str))) (if (= (length subs) 1) (cons (setq ivy--subexps 0) @@ -660,6 +835,44 @@ When GREEDY is non-nil, join words in a greedy way." ".*?"))))) ivy--regex-hash))))) +(defun ivy--regex-ignore-order (str) + "Re-build regex from STR by splitting it on spaces. +Ignore the order of each group." + (let* ((subs (split-string str " +" t)) + (len (length subs))) + (cl-case len + (1 + (setq ivy--subexps 0) + (car subs)) + (t + (setq ivy--subexps len) + (let ((all (mapconcat #'identity subs "\\|"))) + (mapconcat + (lambda (x) + (if (string-match "^\\\\(.*\\\\)$" x) + x + (format "\\(%s\\)" x))) + (make-list len all) + ".*?")))))) + +(defun ivy--regex-plus (str) + "Build a regex sequence from STR. +Spaces are wild, everything before \"!\" should match. +Everything after \"!\" should not match." + (let ((parts (split-string str "!" t))) + (cl-case (length parts) + (0 + "") + (1 + (ivy--regex (car parts))) + (2 + (let ((res + (mapcar #'list + (split-string (cadr parts) " " t)))) + (cons (cons (ivy--regex (car parts)) t) + res))) + (t (error "Unexpected: use only one !"))))) + ;;** Rest (defun ivy--minibuffer-setup () "Setup ivy completion in the minibuffer." @@ -685,19 +898,17 @@ When GREEDY is non-nil, join words in a greedy way." (goto-char (minibuffer-prompt-end)) (delete-region (line-end-position) (point-max)))) -(defvar ivy--dynamic-function nil - "When this is non-nil, call it for each input change to get new candidates.") - (defvar ivy--full-length nil - "When `ivy--dynamic-function' is non-nil, this can be the total amount of candidates.") + "When :dynamic-collection is non-nil, this can be the total amount of candidates.") -(defvar ivy--old-text nil +(defvar ivy--old-text "" "Store old `ivy-text' for dynamic completion.") (defun ivy--insert-prompt () "Update the prompt according to `ivy--prompt'." (when ivy--prompt - (unless (memq this-command '(ivy-done ivy-alt-done)) + (unless (memq this-command '(ivy-done ivy-alt-done ivy-partial-or-done + counsel-find-symbol)) (setq ivy--prompt-extra "")) (let (head tail) (if (string-match "\\(.*\\): $" ivy--prompt) @@ -716,7 +927,7 @@ When GREEDY is non-nil, join words in a greedy way." (if ivy--directory (abbreviate-file-name ivy--directory) "")) - (or (and ivy--dynamic-function + (or (and (ivy-state-dynamic-collection ivy-last) ivy--full-length) ivy--length)))) (save-excursion @@ -748,35 +959,50 @@ When GREEDY is non-nil, join words in a greedy way." "Insert Ivy completions display. Should be run via minibuffer `post-command-hook'." (setq ivy-text (ivy--input)) - (if ivy--dynamic-function + (if (ivy-state-dynamic-collection ivy-last) ;; while-no-input would cause annoying ;; "Waiting for process to die...done" message interruptions (let ((inhibit-message t)) - (while-no-input - (unless (equal ivy--old-text ivy-text) - (let ((store ivy--dynamic-function) - (ivy--dynamic-function nil)) - (setq ivy--all-candidates (funcall store ivy-text))) - (setq ivy--old-text ivy-text)) - (ivy--insert-minibuffer (ivy--format ivy--all-candidates)))) - (when ivy--directory - (if (string-match "/$" ivy-text) - (if (member ivy-text ivy--all-candidates) - (ivy--cd (expand-file-name ivy-text ivy--directory)) - (when (string-match "//$" ivy-text) - (ivy--cd "/"))) - (if (string-match "~$" ivy-text) - (ivy--cd (expand-file-name "~/"))))) + (while-no-input + (unless (equal ivy--old-text ivy-text) + (cl-letf ((store (ivy-state-dynamic-collection ivy-last)) + ((ivy-state-dynamic-collection ivy-last) nil)) + (setq ivy--all-candidates (funcall store ivy-text)))) + (ivy--insert-minibuffer (ivy--format ivy--all-candidates)))) + (cond (ivy--directory + (if (string-match "/$" ivy-text) + (if (member ivy-text ivy--all-candidates) + (ivy--cd (expand-file-name ivy-text ivy--directory)) + (when (string-match "//$" ivy-text) + (ivy--cd "/"))) + (if (string-match "~$" ivy-text) + (ivy--cd (expand-file-name "~/"))))) + ((eq (ivy-state-collection ivy-last) 'internal-complete-buffer) + (when (or (and (string-match "^ " ivy-text) + (not (string-match "^ " ivy--old-text))) + (and (string-match "^ " ivy--old-text) + (not (string-match "^ " ivy-text)))) + (setq ivy--all-candidates + (if (and (> (length ivy-text) 0) + (eq (aref ivy-text 0) + ?\ )) + (ivy--buffer-list " ") + (ivy--buffer-list "" ivy-use-virtual-buffers))) + (setq ivy--old-re nil)))) (ivy--insert-minibuffer (ivy--format - (ivy--filter ivy-text ivy--all-candidates))))) + (ivy--filter ivy-text ivy--all-candidates)))) + (setq ivy--old-text ivy-text)) (defun ivy--insert-minibuffer (text) - (ivy--cleanup) - (let ((buffer-undo-list t) + "Insert TEXT into minibuffer with appropriate cleanup." + (let ((resize-mini-windows nil) + (buffer-undo-list t) + (update-fn (ivy-state-update-fn ivy-last)) deactivate-mark) - (when ivy--update-fn - (funcall ivy--update-fn)) + (ivy--cleanup) + (when update-fn + (funcall update-fn)) (ivy--insert-prompt) ;; Do nothing if while-no-input was aborted. (when (stringp text) @@ -788,34 +1014,53 @@ Should be run via minibuffer `post-command-hook'." "Propertize STR with FACE. `font-lock-append-text-property' is used, since it's better than `propertize' or `add-face-text-property' in this case." - (ignore-errors - (font-lock-append-text-property 0 (length str) 'face face str)) + (require 'colir) + (condition-case nil + (colir-blend-face-background 0 (length str) face str) + (error + (ignore-errors + (font-lock-append-text-property 0 (length str) 'face face str)))) str) (defun ivy--filter (name candidates) - "Return the matches for NAME for CANDIDATES. + "Return all items that match NAME in CANDIDATES. CANDIDATES are assumed to be static." (let* ((re (funcall ivy--regex-function name)) - (cands (cond ((and (equal re ivy--old-re) - ivy--old-cands) - ivy--old-cands) - ((and ivy--old-re - (not (string-match "\\\\" ivy--old-re)) - (not (equal ivy--old-re "")) - (memq (cl-search - (if (string-match "\\\\)$" ivy--old-re) - (substring ivy--old-re 0 -2) - ivy--old-re) - re) '(0 2))) - (ignore-errors - (cl-remove-if-not - (lambda (x) (string-match re x)) - ivy--old-cands))) - (t - (ignore-errors - (cl-remove-if-not - (lambda (x) (string-match re x)) - candidates))))) + (matcher (ivy-state-matcher ivy-last)) + (cands (cond + (matcher + (let ((ivy--old-re re)) + (cl-remove-if-not matcher candidates))) + ((and (equal re ivy--old-re) + ivy--old-cands) + ivy--old-cands) + ((and ivy--old-re + (stringp re) + (stringp ivy--old-re) + (not (string-match "\\\\" ivy--old-re)) + (not (equal ivy--old-re "")) + (memq (cl-search + (if (string-match "\\\\)$" ivy--old-re) + (substring ivy--old-re 0 -2) + ivy--old-re) + re) '(0 2))) + (ignore-errors + (cl-remove-if-not + (lambda (x) (string-match re x)) + ivy--old-cands))) + (t + (let ((re-list (if (stringp re) (list (cons re t)) re)) + (res candidates)) + (dolist (re re-list) + (setq res + (ignore-errors + (funcall + (if (cdr re) + #'cl-remove-if-not + #'cl-remove-if) + `(lambda (x) (string-match ,(car re) x)) + res)))) + res)))) (tail (nthcdr ivy--index ivy--old-cands)) idx) (when (and tail ivy--old-cands) @@ -834,11 +1079,37 @@ CANDIDATES are assumed to be static." (setq ivy--index (or idx 0)))) (when (and (string= name "") (not (equal ivy--old-re ""))) (setq ivy--index - (or (cl-position ivy-def cands :test 'equal) + (or (cl-position (ivy-state-preselect ivy-last) + cands :test 'equal) ivy--index))) - (setq ivy--old-re re) + (setq ivy--old-re (if cands re "")) (setq ivy--old-cands cands))) +(defvar ivy-format-function 'ivy-format-function-default + "Function to transform the list of candidates into a string. +This string will be inserted into the minibuffer.") + +(defun ivy-format-function-default (cands) + "Transform CANDS into a string for minibuffer." + (let ((ww (window-width))) + (mapconcat + (lambda (s) + (if (> (length s) ww) + (concat (substring s 0 (- ww 3)) "...") + s)) + cands "\n"))) + +(defun ivy-format-function-arrow (cands) + "Transform CANDS into a string for minibuffer." + (let ((i -1)) + (mapconcat + (lambda (s) + (concat (if (eq (cl-incf i) ivy--index) + "==> " + " ") + s)) + cands "\n"))) + (defun ivy--format (cands) "Return a string for CANDS suitable for display in the minibuffer. CANDS is a list of strings." @@ -846,7 +1117,7 @@ CANDS is a list of strings." (when (>= ivy--index ivy--length) (setq ivy--index (max (1- ivy--length) 0))) (if (null cands) - "" + (setq ivy--current "") (let* ((half-height (/ ivy-height 2)) (start (max 0 (- ivy--index half-height))) (end (min (+ start (1- ivy-height)) ivy--length)) @@ -861,16 +1132,84 @@ CANDS is a list of strings." (setq ivy--current (copy-sequence (nth index cands))) (setf (nth index cands) (ivy--add-face ivy--current 'ivy-current-match)) - (let* ((ww (window-width)) - (res (concat "\n" (mapconcat - (lambda (s) - (if (> (length s) ww) - (concat (substring s 0 (- ww 3)) "...") - s)) - cands "\n")))) + (let* ((ivy--index index) + (res (concat "\n" (funcall ivy-format-function cands)))) (put-text-property 0 (length res) 'read-only nil res) res)))) +(defvar ivy--virtual-buffers nil + "Store the virtual buffers alist.") + +(defvar recentf-list) +(defvar ido-use-faces) + +(defun ivy--virtual-buffers () + "Adapted from `ido-add-virtual-buffers-to-list'." + (unless recentf-mode + (recentf-mode 1)) + (let ((bookmarks (and (boundp 'bookmark-alist) + bookmark-alist)) + virtual-buffers name) + (dolist (head (append + recentf-list + (delete " - no file -" + (delq nil (mapcar (lambda (bookmark) + (cdr (assoc 'filename bookmark))) + bookmarks))))) + (setq name (file-name-nondirectory head)) + (when (equal name "") + (setq name (file-name-nondirectory (directory-file-name head)))) + (when (equal name "") + (setq name head)) + (and (not (equal name "")) + (null (get-file-buffer head)) + (not (assoc name virtual-buffers)) + (push (cons name head) virtual-buffers))) + (when virtual-buffers + (if ido-use-faces + (dolist (comp virtual-buffers) + (put-text-property 0 (length (car comp)) + 'face 'ido-virtual + (car comp)))) + (setq ivy--virtual-buffers (nreverse virtual-buffers)) + (mapcar #'car ivy--virtual-buffers)))) + +(defun ivy--buffer-list (str &optional virtual) + "Return the buffers that match STR. +When VIRTUAL is non-nil, add virtual buffers." + (delete-dups + (append + (mapcar + (lambda (x) + (if (with-current-buffer x + (file-remote-p + (abbreviate-file-name default-directory))) + (propertize x 'face 'ivy-remote) + x)) + (all-completions str 'internal-complete-buffer)) + (and virtual + (ivy--virtual-buffers))))) + +(defun ivy--switch-buffer-action () + "Finalizer for `ivy-switch-buffer'." + (if (zerop (length ivy--current)) + (switch-to-buffer + ivy-text nil 'force-same-window) + (let ((virtual (assoc ivy--current ivy--virtual-buffers))) + (if virtual + (find-file (cdr virtual)) + (switch-to-buffer + ivy--current nil 'force-same-window))))) + +(defun ivy-switch-buffer () + "Switch to another buffer." + (interactive) + (if (not ivy-mode) + (call-interactively 'switch-to-buffer) + (ivy-read "Switch to buffer: " 'internal-complete-buffer + :action #'ivy--switch-buffer-action + :preselect (buffer-name (other-buffer (current-buffer)))))) + (provide 'ivy) ;;; ivy.el ends here