]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/swiper/ivy.el
Merge commit 'efa18eca10e5a0e05043f872cf9945842bb3a034' from swiper
[gnu-emacs-elpa] / packages / swiper / ivy.el
index ee1a74bee4aedc8e56833208038609dadbef4be1..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,6 +70,11 @@ 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)
@@ -84,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.")
 
@@ -99,38 +110,89 @@ of `history-length', which see.")
 (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))
-    (if ivy--directory
-        (insert (expand-file-name ivy--current ivy--directory))
-      (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)
-  (if (and ivy--directory
-           (file-directory-p
-            (expand-file-name ivy--current ivy--directory)))
-      (progn
-        (delete-minibuffer-contents)
-        (setq ivy--directory
-              (expand-file-name ivy--current ivy--directory))
-        (setq ivy--old-cands nil)
-        (setq ivy--all-candidates
-              (let ((default-directory ivy--directory))
-                (all-completions "" 'read-file-name-internal)))
-
-        (ivy--exhibit))
-    (ivy-done)))
+  (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."
@@ -142,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")
@@ -190,18 +264,24 @@ 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)
   (if (and ivy--directory (= (minibuffer-prompt-end) (point)))
       (progn
-        (setq ivy--old-cands nil)
-        (setq ivy--all-candidates
-              (let ((default-directory (setq ivy--directory
-                                             (file-name-directory
-                                              (directory-file-name ivy--directory)))))
-                (all-completions "" 'read-file-name-internal)))
+        (ivy--cd (file-name-directory
+                  (directory-file-name ivy--directory)))
         (ivy--exhibit))
     (condition-case nil
         (backward-delete-char 1)
@@ -209,6 +289,38 @@ On error (read-only), call `ivy-on-del-error-function'."
        (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 predicate initial-input keymap preselect update-fn)
@@ -230,16 +342,30 @@ the ones that match INITIAL-INPUT.
 
 UPDATE-FN is called each time the current candidate(s) is changed."
   (setq ivy--directory nil)
-  (cond ((or (functionp collection)
+  (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))
-         (when (eq collection 'read-file-name-internal)
-           (setq ivy--directory default-directory)
-           (setq initial-input nil))
          (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))
@@ -314,7 +440,9 @@ The history, defaults and input-method arguments are ignored for now."
     "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"
@@ -322,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
@@ -333,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
@@ -388,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
@@ -419,10 +520,6 @@ 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
@@ -448,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))
@@ -462,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
@@ -481,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)
@@ -510,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))