#+title: Completion configuration using ivy, counsel and friends

* selectrum
  #+begin_src emacs-lisp :load no
    (use-package selectrum
      :ensure t
      :init
      (defun jao-selectrum--ord-refine (&rest args)
        (let ((completion-styles '(orderless)))
          (apply #'selectrum-refine-candidates-using-completions-styles args)))

      (defun jao-selectrum-orderless ()
        (interactive)
        (setq selectrum-refine-candidates-function #'jao-selectrum--ord-refine)
        (setq selectrum-highlight-candidates-function #'orderless-highlight-matches)
        (setq orderless-skip-highlighting (lambda () selectrum-is-active)))


      :config
      ;; https://github.com/raxod502/selectrum/wiki/Ido,-icomplete(fido)-emulation
      (defun selectrum-fido-backward-updir ()
        "Delete char before or go up directory, like `ido-mode'."
        (interactive)
        (if (and (eq (char-before) ?/)
                 (eq (selectrum--get-meta 'category) 'file))
            (save-excursion
              (goto-char (1- (point)))
              (when (search-backward "/" (point-min) t)
                (delete-region (1+ (point)) (point-max))))
          (call-interactively 'backward-delete-char)))

      (defun selectrum-fido-delete-char ()
        "Delete char or maybe call `dired', like `ido-mode'."
        (interactive)
        (let ((end (point-max)))
          (if (or (< (point) end) (not (eq (selectrum--get-meta 'category) 'file)))
              (call-interactively 'delete-char)
            (dired (file-name-directory (minibuffer-contents)))
            (exit-minibuffer))))

      (defun selectrum-fido-ret ()
        "Exit minibuffer or enter directory, like `ido-mode'."
        (interactive)
        (let* ((dir (and (eq (selectrum--get-meta 'category) 'file)
                         (file-name-directory (minibuffer-contents))))
               (current (selectrum-get-current-candidate))
               (probe (and dir current
                           (expand-file-name (directory-file-name current) dir))))
          (if (and probe (file-directory-p probe) (not (string= current "./")))
              (selectrum-insert-current-candidate)
            (selectrum-select-current-candidate))))

      ;; (define-key selectrum-minibuffer-map (kbd "RET") 'selectrum-fido-ret)
      (define-key selectrum-minibuffer-map (kbd "DEL") 'selectrum-fido-backward-updir)
      (define-key selectrum-minibuffer-map (kbd "C-d") 'selectrum-fido-delete-char)

      :custom ((selectrum-complete-in-buffer t)
               ;; (selectrum-display-action '(display-buffer-at-bottom))
               (selectrum-extend-current-candidate-highlight t)
               (selectrum-fix-vertical-window-height nil)
               (selectrum-max-window-height 20)
               (selectrum-show-indices nil)
               (selectrum-count-style 'current/matches))
      :bind (("C-R" . selectrum-repeat)))
  #+end_src
* ivy
  #+begin_src emacs-lisp
    (use-package ivy
      :ensure t
      :demand t
      :custom
      ((ivy-count-format "(%d/%d) ")
       (ivy-do-completion-in-region t)
       (ivy-height 20)
       (ivy-re-builders-alist '((counsel-ag . ivy--regex)
                                (counsel-rg . ivy--regex)
                                (counsel-yank-pop . ivy--regex)
                                (swiper . ivy--regex)
                                (swiper-isearch . ivy--regex)
                                (t . ivy--regex-fuzzy)))
       (ivy-use-virtual-buffers t)
       (ivy-virtual-abbreviate 'abbreviate)
       (ivy-wrap t))

      :config
      ;; used by ivy--regex-fuzzy to order results
      (use-package flx :ensure t)

      ;; Try C-o in the minibuffer
      (use-package ivy-hydra
        :after ivy
        :ensure t
        :init (setq ivy-read-action-function #'ivy-hydra-read-action))

      (add-to-list 'ivy-initial-inputs-alist
                   '(gnus-summary-move-article . ""))

      :bind (("C-R" . ivy-resume)
             ("C-x b" . ivy-switch-buffer)
             ("C-c v" . ivy-push-view)
             ("C-c V" . ivy-pop-view))
      :diminish)
  #+end_src
* counsel
  #+begin_src emacs-lisp
    (use-package counsel
      :ensure t
      :custom ((counsel-describe-function-function 'helpful-callable)
               (counsel-describe-variable-function 'helpful-variable)
               (counsel-find-file-at-point t)
               (counsel-linux-app-format-function
                #'counsel-linux-app-format-function-name-pretty)
               (counsel-mode-override-describe-bindings nil)
               (counsel-recentf-include-xdg-list t))
      :config
      :bind (("C-s" . swiper-isearch)
             ("C-S-s" . isearch-forward)
             ("M-x" . counsel-M-x)
             ("C-x f" . counsel-find-file)
             ("C-c k" . counsel-ag)
             ("C-c K" . counsel-rg)
             ("C-c l" . counsel-locate)
             ("C-c b" . counsel-git)
             ("C-c i" . counsel-imenu)
             ("C-c G" . counsel-search)
             ("s-r" . counsel-linux-app))
      :diminish)
  #+end_src
* counsel add-ons
*** notmuch
    #+begin_src emacs-lisp
      (use-package counsel-notmuch
        :ensure t
        :config (with-eval-after-load "gnus-group"
                  (define-key gnus-group-mode-map "Gg" 'counsel-notmuch)))
    #+end_src
*** recoll
    #+begin_src emacs-lisp
      (require 'jao-recoll)
      (defvar jao-counsel-recoll--history nil)
      (defun jao-counsel-recoll--function (str)
        (let ((xs (counsel-recoll-function str)))
          (cl-remove-if-not (lambda (x) (string-prefix-p "file://" x)) xs)))

      (defun jao-counsel-recoll (&optional initial-input)
        (interactive)
        (counsel-require-program "recoll")
        (ivy-read "recoll: " 'jao-counsel-recoll--function
                  :initial-input initial-input
                  :dynamic-collection t
                  :history 'jao-counsel-recoll--history
                  :action (lambda (x)
                            (when (string-match "file://\\(.*\\)\\'" x)
                              (let ((file-name (match-string 1 x)))
                                (if (string-match "pdf$" x)
                                    (jao-open-doc file-name)
                                  (find-file file-name)))))
                  :unwind #'counsel-delete-process
                  :caller 'jao-counsel-recoll))

      (defun jao-counsel-recoll--recoll (_s) (jao-recoll ivy-text))

      (ivy-set-actions 'jao-counsel-recoll
                       '(("x" jao-counsel-recoll--recoll "List in buffer")))

      (global-set-key (kbd "C-c R") #'jao-counsel-recoll)
    #+end_src
* ivy rich
  #+begin_src emacs-lisp
    (use-package ivy-rich
      :after (ivy counsel)
      :ensure t
      :custom ((ivy-rich-path-style 'relative)
               (ivy-rich-parse-remote-buffer nil)
               (ivy-rich-parse-remote-file-path nil))
      :config
      (ivy-rich-modify-columns
       'ivy-switch-buffer
       '((ivy-rich-candidate (:width 80))
         (ivy-rich-switch-buffer-indicators (:face jao-themes-f00))
         (ivy-rich-switch-buffer-project (:width 15))
         (ivy-rich-switch-buffer-major-mode (:width 15 :face jao-themes-f12)))))
  #+end_src
* cmap
  #+begin_src emacs-lisp
    (jao-load-path "cmap")
    (use-package cmap
      :demand t
      :bind (("C-;" . cmap-cmap)
             ("C-'" . cmap-default)))
  #+end_src
*** prompter
    #+begin_src emacs-lisp
      (defun jao-cmap--hide-help ()
        (when-let ((w (get-buffer-window (help-buffer))))
          (with-selected-window w (kill-buffer-and-window))))

      (defun jao-cmap--prompter (keymap)
        (let ((display-buffer-alist '(("*Help*"
                                       (display-buffer-at-bottom)
                                       (window-parameters (mode-line-format . none))
                                       (window-height . fit-window-to-buffer)))))
          (let ((inhibit-message t))
            (describe-keymap keymap))))

      (defun jao-cmap--prompter-done ()
        (save-current-buffer (jao-cmap--hide-help)))

      (setq cmap-prompter #'jao-cmap--prompter)
      (setq cmap-prompter-done #'jao-cmap--prompter-done)
    #+end_src
*** minibuffer actions
    #+begin_src emacs-lisp
      (defun jao-cmap--completion-metadata ()
        (completion-metadata
         (buffer-substring-no-properties (field-beginning) (point))
         minibuffer-completion-table
         minibuffer-completion-predicate))

      (defun jao-cmap--completion-category ()
        (completion-metadata-get (jao-cmap--completion-metadata) 'category))

      (defmacro cmap-define-keymap (v d &rest b)
        `(defvar ,v (cmap-keymap ,@b) ,d))

      (cmap-define-keymap jao-cmap-buffer-map
        "Keymap for buffer actions."
        ("k" . kill-buffer)
        ("b" . switch-to-buffer)
        ("o" . switch-to-buffer-other-window)
        ("z" . bury-buffer)
        ("q" . kill-buffer-and-window)
        ("=" . ediff-buffers))

      ;; (cmap-define-keymap espotify-item-keymap
      ;;   "Actions for Spotify search results"
      ;;   ("a" espotify--play-album)
      ;;   ("h" espotify--show-info))

      (defvar jao-cmap--smaps
        '((command . cmap-command-map)
          ;; (espotify-search-item . espotify-item-keymap)
          (function . cmap-function-map)
          (variable . cmap-variable-map)
          (face . cmap-face-map)
          (buffer . jao-cmap-buffer-map)
          (consult-buffer . jao-cmap-buffer-map)))

      (defun jao-cmap-target-minibuffer-candidate ()
        (when (minibuffer-window-active-p (selected-window))
          (let ((cand (ivy-state-current ivy-last))
                (cat (jao-cmap--completion-category)))
            (when-let (m (alist-get cat jao-cmap--smaps))
              (cons m cand)))))

      (add-to-list 'cmap-targets #'jao-cmap-target-minibuffer-candidate)
    #+end_src
*** url / video actions
    #+begin_src emacs-lisp
      (defvar jao-cmap-video-url-rx
        (format "^https?://\\(?:www\\.\\)?%s/.+"
                (regexp-opt '("youtu.be"
                              "youtube.com"
                              "blip.tv"
                              "vimeo.com"
                              "infoq.com")
                            t))
        "A regular expression matching URLs that point to video streams")

      (defun jao-cmap--play-video (player url)
        (interactive "sURL: ")
        (let ((cmd (format "%s %s" player (shell-quote-argument url))))
          (start-process-shell-command player nil cmd)))

      (defun jao-cmap-mpv (&optional url)
        "Play video stream with mpv"
        (interactive "sURL: ")
        (jao-cmap--play-video "mpv" url))

      (defun jao-cmap-vlc (&optional url)
        "Play video stream with vlc"
        (interactive "sURL: ")
        (jao-cmap--play-video "vlc" url))

      (defun jao-cmap-target-w3m-url ()
        (when-let (url (or (thing-at-point-url-at-point)
                           (w3m-anchor)
                           w3m-current-url))
          (cons 'cmap-url-map url)))

      (defun jao-cmap-kill (&optional x)
        "Save to kill ring"
        (interactive "s")
        (kill-new x))

      (defun jao-cmap-url (url)
        "Browse URL, externally if we're already in emacs-w3m"
        (if (derived-mode-p 'w3m-mode)
            (jao-browse-with-external-browser url)
          (browse-url url)))

      (define-key cmap-url-map [return] #'jao-cmap-url)
      (define-key cmap-url-map "f" #'browse-url-firefox)
      (define-key cmap-url-map "w" #'jao-cmap-kill)

      (defun jao-cmap-target-video-url ()
        (when-let (url (jao-cmap-target-w3m-url))
          (when (string-match-p jao-cmap-video-url-rx (cdr url))
            (cons 'jao-cmap-video-url-map (cdr url)))))

      (cmap-define-keymap jao-cmap-video-url-map
        "Actions on URLs pointing to remote video streams."
        ("v" . jao-cmap-vlc)
        ([return] . jao-cmap-mpv))

      (add-to-list 'cmap-targets #'jao-cmap-target-w3m-url)
      (add-to-list 'cmap-targets #'jao-cmap-target-video-url)
    #+end_src
* hooks
  #+begin_src emacs-lisp
    (with-eval-after-load "exwm"
      (add-to-list 'exwm-input-global-keys '([?\s-r] . counsel-linux-app)))

    (with-eval-after-load "espotify"
      (require 'ivy-spotify)
      (defalias 'jao-spotify-album #'ivy-spotify-album)
      (defalias 'jao-spotify-track #'ivy-spotify-track)
      (defalias 'jao-spotify-artist #'ivy-spotify-artist)
      (defalias 'jao-spotify-playlist #'ivy-spotify-playlist))
  #+end_src
* startup
  #+begin_src emacs-lisp
    (ivy-mode 1)
    (counsel-mode 1)
    (ivy-rich-mode 1)
    (ivy-rich-project-root-cache-mode 1)
  #+end_src