;;; cider-selector.el --- Buffer selection command inspired by SLIME's selector -*- lexical-binding: t -*-

;; Copyright © 2012-2013 Tim King, Phil Hagelberg, Bozhidar Batsov
;; Copyright © 2013-2019 Bozhidar Batsov, Artur Malabarba and CIDER contributors
;;
;; Author: Tim King <kingtim@gmail.com>
;;         Phil Hagelberg <technomancy@gmail.com>
;;         Bozhidar Batsov <bozhidar@batsov.com>
;;         Artur Malabarba <bruce.connor.am@gmail.com>
;;         Hugo Duncan <hugo@hugoduncan.org>
;;         Steve Purcell <steve@sanityinc.com>

;; 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/>.

;; This file is not part of GNU Emacs.

;;; Commentary:

;; Buffer selection command inspired by SLIME's selector.

;;; Code:

(require 'cider-client)
(require 'cider-eval)
(require 'cider-scratch)
(require 'cider-profile)

(defconst cider-selector-help-buffer "*CIDER Selector Help*"
  "The name of the selector's help buffer.")

(defvar cider-selector-methods nil
  "List of buffer-selection methods for the `cider-selector' command.
Each element is a list (KEY DESCRIPTION FUNCTION).
DESCRIPTION is a one-line description of what the key selects.")

(defvar cider-selector-other-window nil
  "If non-nil use `switch-to-buffer-other-window'.
Not meant to be set by users.  It's used internally
by `cider-selector'.")

(defun cider-selector--recently-visited-buffer (mode)
  "Return the most recently visited buffer, deriving its `major-mode' from MODE.
Only considers buffers that are not already visible."
  (cl-loop for buffer in (buffer-list)
           when (and (with-current-buffer buffer
                       (derived-mode-p mode))
                     ;; names starting with space are considered hidden by Emacs
                     (not (string-match-p "^ " (buffer-name buffer)))
                     (null (get-buffer-window buffer 'visible)))
           return buffer
           finally (error "Can't find unshown buffer in %S" mode)))

;;;###autoload
(defun cider-selector (&optional other-window)
  "Select a new buffer by type, indicated by a single character.
The user is prompted for a single character indicating the method by
which to choose a new buffer.  The `?' character describes then
available methods.  OTHER-WINDOW provides an optional target.
See `def-cider-selector-method' for defining new methods."
  (interactive)
  (message "Select [%s]: "
           (apply #'string (mapcar #'car cider-selector-methods)))
  (let* ((cider-selector-other-window other-window)
         (ch (save-window-excursion
               (select-window (minibuffer-window))
               (read-char)))
         (method (cl-find ch cider-selector-methods :key #'car)))
    (cond (method
           (funcall (cl-caddr method)))
          (t
           (message "No method for character: ?\\%c" ch)
           (ding)
           (sleep-for 1)
           (discard-input)
           (cider-selector)))))

(defmacro def-cider-selector-method (key description &rest body)
  "Define a new `cider-select' buffer selection method.
KEY is the key the user will enter to choose this method.

DESCRIPTION is a one-line sentence describing how the method
selects a buffer.

BODY is a series of forms which are evaluated when the selector
is chosen.  The returned buffer is selected with
`switch-to-buffer'."
  (let ((method `(lambda ()
                   (let ((buffer (progn ,@body)))
                     (cond ((not (get-buffer buffer))
                            (message "No such buffer: %S" buffer)
                            (ding))
                           ((get-buffer-window buffer)
                            (select-window (get-buffer-window buffer)))
                           (cider-selector-other-window
                            (switch-to-buffer-other-window buffer))
                           (t
                            (switch-to-buffer buffer)))))))
    `(setq cider-selector-methods
           (cl-sort (cons (list ,key ,description ,method)
                          (cl-remove ,key cider-selector-methods :key #'car))
                    #'< :key #'car))))

(def-cider-selector-method ?? "Selector help buffer."
  (ignore-errors (kill-buffer cider-selector-help-buffer))
  (with-current-buffer (get-buffer-create cider-selector-help-buffer)
    (insert "CIDER Selector Methods:\n\n")
    (cl-loop for (key line nil) in cider-selector-methods
             do (insert (format "%c:\t%s\n" key line)))
    (goto-char (point-min))
    (help-mode)
    (display-buffer (current-buffer) t))
  (cider-selector)
  (current-buffer))

(cl-pushnew (list ?4 "Select in other window" (lambda () (cider-selector t)))
            cider-selector-methods :key #'car)

(def-cider-selector-method ?c
  "Most recently visited clojure-mode buffer."
  (cider-selector--recently-visited-buffer 'clojure-mode))

(def-cider-selector-method ?e
  "Most recently visited emacs-lisp-mode buffer."
  (cider-selector--recently-visited-buffer 'emacs-lisp-mode))

(def-cider-selector-method ?q "Abort."
  (top-level))

(def-cider-selector-method ?r
  "Current REPL buffer."
  (cider-current-repl))

(def-cider-selector-method ?m
  "Current connection's *nrepl-messages* buffer."
  (nrepl-messages-buffer (cider-current-repl)))

(def-cider-selector-method ?x
  "*cider-error* buffer."
  cider-error-buffer)

(def-cider-selector-method ?p
  "CIDER profiler buffer."
  cider-profile-buffer)

(def-cider-selector-method ?d
  "*cider-doc* buffer."
  cider-doc-buffer)

(def-cider-selector-method ?s
  "*cider-scratch* buffer."
  (cider-scratch-find-or-create-buffer))

(provide 'cider-selector)

;;; cider-selector.el ends here
