]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/swiper/ivy.el
Merge commit 'efa18eca10e5a0e05043f872cf9945842bb3a034' from swiper
[gnu-emacs-elpa] / packages / swiper / ivy.el
index 8c676e5cb8d74cada70dfdc9323c67d31cebecfd..2802f078fd92e798ec38f23bbc9532c6cdb9792d 100644 (file)
   '((t (:inherit highlight)))
   "Face used by Ivy for highlighting first match.")
 
+(defface ivy-subdir
+  '((t (:weight bold)))
+  "Face used by Ivy for highlighting subdirs in the alternatives.")
+
 (defcustom ivy-height 10
   "Number of lines for the minibuffer window."
   :type 'integer)
@@ -66,12 +70,18 @@ Set this to nil if you don't want the count."
 This is usually meant as a quick exit out of the minibuffer."
   :type 'function)
 
+(defcustom ivy-extra-directories '("../" "./")
+  "Add this to the front of the list when completing file names.
+Only \"./\" and \"../\" apply here. They appear in reverse order."
+  :type 'list)
+
 ;;* User Visible
 ;;** 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 "C-n") 'ivy-next-line)
     (define-key map (kbd "C-p") 'ivy-previous-line)
     (define-key map (kbd "C-s") 'ivy-next-line-or-history)
@@ -83,6 +93,8 @@ This is usually meant as a quick exit out of the minibuffer."
     (define-key map (kbd "M-n") 'ivy-next-history-element)
     (define-key map (kbd "M-p") 'ivy-previous-history-element)
     (define-key map (kbd "C-g") 'minibuffer-keyboard-quit)
+    (define-key map (kbd "C-v") 'ivy-scroll-up-command)
+    (define-key map (kbd "M-v") 'ivy-scroll-down-command)
     map)
   "Keymap used in the minibuffer.")
 
@@ -95,19 +107,93 @@ of `history-length', which see.")
 (defvar ivy-require-match t
   "Store require-match. See `completing-read'.")
 
+(defvar ivy--directory nil
+  "Current directory when completing file names.")
+
+(defvar ivy--length 0
+  "Store the amount of viable candidates.")
+
+(defvar ivy-text ""
+  "Store the user's string as it is typed in.")
+
+(defvar ivy--current ""
+  "Current candidate.")
+
+(defvar ivy--index 0
+  "Store the index of the current candidate.")
+
+(defvar ivy-exit nil
+  "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--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--old-re nil
+  "Store the old regexp.")
+
+(defvar ivy--old-cands nil
+  "Store the candidates matched by `ivy--old-re'.")
+
 ;;** Commands
 (defun ivy-done ()
   "Exit the minibuffer with the selected candidate."
   (interactive)
   (delete-minibuffer-contents)
-  (if (zerop ivy--length)
-      (when (memq ivy-require-match '(nil confirm confirm-after-completion))
-        (insert ivy-text)
-        (setq ivy-exit 'done))
-    (insert ivy--current)
-    (setq ivy-exit 'done))
+  (cond (ivy--directory
+         (insert
+          (cond ((string= ivy-text "")
+                 (if (equal ivy--current "./")
+                     ivy--directory
+                   ivy--current))
+                ((zerop ivy--length)
+                 (expand-file-name ivy-text ivy--directory))
+                (t
+                 (expand-file-name ivy--current ivy--directory))))
+         (setq ivy-exit 'done))
+        ((zerop ivy--length)
+         (when (memq ivy-require-match
+                     '(nil confirm confirm-after-completion))
+           (insert ivy-text)
+           (setq ivy-exit 'done)))
+        (t
+         (insert ivy--current)
+         (setq ivy-exit 'done)))
   (exit-minibuffer))
 
+(defun ivy-alt-done ()
+  "Exit the minibuffer with the selected candidate."
+  (interactive)
+  (let (dir)
+    (cond ((and ivy--directory
+                (= 0 ivy--index)
+                (= 0 (length ivy-text)))
+           (ivy-done))
+
+          ((and ivy--directory
+                (plusp ivy--length)
+                (file-directory-p
+                 (setq dir (expand-file-name
+                            ivy--current ivy--directory))))
+           (ivy--cd dir)
+           (ivy--exhibit))
+
+          (t
+           (ivy-done)))))
+
 (defun ivy-beginning-of-buffer ()
   "Select the first completion candidate."
   (interactive)
@@ -118,6 +204,18 @@ of `history-length', which see.")
   (interactive)
   (setq ivy--index (1- ivy--length)))
 
+(defun ivy-scroll-up-command ()
+  "Scroll the candidates upward by the minibuffer height."
+  (interactive)
+  (setq ivy--index (min (+ ivy--index ivy-height)
+                        (1- ivy--length))))
+
+(defun ivy-scroll-down-command ()
+  "Scroll the candidates downward by the minibuffer height."
+  (interactive)
+  (setq ivy--index (max (- ivy--index ivy-height)
+                        0)))
+
 (defun ivy-next-line (&optional arg)
   "Move cursor vertically down ARG candidates."
   (interactive "p")
@@ -166,19 +264,66 @@ If the input is empty, select the previous history element instead."
   (next-history-element arg)
   (move-end-of-line 1))
 
+(defun ivy--cd (dir)
+  "When completing file names, move to directory DIR."
+  (if (null ivy--directory)
+      (error "Unexpected")
+    (setq ivy--old-cands nil)
+    (setq ivy--all-candidates
+          (ivy--sorted-files (setq ivy--directory dir)))
+    (setq ivy-text "")
+    (delete-minibuffer-contents)))
+
 (defun ivy-backward-delete-char ()
   "Forward to `backward-delete-char'.
 On error (read-only), call `ivy-on-del-error-function'."
   (interactive)
-  (condition-case nil
-      (backward-delete-char 1)
-    (error
-     (when ivy-on-del-error-function
-       (funcall ivy-on-del-error-function)))))
+  (if (and ivy--directory (= (minibuffer-prompt-end) (point)))
+      (progn
+        (ivy--cd (file-name-directory
+                  (directory-file-name ivy--directory)))
+        (ivy--exhibit))
+    (condition-case nil
+        (backward-delete-char 1)
+      (error
+       (when ivy-on-del-error-function
+         (funcall ivy-on-del-error-function))))))
+
+(defun ivy-sort-file-function-default (x y)
+  "Compare two files X and Y.
+Prioritize directories."
+  (if (get-text-property 0 'dirp x)
+      (if (get-text-property 0 'dirp y)
+          (string< x y)
+        t)
+    (if (get-text-property 0 'dirp y)
+        nil
+      (string< x y))))
+
+(defvar ivy-sort-file-function 'ivy-sort-file-function-default
+  "The function that compares file names.
+It should take two string arguments and return nil and non-nil.")
+
+(defun ivy--sorted-files (dir)
+  "Return the list of files in DIR.
+Directories come first."
+  (let* ((default-directory dir)
+         (seq (all-completions "" 'read-file-name-internal)))
+    (if (equal dir "/")
+        seq
+      (setq seq (delete "./" (delete "../" seq)))
+      (when (eq ivy-sort-file-function 'ivy-sort-file-function-default)
+        (setq seq (mapcar (lambda (x)
+                            (propertize x 'dirp (string-match-p "/$" x)))
+                          (delete "./" (delete "../" seq)))))
+      (setq seq (cl-sort seq ivy-sort-file-function))
+      (dolist (dir ivy-extra-directories)
+        (push dir seq))
+      seq)))
 
 ;;** Entry Point
 (defun ivy-read (prompt collection
-                 &optional initial-input keymap preselect update-fn)
+                 &optional predicate initial-input keymap preselect update-fn)
   "Read a string in the minibuffer, with completion.
 
 PROMPT is a string to prompt with; normally it ends in a colon
@@ -196,6 +341,31 @@ If PRESELECT is non-nil select the corresponding candidate out of
 the ones that match INITIAL-INPUT.
 
 UPDATE-FN is called each time the current candidate(s) is changed."
+  (setq ivy--directory nil)
+  (cond ((eq collection 'Info-read-node-name-1)
+         (if (equal Info-current-file "dir")
+             (setq collection
+                   (mapcar (lambda (x) (format "(%s)" x))
+                           (cl-delete-duplicates
+                            (all-completions "(" collection predicate)
+                            :test 'equal)))
+           (setq collection (all-completions "" collection predicate))))
+        ((eq collection 'read-file-name-internal)
+         (setq ivy--directory default-directory)
+         (setq initial-input nil)
+         (setq collection
+               (ivy--sorted-files default-directory)))
+        ((or (functionp collection)
+             (vectorp collection))
+         (setq collection (all-completions "" collection predicate)))
+        ((hash-table-p collection)
+         (error "Hash table as a collection unsupported"))
+        ((listp (car collection))
+         (setq collection (all-completions "" collection predicate))))
+  (when preselect
+    (unless (or ivy-require-match
+                (all-completions preselect collection))
+      (setq collection (cons preselect collection))))
   (cl-case (length collection)
     (0 nil)
     (1 (car collection))
@@ -217,6 +387,8 @@ UPDATE-FN is called each time the current candidate(s) is changed."
                   prompt)
                  ((string-match "%.*d" ivy-count-format)
                   (concat ivy-count-format prompt))
+                 (ivy--directory
+                  prompt)
                  (t
                   nil)))
      (setq ivy--action nil)
@@ -258,26 +430,19 @@ DEF is the default value.
 _INHERIT-INPUT-METHOD is ignored for now.
 
 The history, defaults and input-method arguments are ignored for now."
-  (cond ((or (functionp collection)
-             (vectorp collection))
-         (setq collection (all-completions "" collection predicate))
-         ;; find-file is problematic
-         (setq initial-input nil))
-        ((hash-table-p collection)
-         (error "Hash table as a collection unsupported"))
-        ((listp (car collection))
-         (setq collection (all-completions "" collection predicate))))
   (when (listp def)
     (setq def (car def)))
   (setq ivy-require-match require-match)
-  (ivy-read prompt collection initial-input nil def))
+  (ivy-read prompt collection predicate initial-input nil def))
 
 ;;;###autoload
 (define-minor-mode ivy-mode
     "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'."
+`ivy-completing-read'.
+
+\\{ivy-minibuffer-map}"
   :group 'ivy
   :global t
   :lighter " ivy"
@@ -285,9 +450,6 @@ Turning on Ivy mode will set `completing-read-function' to
       (setq completing-read-function 'ivy-completing-read)
     (setq completing-read-function 'completing-read-default)))
 
-(defvar ivy--action nil
-  "Store a function to call at the end of `ivy--read'.")
-
 (defun ivy--preselect-index (candidates initial-input preselect)
   "Return the index in CANDIDATES filtered by INITIAL-INPUT for PRESELECT."
   (when initial-input
@@ -296,17 +458,11 @@ Turning on Ivy mode will set `completing-read-function' to
            (lambda (x)
              (string-match initial-input x))
            candidates)))
-  (cl-position-if
-   (lambda (x)
-     (string-match preselect x))
-   candidates))
-
-(defvar ivy-text ""
-  "Stores the user's string as it is typed in.")
-
-(defvar ivy-exit nil
-  "Store 'done if the completion was successfully selected.
-Otherwise, store nil.")
+  (or (cl-position preselect candidates :test 'equal)
+      (cl-position-if
+       (lambda (x)
+         (string-match preselect x))
+       candidates)))
 
 ;;* Implementation
 ;;** Regex
@@ -351,24 +507,6 @@ Otherwise, store nil.")
   ;; 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--default nil
-  "Default initial input.")
-
-(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
@@ -382,15 +520,15 @@ Otherwise, store nil.")
     (goto-char (minibuffer-prompt-end))
     (delete-region (line-end-position) (point-max))))
 
-(defvar ivy--prompt nil
-  "Store the format-style prompt.
-When non-nil, it should contain one %d.")
-
 (defun ivy--insert-prompt ()
   "Update the prompt according to `ivy--prompt'."
   (when ivy--prompt
     (let ((inhibit-read-only t)
-          (n-str (format ivy--prompt ivy--length)))
+          (n-str
+           (format
+            (if ivy--directory
+                (concat ivy--prompt (abbreviate-file-name ivy--directory))
+              ivy--prompt) ivy--length)))
       (save-excursion
         (goto-char (point-min))
         (delete-region (point-min) (minibuffer-prompt-end))
@@ -407,6 +545,13 @@ When non-nil, it should contain one %d.")
 Should be run via minibuffer `post-command-hook'."
   (setq ivy-text (ivy--input))
   (ivy--cleanup)
+  (when ivy--directory
+    (if (string-match "/$" ivy-text)
+        (if (member ivy-text ivy--all-candidates)
+            (ivy--cd (expand-file-name ivy-text ivy--directory))
+          (ivy--cd "/"))
+      (if (string-match "~$" ivy-text)
+          (ivy--cd (expand-file-name "~/")))))
   (let ((text (ivy-completions
                ivy-text
                ivy--all-candidates))
@@ -421,12 +566,6 @@ Should be run via minibuffer `post-command-hook'."
         (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--add-face (str face)
   "Propertize STR with FACE.
 `font-lock-append-text-property' is used, since it's better than
@@ -440,13 +579,21 @@ 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 (and (equal re ivy--old-re)
-                         ivy--old-cands)
-                    ivy--old-cands
-                  (ignore-errors
-                    (cl-remove-if-not
-                     (lambda (x) (string-match re x))
-                     candidates))))
+         (cands (cond ((and (equal re ivy--old-re)
+                            ivy--old-cands)
+                       ivy--old-cands)
+                      ((and ivy--old-re
+                            (not (equal ivy--old-re ""))
+                            (eq 0 (cl-search ivy--old-re re)))
+                       (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)))))
          (tail (nthcdr ivy--index ivy--old-cands))
          (ww (window-width))
          idx)
@@ -469,6 +616,12 @@ CANDIDATES is a list of strings."
              (end (min (+ start (1- ivy-height)) ivy--length))
              (cands (cl-subseq cands start end))
              (index (min ivy--index half-height (1- (length cands)))))
+        (when ivy--directory
+          (setq cands (mapcar (lambda (x)
+                                (if (string-match-p "/$" x)
+                                    (propertize x 'face 'ivy-subdir)
+                                  x))
+                              cands)))
         (setq ivy--current (copy-sequence (nth index cands)))
         (setf (nth index cands)
               (ivy--add-face ivy--current 'ivy-current-match))