X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/f00696aeeb23afc4f01b67f46ceb9da3f5f01314..59df2e994af2c0679e9a0fb0dcc22d5e35786adc:/ivy.el diff --git a/ivy.el b/ivy.el index e6a9ac9aa..f70c54f19 100644 --- a/ivy.el +++ b/ivy.el @@ -37,6 +37,7 @@ ;;; Code: (require 'cl-lib) +(require 'ffap) ;;* Customization (defgroup ivy nil @@ -62,6 +63,10 @@ '((t (:inherit 'dired-directory))) "Face used by Ivy for highlighting subdirs in the alternatives.") +(defface ivy-modified-buffer + '((t :inherit 'default)) + "Face used by Ivy for highlighting modified file visiting buffers.") + (defface ivy-remote '((t (:foreground "#110099"))) "Face used by Ivy for highlighting remotes in the alternatives.") @@ -165,6 +170,8 @@ Only \"./\" and \"../\" apply here. They appear in reverse order." (define-key map (kbd "S-SPC") 'ivy-restrict-to-matches) (define-key map (kbd "M-w") 'ivy-kill-ring-save) (define-key map (kbd "C-'") 'ivy-avy) + (define-key map (kbd "C-M-a") 'ivy-read-action) + (define-key map (kbd "C-c C-o") 'ivy-occur) map) "Keymap used in the minibuffer.") (autoload 'hydra-ivy/body "ivy-hydra" "" t) @@ -182,6 +189,10 @@ Only \"./\" and \"../\" apply here. They appear in reverse order." history preselect keymap update-fn sort ;; The window in which `ivy-read' was called window + ;; The buffer in which `ivy-read' was called + buffer + ;; The value of `ivy-text' to be used by `ivy-occur' + text action unwind re-builder @@ -270,11 +281,20 @@ When non-nil, it should contain one %d.") ,@body)) (minibuffer-keyboard-quit))) +(defun ivy-exit-with-action (action) + "Quit the minibuffer and call ACTION afterwards." + (ivy-set-action + `(lambda (x) + (funcall ',action x) + (ivy-set-action ',(ivy-state-action ivy-last)))) + (setq ivy-exit 'done) + (exit-minibuffer)) + (defmacro with-ivy-window (&rest body) "Execute BODY in the window from which `ivy-read' was called." (declare (indent 0) (debug t)) - `(with-selected-window (ivy-state-window ivy-last) + `(with-selected-window (ivy--get-window ivy-last) ,@body)) (defun ivy--done (text) @@ -349,9 +369,10 @@ When non-nil, it should contain one %d.") "Select one of the available actions and call `ivy-call'." (interactive) (let ((actions (copy-sequence (ivy-state-action ivy-last)))) - (ivy-read-action) - (ivy-call) - (ivy-set-action actions))) + (unwind-protect + (when (ivy-read-action) + (ivy-call)) + (ivy-set-action actions)))) (defun ivy-build-tramp-name (x) "Reconstruct X into a path. @@ -372,16 +393,69 @@ When ARG is t, exit with current text, ignoring the candidates." (let (dir) (cond (arg (ivy-immediate-done)) - ((and ivy--directory - (or - (and - (not (string= ivy--current "./")) - (cl-plusp ivy--length) + (ivy--directory + (cond + ((equal ivy-text "/sudo::") + (setq dir (concat ivy-text ivy--directory)) + (ivy--cd dir) + (ivy--exhibit)) + ((or + (and + (not (equal ivy-text "")) + (ignore-errors (file-directory-p - (setq dir (expand-file-name - ivy--current ivy--directory)))))) - (ivy--cd dir) - (ivy--exhibit)) + (setq dir + (file-name-as-directory + (expand-file-name + ivy-text ivy--directory)))))) + (and + (not (string= ivy--current "./")) + (cl-plusp ivy--length) + (ignore-errors + (file-directory-p + (setq dir (file-name-as-directory + (expand-file-name + ivy--current ivy--directory))))))) + (ivy--cd dir) + (ivy--exhibit)) + ((or (and (equal ivy--directory "/") + (string-match "\\`[^/]+:.*:.*\\'" ivy-text)) + (string-match "\\`/[^/]+:.*:.*\\'" ivy-text)) + (ivy-done)) + ((or (and (equal ivy--directory "/") + (cond ((string-match + "\\`\\([^/]+?\\):\\(?:\\(.*\\)@\\)?\\(.*\\)\\'" + ivy-text)) + ((string-match + "\\`\\([^/]+?\\):\\(?:\\(.*\\)@\\)?\\(.*\\)\\'" + ivy--current) + (setq ivy-text ivy--current)))) + (string-match + "\\`/\\([^/]+?\\):\\(?:\\(.*\\)@\\)?\\(.*\\)\\'" + ivy-text)) + (let ((method (match-string 1 ivy-text)) + (user (match-string 2 ivy-text)) + (rest (match-string 3 ivy-text)) + res) + (require 'tramp) + (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* ((old-ivy-last ivy-last) + (enable-recursive-minibuffers t) + (host (ivy-read "Find File: " + (mapcar #'ivy-build-tramp-name res) + :initial-input rest))) + (setq ivy-last old-ivy-last) + (when host + (setq ivy--directory "/") + (ivy--cd (concat "/" method ":" host ":")))))) + (t + (ivy-done)))) ((eq (ivy-state-collection ivy-last) 'Info-read-node-name-1) (if (or (equal ivy--current "(./)") (equal ivy--current "(../)")) @@ -392,34 +466,6 @@ When ARG is t, exit with current text, ignoring the candidates." (expand-file-name x ivy--directory) "Top")))) (ivy-done))) - ((and ivy--directory - (string-match "\\`/[^/]+:.*:.*\\'" ivy-text)) - (ivy-done)) - ((and ivy--directory - (string-match - "\\`/\\([^/]+?\\):\\(?:\\(.*\\)@\\)?\\(.*\\)\\'" - ivy-text)) - (let ((method (match-string 1 ivy-text)) - (user (match-string 2 ivy-text)) - (rest (match-string 3 ivy-text)) - res) - (require 'tramp) - (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* ((old-ivy-last ivy-last) - (enable-recursive-minibuffers t) - (host (ivy-read "Find File: " - (mapcar #'ivy-build-tramp-name res) - :initial-input rest))) - (setq ivy-last old-ivy-last) - (when host - (setq ivy--directory "/") - (ivy--cd (concat "/" method ":" host ":")))))) (t (ivy-done))))) @@ -432,13 +478,16 @@ When ARG is t, exit with current text, ignoring the candidates." If the text hasn't changed as a result, forward to `ivy-alt-done'." (interactive) (if (and (eq (ivy-state-collection ivy-last) #'read-file-name-internal) - (string-match "\\`/" ivy-text)) + (or (and (equal ivy--directory "/") + (string-match "\\`[^/]+:.*\\'" ivy-text)) + (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)))) + (when (file-directory-p + (expand-file-name ivy-text ivy--directory)) + (ivy--cd (file-name-as-directory + (expand-file-name ivy-text ivy--directory))))) (or (ivy-partial) (when (or (eq this-command last-command) (eq ivy--length 1)) @@ -454,7 +503,10 @@ If the text hasn't changed as a result, forward to `ivy-alt-done'." (new (try-completion (if startp (substring postfix 1) postfix) - (mapcar (lambda (str) (substring str (string-match postfix str))) + (mapcar (lambda (str) + (let ((i (string-match postfix str))) + (when i + (substring str i)))) ivy--old-cands)))) (cond ((eq new t) nil) ((string= new ivy-text) nil) @@ -472,7 +524,10 @@ If the text hasn't changed as a result, forward to `ivy-alt-done'." "Exit the minibuffer with the current input." (interactive) (delete-minibuffer-contents) - (insert (setq ivy--current ivy-text)) + (insert (setq ivy--current + (if ivy--directory + (expand-file-name ivy-text ivy--directory) + ivy-text))) (setq ivy-exit 'done) (exit-minibuffer)) @@ -480,24 +535,26 @@ If the text hasn't changed as a result, forward to `ivy-alt-done'." (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 (unless (eq (ivy-state-collection ivy-last) - 'read-file-name-internal) - 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))) + (with-current-buffer (ivy-state-buffer ivy-last) + (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 (unless (eq (ivy-state-collection ivy-last) + 'read-file-name-internal) + 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) + :caller (ivy-state-caller ivy-last)))) (defvar ivy-calling nil "When non-nil, call the current action when `ivy--index' changes.") @@ -522,13 +579,13 @@ If the text hasn't changed as a result, forward to `ivy-alt-done'." (defun ivy-scroll-up-command () "Scroll the candidates upward by the minibuffer height." (interactive) - (ivy-set-index (min (+ ivy--index ivy-height) + (ivy-set-index (min (1- (+ ivy--index ivy-height)) (1- ivy--length)))) (defun ivy-scroll-down-command () "Scroll the candidates downward by the minibuffer height." (interactive) - (ivy-set-index (max (- ivy--index ivy-height) + (ivy-set-index (max (1+ (- ivy--index ivy-height)) 0))) (defun ivy-minibuffer-grow () @@ -597,6 +654,15 @@ If the input is empty, select the previous history element instead." action (cadr (nth (car action) action)))))) +(defun ivy--get-window (state) + "Get the window from STATE." + (let ((window (ivy-state-window state))) + (if (window-live-p window) + window + (if (= (length (window-list)) 1) + (selected-window) + (next-window))))) + (defun ivy--actionp (x) "Return non-nil when X is a list of actions." (and x (listp x) (not (eq (car x) 'closure)))) @@ -682,15 +748,21 @@ Call the permanent action if possible." "Check if the current input points to a different directory. If so, move to that directory, while keeping only the file name." (when ivy--directory - (let* ((input (expand-file-name (ivy--input))) - (file (file-name-nondirectory input)) - (dir (expand-file-name (file-name-directory input)))) - (if (string= dir ivy--directory) - (progn - (delete-minibuffer-contents) - (insert file)) - (ivy--cd dir) - (insert file))))) + (let ((input (ivy--input)) + url) + (if (setq url (ffap-url-p input)) + (ivy-exit-with-action + (lambda (_) + (funcall ffap-url-fetcher url))) + (setq input (expand-file-name input)) + (let ((file (file-name-nondirectory input)) + (dir (expand-file-name (file-name-directory input)))) + (if (string= dir ivy--directory) + (progn + (delete-minibuffer-contents) + (insert file)) + (ivy--cd dir) + (insert file))))))) (defun ivy--maybe-scroll-history () "If the selected history element has an index, scroll there." @@ -837,7 +909,7 @@ Prioritize directories." nil (string< x y)))) -(defvar ivy-sort-functions-alist +(defcustom ivy-sort-functions-alist '((read-file-name-internal . ivy-sort-file-function-default) (internal-complete-buffer . nil) (counsel-git-grep-function . nil) @@ -851,7 +923,19 @@ For each entry, nil means no sorting. It's very useful to turn off the sorting for functions that have candidates in the natural buffer order, like `org-refile' or `Man-goto-section'. -The entry associated to t is used for all fall-through cases.") +The entry associated to t is used for all fall-through cases. + +See also `ivy-sort-max-size'." + :type + '(alist + :key-type (choice + (const :tag "All other functions" t) + (symbol :tag "Function")) + :value-type (choice + (const :tag "plain sort" string-lessp) + (const :tag "file sort" ivy-sort-file-function-default) + (const :tag "no sort" nil))) + :group 'ivy) (defvar ivy-index-functions-alist '((swiper . ivy-recompute-index-swiper) @@ -951,8 +1035,9 @@ 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. +DYNAMIC-COLLECTION is a boolean that determines whether to update +the list of candidates with each input by calling COLLECTION for +the current input. CALLER is a symbol to uniquely identify the caller to `ivy-read'. It's used in conjunction with COLLECTION to indentify which @@ -965,56 +1050,60 @@ customizations should apply to the current completion session." ("o" ,action "default") ,@extra-actions) (delete-dups (append action extra-actions)))))) - (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 - :caller caller)) - (ivy--reset-state ivy-last) - (prog1 - (unwind-protect - (minibuffer-with-setup-hook - #'ivy--minibuffer-setup - (let* ((hist (or history 'ivy-history)) - (minibuffer-completion-table collection) - (minibuffer-completion-predicate predicate) - (resize-mini-windows (cond - ((display-graphic-p) nil) - ((null resize-mini-windows) 'grow-only) - (t resize-mini-windows))) - (res (read-from-minibuffer - prompt - (ivy-state-initial-input ivy-last) - (make-composed-keymap keymap ivy-minibuffer-map) - nil - hist))) - (when (eq ivy-exit 'done) - (let ((item (if ivy--directory - ivy--current - ivy-text))) - (unless (equal item "") - (set hist (cons (propertize item 'ivy-index ivy--index) - (delete item - (cdr (symbol-value hist))))))) - res))) - (remove-hook 'post-command-hook #'ivy--exhibit) - (when (setq unwind (ivy-state-unwind ivy-last)) - (funcall unwind))) - (ivy-call))) + (let ((recursive-ivy-last (and (window-minibuffer-p) ivy-last))) + (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) + :buffer (current-buffer) + :unwind unwind + :re-builder re-builder + :matcher matcher + :dynamic-collection dynamic-collection + :caller caller)) + (ivy--reset-state ivy-last) + (prog1 + (unwind-protect + (minibuffer-with-setup-hook + #'ivy--minibuffer-setup + (let* ((hist (or history 'ivy-history)) + (minibuffer-completion-table collection) + (minibuffer-completion-predicate predicate) + (resize-mini-windows (cond + ((display-graphic-p) nil) + ((null resize-mini-windows) 'grow-only) + (t resize-mini-windows))) + (res (read-from-minibuffer + prompt + (ivy-state-initial-input ivy-last) + (make-composed-keymap keymap ivy-minibuffer-map) + nil + hist))) + (when (eq ivy-exit 'done) + (let ((item (if ivy--directory + ivy--current + ivy-text))) + (unless (equal item "") + (set hist (cons (propertize item 'ivy-index ivy--index) + (delete item + (cdr (symbol-value hist))))))) + res))) + (remove-hook 'post-command-hook #'ivy--exhibit) + (when (setq unwind (ivy-state-unwind ivy-last)) + (funcall unwind))) + (ivy-call) + (when recursive-ivy-last + (ivy--reset-state (setq ivy-last recursive-ivy-last)))))) (defun ivy--reset-state (state) "Reset the ivy to STATE. @@ -1113,13 +1202,16 @@ This is useful for recursive `ivy-read'." (setq ivy--old-cands nil) (setq ivy--all-candidates coll)) (setq ivy-exit nil) - (setq ivy--default (or (thing-at-point 'symbol) "")) + (setq ivy--default (or + (thing-at-point 'url) + (thing-at-point 'symbol) + "")) (setq ivy--prompt (cond ((string-match "%.*d" prompt) prompt) ((null ivy-count-format) (error - "`ivy-count-format' can't be nil. Set it to an empty string instead.")) + "`ivy-count-format' can't be nil. Set it to an empty string instead")) ((string-match "%d.*%d" ivy-count-format) (let ((w (length (number-to-string (length ivy--all-candidates)))) @@ -1151,13 +1243,19 @@ 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 considered boolean. 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. _INHERIT-INPUT-METHOD is ignored for now. The history, defaults and input-method arguments are ignored for now." + ;; See the doc of `completing-read'. + (when (consp history) + (when (numberp (cdr history)) + (setq initial-input (nth (1- (cdr history)) + (symbol-value (car history))))) + (setq history (car history))) (ivy-read (replace-regexp-in-string "%" "%%" prompt) collection :predicate predicate @@ -1213,13 +1311,15 @@ When MATCHER is non-nil it's used instead of `cl-remove-if-not'." candidates)))) (when matcher (setq candidates (funcall matcher "" candidates)))) - (or (cl-position preselect candidates :test #'equal) - (and (stringp preselect) - (let ((re (regexp-quote preselect))) - (cl-position-if - (lambda (x) - (string-match re x)) - candidates))))) + (cond ((integerp preselect) + preselect) + ((cl-position preselect candidates :test #'equal)) + ((stringp preselect) + (let ((re (regexp-quote preselect))) + (cl-position-if + (lambda (x) + (string-match re x)) + candidates))))) ;;* Implementation ;;** Regex @@ -1482,7 +1582,7 @@ Should be run via minibuffer `post-command-hook'." (string-match "\\`[[:alpha:]]:/" default-directory)) (ivy--cd (match-string 0 default-directory)) (ivy--cd "/"))) - (when (string-match "[[:alpha:]]:/" ivy-text) + (when (string-match "[[:alpha:]]:/$" ivy-text) (let ((drive-root (match-string 0 ivy-text))) (when (file-exists-p drive-root) (ivy--cd drive-root))))) @@ -1630,24 +1730,88 @@ CANDIDATES are assumed to be static." res))))) (ivy--recompute-index name re-str cands) (setq ivy--old-re (if cands re-str "")) - (when (and (require 'flx nil 'noerror) - (eq ivy--regex-function 'ivy--regex-fuzzy)) - (setq cands (ivy--flx-sort name cands))) - (setq ivy--old-cands cands))))) + (setq ivy--old-cands (ivy--sort name cands)))))) + +(defcustom ivy-sort-matches-functions-alist '((t . nil)) + "An alist of functions used to sort the matching candidates. + +This is different from `ivy-sort-functions-alist', which is used +to sort the whole collection only once. The functions taken from +here are instead used on each input change, but they are used +only on already matching candidates, not on all of them. + +The alist KEY is a collection function or t to match previously +not matched collection functions. + +The alist VAL is a sorting function with the signature of +`ivy--prefix-sort'.") + +(defun ivy--sort-files-by-date (_name candidates) + "Re-soft CANDIDATES according to file modification date." + (let ((default-directory ivy--directory)) + (cl-sort (copy-sequence candidates) + (lambda (f1 f2) + (time-less-p + (nth 5 (file-attributes f2)) + (nth 5 (file-attributes f1))))))) + +(defun ivy--sort (name candidates) + "Re-sort CANDIDATES according to NAME. +All CANDIDATES are assumed to match NAME." + (let ((key (or (ivy-state-caller ivy-last) + (when (functionp (ivy-state-collection ivy-last)) + (ivy-state-collection ivy-last)))) + fun) + (cond ((and (require 'flx nil 'noerror) + (eq ivy--regex-function 'ivy--regex-fuzzy)) + (ivy--flx-sort name candidates)) + ((setq fun (cdr (or (assoc key ivy-sort-matches-functions-alist) + (assoc t ivy-sort-matches-functions-alist)))) + (funcall fun name candidates)) + (t + candidates)))) + +(defun ivy--prefix-sort (name candidates) + "Re-sort CANDIDATES. +Prefix matches to NAME are put ahead of the list." + (if (or (string-match "^\\^" name) (string= name "")) + candidates + (let ((re-prefix (concat "^" (funcall ivy--regex-function name))) + res-prefix + res-noprefix) + (dolist (s candidates) + (if (string-match re-prefix s) + (push s res-prefix) + (push s res-noprefix))) + (nconc + (nreverse res-prefix) + (nreverse res-noprefix))))) (defun ivy--recompute-index (name re-str cands) (let* ((caller (ivy-state-caller ivy-last)) (func (or (and caller (cdr (assoc caller ivy-index-functions-alist))) (cdr (assoc t ivy-index-functions-alist)) #'ivy-recompute-index-zero))) - (setq ivy--index - (or (and (not (string= name "")) - (not (and (require 'flx nil 'noerror) - (eq ivy--regex-function 'ivy--regex-fuzzy) - (< (length cands) 200))) - (cl-position (nth ivy--index ivy--old-cands) - cands)) - (funcall func re-str cands))) + (unless (eq this-command 'ivy-resume) + (setq ivy--index + (or + (cl-position (if (and (> (length re-str) 0) + (eq ?^ (aref re-str 0))) + (substring re-str 1) + re-str) cands + :test #'equal) + (and ivy--directory + (cl-position + (concat re-str "/") cands + :test #'equal)) + (and (not (string= name "")) + (not (and (require 'flx nil 'noerror) + (eq ivy--regex-function 'ivy--regex-fuzzy) + (< (length cands) 200))) + + (cl-position (nth ivy--index ivy--old-cands) + cands)) + (funcall func re-str cands)))) (when (and (or (string= name "") (string= name "^")) (not (equal ivy--old-re ""))) @@ -1659,26 +1823,15 @@ CANDIDATES are assumed to be static." nil) ivy--index))))) -(defun ivy-recompute-index-swiper (re-str cands) +(defun ivy-recompute-index-swiper (_re-str cands) (let ((tail (nthcdr ivy--index ivy--old-cands)) idx) (if (and tail ivy--old-cands (not (equal "^" ivy--old-re))) - (or (and (not (equal re-str ivy--old-re)) - (or - (cl-position (if (and (> (length re-str) 0) - (eq ?^ (aref re-str 0))) - (substring re-str 1) - re-str) cands - :test #'equal) - (and ivy--directory - (cl-position - (concat re-str "/") cands - :test #'equal)))) - (progn - (while (and tail (null idx)) - ;; Compare with eq to handle equal duplicates in cands - (setq idx (cl-position (pop tail) cands))) - (or idx 0))) + (progn + (while (and tail (null idx)) + ;; Compare with eq to handle equal duplicates in cands + (setq idx (cl-position (pop tail) cands))) + (or idx 0)) ivy--index))) (defun ivy-recompute-index-zero (_re-str _cands) @@ -1714,6 +1867,13 @@ CANDIDATES are assumed to be static." "Function to transform the list of candidates into a string. This string will be inserted into the minibuffer.") +(defun ivy--truncate-string (str width) + "Truncate STR to WIDTH." + (if (> (string-width str) width) + (concat (substring str 0 (min (- width 3) + (- (length str) 3))) "...") + str)) + (defun ivy-format-function-default (cands) "Transform CANDS into a string for minibuffer." (if (bound-and-true-p truncate-lines) @@ -1723,9 +1883,7 @@ This string will be inserted into the minibuffer.") (mapconcat (if truncate-lines (lambda (s) - (if (> (length s) ww) - (concat (substring s 0 (- ww 3)) "...") - s)) + (ivy--truncate-string s ww)) #'identity) cands "\n")))) @@ -1740,12 +1898,40 @@ This string will be inserted into the minibuffer.") s)) cands "\n"))) -(defcustom swiper-minibuffer-faces - '(swiper-minibuffer-match-face-1 - swiper-minibuffer-match-face-2 - swiper-minibuffer-match-face-3 - swiper-minibuffer-match-face-4) - "List of `swiper' faces for minibuffer group matches.") +(defface ivy-minibuffer-match-face-1 + '((((class color) (background light)) + :background "#d3d3d3") + (((class color) (background dark)) + :background "#555555")) + "The background face for `ivy' minibuffer matches.") + +(defface ivy-minibuffer-match-face-2 + '((((class color) (background light)) + :background "#e99ce8" :weight bold) + (((class color) (background dark)) + :background "#777777" :weight bold)) + "Face for `ivy' minibuffer matches modulo 1.") + +(defface ivy-minibuffer-match-face-3 + '((((class color) (background light)) + :background "#bbbbff" :weight bold) + (((class color) (background dark)) + :background "#7777ff" :weight bold)) + "Face for `ivy' minibuffer matches modulo 2.") + +(defface ivy-minibuffer-match-face-4 + '((((class color) (background light)) + :background "#ffbbff" :weight bold) + (((class color) (background dark)) + :background "#8a498a" :weight bold)) + "Face for `ivy' minibuffer matches modulo 3.") + +(defcustom ivy-minibuffer-faces + '(ivy-minibuffer-match-face-1 + ivy-minibuffer-match-face-2 + ivy-minibuffer-match-face-3 + ivy-minibuffer-match-face-4) + "List of `ivy' faces for minibuffer group matches.") (defun ivy--format-minibuffer-line (str) (let ((start 0) @@ -1760,12 +1946,12 @@ This string will be inserted into the minibuffer.") (while (<= i ivy--subexps) (let ((face (cond ((zerop ivy--subexps) - (cadr swiper-minibuffer-faces)) + (cadr ivy-minibuffer-faces)) ((zerop i) - (car swiper-minibuffer-faces)) + (car ivy-minibuffer-faces)) (t - (nth (1+ (mod (+ i 2) (1- (length swiper-minibuffer-faces)))) - swiper-minibuffer-faces))))) + (nth (1+ (mod (+ i 2) (1- (length ivy-minibuffer-faces)))) + ivy-minibuffer-faces))))) (if (fboundp 'add-face-text-property) (add-face-text-property (match-beginning i) @@ -1796,12 +1982,21 @@ CANDS is a list of strings." (start (max 0 (min start (- end (1- ivy-height))))) (cands (cl-subseq cands start end)) (index (- ivy--index start))) - (when ivy--directory - (setq cands (mapcar (lambda (x) - (if (string-match-p "/\\'" x) - (propertize x 'face 'ivy-subdir) - x)) - cands))) + (cond (ivy--directory + (setq cands (mapcar (lambda (x) + (if (string-match-p "/\\'" x) + (propertize x 'face 'ivy-subdir) + x)) + cands))) + ((eq (ivy-state-collection ivy-last) 'internal-complete-buffer) + (setq cands (mapcar (lambda (x) + (let ((b (get-buffer x))) + (if (and b + (buffer-file-name b) + (buffer-modified-p b)) + (propertize x 'face 'ivy-modified-buffer) + x))) + cands)))) (setq ivy--current (copy-sequence (nth index cands))) (setq cands (mapcar #'ivy--format-minibuffer-line @@ -1824,8 +2019,8 @@ CANDS is a list of strings." (defcustom ivy-virtual-abbreviate 'name "The mode of abbreviation for virtual buffer names." :type '(choice - (const :tag "Only name" 'name) - (const :tag "Full path" 'full) + (const :tag "Only name" name) + (const :tag "Full path" full) ;; eventually, uniquify )) @@ -2014,6 +2209,170 @@ The selected history element will be inserted into the minibufer." (setq ivy--all-candidates (ivy--filter ivy-text ivy--all-candidates))) +;;* Occur +(defvar-local ivy-occur-last nil + "Buffer-local value of `ivy-last'. +Can't re-use `ivy-last' because using e.g. `swiper' in the same +buffer would modify `ivy-last'.") + +(defvar ivy-occur-mode-map + (let ((map (make-sparse-keymap))) + (define-key map [mouse-1] 'ivy-occur-click) + (define-key map (kbd "RET") 'ivy-occur-press) + (define-key map (kbd "j") 'next-line) + (define-key map (kbd "k") 'previous-line) + (define-key map (kbd "h") 'backward-char) + (define-key map (kbd "l") 'forward-char) + (define-key map (kbd "g") 'ivy-occur-press) + (define-key map (kbd "a") 'ivy-occur-read-action) + (define-key map (kbd "o") 'ivy-occur-dispatch) + (define-key map (kbd "q") 'quit-window) + map) + "Keymap for Ivy Occur mode.") + +(define-derived-mode ivy-occur-mode fundamental-mode "Ivy-Occur" + "Major mode for output from \\[ivy-occur]. + +\\{ivy-occur-mode-map}") + +(defvar ivy-occur-grep-mode-map + (let ((map (copy-keymap ivy-occur-mode-map))) + (define-key map (kbd "C-x C-q") 'ivy-wgrep-change-to-wgrep-mode) + map) + "Keymap for Ivy Occur Grep mode.") + +(define-derived-mode ivy-occur-grep-mode grep-mode "Ivy-Occur" + "Major mode for output from \\[ivy-occur]. + +\\{ivy-occur-grep-mode-map}") + +(defvar counsel-git-grep-cmd) + +(defun ivy-occur () + "Stop completion and put the current matches into a new buffer. + +The new buffer will also remember the current action(s). + +While in the *ivy-occur* buffer, selecting a cadidate with RET or +a mouse click will call the appropriate action for that candidate. + +It's possible to have an unlimited amount of *ivy-occur* buffers." + (interactive) + (let ((buffer + (generate-new-buffer + (format "*ivy-occur%s \"%s\"*" + (let (caller) + (if (setq caller (ivy-state-caller ivy-last)) + (concat " " (prin1-to-string caller)) + "")) + ivy-text))) + (do-grep (eq (ivy-state-caller ivy-last) 'counsel-git-grep))) + (with-current-buffer buffer + (if do-grep + (progn + (setq ivy--old-cands + (split-string + (shell-command-to-string + (format counsel-git-grep-cmd ivy--old-re)) + "\n" + t)) + (ivy-occur-grep-mode)) + (ivy-occur-mode)) + (setf (ivy-state-text ivy-last) ivy-text) + (setq ivy-occur-last ivy-last) + (setq-local ivy--directory ivy--directory) + (let ((inhibit-read-only t)) + (erase-buffer) + (when do-grep + ;; Need precise number of header lines for `wgrep' to work. + (insert (format "-*- mode:grep; default-directory: %S -*-\n\n\n" + default-directory))) + (insert (format "%d candidates:\n" (length ivy--old-cands))) + (dolist (cand ivy--old-cands) + (let ((str (if do-grep + (concat "./" cand) + (concat " " cand)))) + (add-text-properties + 0 (length str) + `(mouse-face + highlight + help-echo "mouse-1: call ivy-action") + str) + (insert str "\n"))))) + (ivy-exit-with-action + `(lambda (_) (pop-to-buffer ,buffer))))) + +(declare-function wgrep-change-to-wgrep-mode "ext:wgrep") + +(defun ivy-wgrep-change-to-wgrep-mode () + "Forward to `wgrep-change-to-wgrep-mode'." + (interactive) + (if (require 'wgrep nil 'noerror) + (wgrep-change-to-wgrep-mode) + (error "Package wgrep isn't installed"))) + +(defun ivy-occur-read-action () + "Select one of the available actions as the current one." + (interactive) + (let ((ivy-last ivy-occur-last)) + (ivy-read-action))) + +(defun ivy-occur-dispatch () + "Call one of the available actions on the current item." + (interactive) + (let* ((state-action (ivy-state-action ivy-occur-last)) + (actions (if (symbolp state-action) + state-action + (copy-sequence state-action)))) + (unwind-protect + (progn + (ivy-occur-read-action) + (ivy-occur-press)) + (setf (ivy-state-action ivy-occur-last) actions)))) + +(defun ivy-occur-click (event) + "Execute action for the current candidate. +EVENT gives the mouse position." + (interactive "e") + (let ((window (posn-window (event-end event))) + (pos (posn-point (event-end event)))) + (with-current-buffer (window-buffer window) + (goto-char pos) + (ivy-occur-press)))) + +(defun ivy-occur-press () + "Execute action for the current candidate." + (interactive) + (require 'pulse) + (when (save-excursion + (beginning-of-line) + (looking-at "\\(?:./\\| \\)\\(.*\\)$")) + (let* ((ivy-last ivy-occur-last) + (ivy-text (ivy-state-text ivy-last)) + (str (buffer-substring + (match-beginning 1) + (match-end 1))) + (coll (ivy-state-collection ivy-last)) + (action (ivy--get-action ivy-last)) + (ivy-exit 'done)) + (with-ivy-window + (funcall action + (if (and (consp coll) + (consp (car coll))) + (cdr (assoc str coll)) + str)) + (if (memq (ivy-state-caller ivy-last) + '(swiper counsel-git-grep)) + (with-current-buffer (window-buffer (selected-window)) + (swiper--cleanup) + (swiper--add-overlays + (ivy--regex ivy-text) + (line-beginning-position) + (line-end-position) + (selected-window)) + (run-at-time 0.5 nil 'swiper--cleanup)) + (pulse-momentary-highlight-one-line (point))))))) + (provide 'ivy) ;;; ivy.el ends here