spacemacs/core/libs/ido-vertical-mode.el

396 lines
17 KiB
EmacsLisp

;;; ido-vertical-mode.el --- Makes ido-mode display vertically
;; Copyright (C) 2013, 2014 Steven Degutis
;; Author: Steven Degutis
;; Maintainer: Christopher Reichert <creichert07@gmail.com>
;; Version: 1.0.1
;; Package-Requires: ((emacs "24.4"))
;; Keywords: convenience
;; URL: https://github.com/creichert/ido-vertical-mode.el
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Makes ido-mode display prospects vertically
;;; Code:
(require 'ido)
(require 'cl-lib)
;;; The following three variables and their comments are lifted
;;; directly from `ido.el'; they are defined here to avoid compile-log
;;; warnings. See `ido.el' for more information.
;; Non-nil if we should add [confirm] to prompt
(defvar ido-show-confirm-message)
;; Remember if current directory is non-readable (so we cannot do completion).
(defvar ido-directory-nonreadable)
;; Remember if current directory is 'huge' (so we don't want to do completion).
(defvar ido-directory-too-big)
(defvar ido-vertical-decorations nil
"Changing the decorations does most of the work for ido-vertical
This sets up newlines and arrows before, between, and after the
prospects. For additional information, see `ido-decorations'.")
(defcustom ido-vertical-padding " "
"How many spaces to pad the completion candidates.
When setting this variable in ELISP, you must also make sure
`ido-vertical-decorations' is updated. In addition, if
`ido-vertical-mode' is on, it must be set to the new value of
`ido-vertical-decorations' for this variable to take effect in
the next ido completion event."
:type 'string
:group 'ido-vertical
:initialize 'custom-initialize-default
:set (lambda (symbol value)
(set-default symbol value)
(setq ido-vertical-decorations (ido-vertical-make-decorations :padding value))
(when (bound-and-true-p ido-vertical-mode)
(setq ido-decorations ido-vertical-decorations))))
(defcustom ido-vertical-indicator "->"
"Indicator displayed next to the candidate that will be selected.
When setting this variable in ELISP, you must also make sure
`ido-vertical-decorations' is updated. In addition, if
`ido-vertical-mode' is on, it must be set to the new value of
`ido-vertical-decorations' for this variable to take effect in
the next ido completion event."
:type 'string
:group 'ido-vertical
:initialize 'custom-initialize-default
:set (lambda (symbol value)
(set-default symbol value)
(setq ido-vertical-decorations (ido-vertical-make-decorations :indicator value))
(when (bound-and-true-p ido-vertical-mode)
(setq ido-decorations ido-vertical-decorations))))
(defvar ido-vertical-old-decorations nil
"The original `ido-decorations' variable
We need to keep track of the original value so we can restore it
when turning `ido-vertical-mode' off")
(defvar ido-vertical-old-completions nil
"The original `ido-completions' function
We need to keep track of the original value of `ido-completions'
so we can restore it when turning `ido-vertical-mode' off")
(defgroup ido-vertical nil
"Make ido behave vertically."
:group 'ido)
(defcustom ido-vertical-show-count nil
"Non nil means show the count of candidates while completing."
:type 'boolean
:group 'ido-vertical)
(defvar ido-vertical-count-active nil
"Used internally to track whether we're already showing the count")
(defcustom ido-vertical-define-keys nil
"Defines which keys that `ido-vertical-mode' redefines."
:type '(choice
(const :tag "Keep default ido keys." nil)
(const :tag "C-p and C-n are up & down in match" C-n-and-C-p-only)
(const :tag "C-p/up and C-n/down are up and down in match." C-n-C-p-up-and-down)
(const :tag "C-p/up, C-n/down are up/down in match. left or right cycle history or directory." C-n-C-p-up-down-left-right))
:group 'ido-vertical)
(defcustom ido-vertical-pad-list t
"Non nil means to pad the list of candidates to ensure the minibuffer area is always tall"
:type 'boolean
:group 'ido-vertical)
(defcustom ido-vertical-disable-if-short nil
"Non nil means that ido will go back to horizontal mode if the candidates all fit in the minibuffer area"
:type 'boolean
:group 'ido-vertical)
(defface ido-vertical-first-match-face
'((t (:inherit ido-first-match)))
"Face used by Ido Vertical for highlighting first match."
:group 'ido-vertical)
(defface ido-vertical-only-match-face
'((t (:inherit ido-only-match)))
"Face used by Ido Vertical for highlighting only match."
:group 'ido-vertical)
(defface ido-vertical-match-face
'((t (:inherit font-lock-variable-name-face :bold t :underline t)))
"Face used by Ido Vertical for the matched part."
:group 'ido-vertical)
(cl-defun ido-vertical-make-decorations (&key (padding ido-vertical-padding)
(indicator ido-vertical-indicator))
"Construct a new `ido-decorations' format."
(list
(concat "\n" indicator padding) ; left bracket around prospect list
"" ; right bracket around prospect list
(concat (format (format "\n%%-%ds" (length indicator)) "") padding) ; separator between prospects, depends on `ido-separator`
(concat (format (format "\n%%-%ds" (length indicator)) "") padding "...") ; inserted at the end of a truncated list of prospects
"[" ; left bracket around common match string
"]" ; right bracket around common match string
" [No match]"
" [Matched]"
" [Not readable]"
" [Too big]"
" [Confirm]"
(concat "\n" indicator padding) ; left bracket around the sole remaining completion
"" ; right bracket around the sole remaining completion
))
(defun ido-vertical-or-horizontal-completions (name)
(if (and ido-vertical-disable-if-short
(<= (length ido-matches) ido-max-prospects))
(let ((short-result
(let ((ido-decorations ido-vertical-old-decorations))
(funcall ido-vertical-old-completions name))))
(if (>= (window-body-width (minibuffer-window))
(+ (minibuffer-prompt-width)
(length short-result)))
short-result
(ido-vertical-completions name)))
(ido-vertical-completions name)))
;; borrowed from ido.el and modified to work better when vertical
(defun ido-vertical-completions (name)
;; Return the string that is displayed after the user's text.
;; Modified from `icomplete-completions'.
(let* ((comps ido-matches)
(ind (and (consp (car comps)) (> (length (cdr (car comps))) 1)
ido-merged-indicator))
(lencomps (length comps))
(additional-items-indicator (nth 3 ido-decorations))
(comps-empty (null comps))
(ncomps lencomps)
first)
;; Keep the height of the suggestions list constant by padding
;; when lencomps is too small. Also, if lencomps is too short, we
;; should not indicate that there are additional prospects.
(when (< lencomps (1+ ido-max-prospects))
(setq additional-items-indicator "\n")
(when ido-vertical-pad-list
(setq comps (append comps (make-list (- (1+ ido-max-prospects) lencomps) "")))
(setq ncomps (length comps))))
(if (not ido-incomplete-regexp)
(when ido-use-faces
;; Make a copy of [ido-matches], otherwise the selected string
;; could contain text properties which could lead to weird
;; artifacts, e.g. buffer-file-name having text properties.
(setq comps (cl-loop for comps-i being the elements of (if (eq comps ido-matches)
ido-matches
comps)
do
(setf comps-i (substring-no-properties
(if (listp comps-i)
(car comps-i)
comps-i)
0))
(when (string-match (if ido-enable-regexp name (regexp-quote name)) comps-i)
(ignore-errors
(add-face-text-property (match-beginning 0)
(match-end 0)
'ido-vertical-match-face
nil comps-i)))
collect comps-i))))
(if (and ind ido-use-faces)
(put-text-property 0 1 'face 'ido-indicator ind))
(when ido-vertical-show-count
(setcar ido-vertical-decorations (concat (format " [%d]\n%s" lencomps ido-vertical-indicator)
ido-vertical-padding))
(setq ido-vertical-count-active t))
(when (and (not ido-vertical-show-count)
ido-vertical-count-active)
(setcar ido-vertical-decorations (concat "\n" ido-vertical-indicator ido-vertical-padding))
(setq ido-vertical-count-active nil))
(if (and ido-use-faces comps)
(let* ((fn (ido-name (car comps)))
(ln (length fn)))
(setq first (format "%s" fn))
(if (fboundp 'add-face-text-property)
(add-face-text-property 0 (length first)
(cond ((> lencomps 1)
'ido-vertical-first-match-face)
(ido-incomplete-regexp
'ido-incomplete-regexp)
(t
'ido-vertical-only-match-face))
nil first)
(put-text-property 0 ln 'face
(if (= lencomps 1)
(if ido-incomplete-regexp
'ido-incomplete-regexp
'ido-vertical-only-match-face)
'ido-vertical-first-match-face)
first))
(if ind (setq first (concat first ind)))
(setq comps (cons first (cdr comps)))))
;; Previously we'd check null comps to see if the list was
;; empty. We pad the list with empty items to keep the list at a
;; constant height, so we have to check if the entire list is
;; empty, instead of (null comps)
(cond (comps-empty
(cond
(ido-show-confirm-message
(or (nth 10 ido-decorations) " [Confirm]"))
(ido-directory-nonreadable
(or (nth 8 ido-decorations) " [Not readable]"))
(ido-directory-too-big
(or (nth 9 ido-decorations) " [Too big]"))
(ido-report-no-match
(nth 6 ido-decorations)) ;; [No match]
(t "")))
(ido-incomplete-regexp
(concat " " (car comps)))
((null (cdr comps)) ;one match
(concat (concat (nth 11 ido-decorations) ;; [ ... ]
(ido-name (car comps))
(nth 12 ido-decorations))
(if (not ido-use-faces) (nth 7 ido-decorations)))) ;; [Matched]
(t ;multiple matches
(let* ((items (if (> ido-max-prospects 0) (1+ ido-max-prospects) 999))
(alternatives
(apply
#'concat
(cdr (apply
#'nconc
(mapcar
(lambda (com)
(setq com (ido-name com))
(setq items (1- items))
(cond
((< items 0) ())
((= items 0) (list additional-items-indicator)) ; " | ..."
(t
(list (nth 2 ido-decorations) ; " | "
(let ((str (substring com 0)))
(if (and ido-use-faces
(not (string= str first))
(ido-final-slash str))
(put-text-property 0 (length str) 'face 'ido-subdir str))
str)))))
comps))))))
(concat
;; put in common completion item -- what you get by pressing tab
(if (and (stringp ido-common-match-string)
(> (length ido-common-match-string) (length name)))
(concat (nth 4 ido-decorations) ;; [ ... ]
(substring ido-common-match-string (length name))
(nth 5 ido-decorations)))
;; list all alternatives
(nth 0 ido-decorations) ;; { ... }
alternatives
(nth 1 ido-decorations)))))))
(defun ido-vertical-disable-line-truncation ()
"Prevent the newlines in the minibuffer from being truncated"
(set (make-local-variable 'truncate-lines) nil))
(defun ido-vertical-turn-on ()
(if (and (eq nil ido-vertical-old-decorations)
(eq nil ido-vertical-old-completions))
(progn
(setq ido-vertical-old-decorations ido-decorations)
(setq ido-vertical-old-completions (symbol-function 'ido-completions))))
(setq ido-vertical-decorations (ido-vertical-make-decorations) ido-decorations ido-vertical-decorations)
(fset 'ido-completions 'ido-vertical-or-horizontal-completions)
(add-hook 'ido-minibuffer-setup-hook 'ido-vertical-disable-line-truncation)
(add-hook 'ido-setup-hook 'ido-vertical-define-keys))
(make-obsolete 'turn-on-ido-vertical 'ido-vertical-turn-on "1.0.1")
(defun ido-vertical-turn-off ()
(setq ido-decorations ido-vertical-old-decorations)
(fset 'ido-completions ido-vertical-old-completions)
(remove-hook 'ido-minibuffer-setup-hook 'ido-vertical-disable-line-truncation)
(remove-hook 'ido-setup-hook 'ido-vertical-define-keys))
(make-obsolete 'turn-off-ido-vertical 'ido-vertical-turn-off "1.0.1")
(defun ido-vertical-next-match ()
"Call the correct next-match function for right key.
This is based on:
- Different functions for completing directories and prior history.
"
(interactive)
(cond
((and (boundp 'item) item (eq item 'file))
(ido-next-match-dir))
(t
(next-history-element 1))))
(defun ido-vertical-prev-match ()
"Call the correct prev-match function for left key.
This is based on:
- Different functions for completing directories and prior history.
"
(interactive)
(cond
((and (boundp 'item) item (eq item 'file))
(ido-prev-match-dir))
(t
(previous-history-element 1))))
(defun ido-vertical-define-keys () ;; C-n/p is more intuitive in vertical layout
(when ido-vertical-define-keys
(define-key ido-completion-map (kbd "C-n") 'ido-next-match)
(define-key ido-completion-map (kbd "C-p") 'ido-prev-match)
(define-key ido-completion-map (kbd "C-c C-t") 'ido-toggle-prefix))
(when (memq ido-vertical-define-keys '(C-n-C-p-up-and-down C-n-C-p-up-down-left-right))
(define-key ido-completion-map (kbd "<up>") 'ido-prev-match)
(define-key ido-completion-map (kbd "<down>") 'ido-next-match))
(when (eq ido-vertical-define-keys 'C-n-C-p-up-down-left-right)
(define-key ido-completion-map (kbd "<left>") 'ido-vertical-prev-match)
(define-key ido-completion-map (kbd "<right>") 'ido-vertical-next-match)))
;;;###autoload
(define-minor-mode ido-vertical-mode
"Makes ido-mode display vertically."
:global t
(if ido-vertical-mode
(ido-vertical-turn-on)
(ido-vertical-turn-off)))
(provide 'ido-vertical-mode)
;; Local Variables:
;; indent-tabs-mode: nil
;; End:
;;; ido-vertical-mode.el ends here