summaryrefslogtreecommitdiff
path: root/helm-comint.el
blob: 6f85249bfa06a05e96e6391cf225440ec8d730d1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
;;; helm-comint.el --- Comint prompt navigation for helm. -*- lexical-binding: t -*-

;; Copyright (C) 2020 Pierre Neidhardt <mail@ambrevar.xyz>

;; 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:
;;
;; You can bind this as follows in .emacs:
;;
;; (add-hook 'comint-mode-hook
;;           (lambda ()
;;               (define-key comint-mode-map (kbd "M-s f") 'helm-comint-prompts-all)))

;;; Code:
(require 'cl-lib)
(require 'helm)
(require 'helm-lib)
(require 'helm-help)
(require 'helm-elisp)

;;; Comint prompts
;;
(defface helm-comint-prompts-promptidx
  `((t ,@(and (>= emacs-major-version 27) '(:extend t))
       (:foreground "cyan")))
  "Face used to highlight comint prompt index."
  :group 'helm-comint-faces)

(defface helm-comint-prompts-buffer-name
  `((t ,@(and (>= emacs-major-version 27) '(:extend t))
       (:foreground "green")))
  "Face used to highlight comint buffer name."
  :group 'helm-comint-faces)

(defcustom helm-comint-prompts-promptidx-p t
  "Show prompt number."
  :group 'helm-comint
  :type 'boolean)

(defcustom helm-comint-mode-list '(comint-mode slime-repl-mode sly-mrepl-mode sql-interactive-mode)
  "Supported modes for prompt navigation.
Derived modes (e.g., Geiser's REPL) are automatically supported."
  :group 'helm-comint
  :type '(repeat (choice symbol)))

(defcustom helm-comint-next-prompt-function '((sly-mrepl-mode . (lambda ()
                                                                  (sly-mrepl-next-prompt)
                                                                  (point))))
  "Alist of (MODE . NEXT-PROMPT-FUNCTION) to use.
 If the current major mode is a key in this list, the associated
 function will be used to navigate the prompts.
 The function must return the point after the prompt.
 Otherwise (comint-next-prompt 1) will be used."
  :group 'helm-comint
  :type '(alist :key-type symbol :value-type function))

(defcustom helm-comint-max-offset 400
  "Max number of chars displayed per candidate in comint-input-ring browser.
When t, don't truncate candidate, show all.
By default it is approximatively the number of bits contained in
five lines of 80 chars each i.e 80*5.
Note that if you set this to nil multiline will be disabled, i.e
you will not have anymore separators between candidates."
  :type '(choice (const :tag "Disabled" t)
          (integer :tag "Max candidate offset"))
  :group 'helm-misc)

(defvar helm-comint-prompts-keymap
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map helm-map)
    (define-key map (kbd "C-c o")   'helm-comint-prompts-other-window)
    (define-key map (kbd "C-c C-o") 'helm-comint-prompts-other-frame)
    map)
  "Keymap for `helm-comint-prompt-all'.")

(defun helm-comint-prompts-list (mode &optional buffer)
  "List the prompts in BUFFER in mode MODE.

Return a list of (\"prompt\" (point) (buffer-name) prompt-index))
E.g. (\"ls\" 162 \"*shell*\" 3).
If BUFFER is nil, use current buffer."
  (with-current-buffer (or buffer (current-buffer))
    (when (derived-mode-p mode)
      (save-excursion
        (goto-char (point-min))
        (let (result (count 1))
          (save-mark-and-excursion
            (helm-awhile (and (not (eobp))
                              (helm-aif (alist-get major-mode helm-comint-next-prompt-function)
                                  (funcall it)
                                (comint-next-prompt 1)))
              (push (list (buffer-substring-no-properties
                           it (point-at-eol))
                          it (buffer-name) count)
                    result)
              (setq count (1+ count))))
          (nreverse result))))))

(defun helm-comint-prompts-list-all (mode)
  "List the prompts of all buffers in mode MODE.
See `helm-comint-prompts-list'."
  (cl-loop for b in (buffer-list)
           append (helm-comint-prompts-list mode b)))

(defun helm-comint-prompts-transformer (candidates &optional all)
  ;; ("ls" 162 "*shell*" 3) => ("*shell*:3:ls" . ("ls" 162 "*shell*" 3))
  (cl-loop for (prt pos buf id) in candidates
           collect `(,(concat
                       (when all
                         (concat (propertize
                                  buf
                                  'face 'helm-comint-prompts-buffer-name)
                                 ":"))
                       (when helm-comint-prompts-promptidx-p
                         (concat (propertize
                                  (number-to-string id)
                                  'face 'helm-comint-prompts-promptidx)
                                 ":"))
                       prt)
                      . ,(list prt pos buf id))))

(defun helm-comint-prompts-all-transformer (candidates)
  (helm-comint-prompts-transformer candidates t))

(cl-defun helm-comint-prompts-goto (candidate &optional (action 'switch-to-buffer))
  ;; Candidate format: ("ls" 162 "*shell*" 3)
  (let ((buf (nth 2 candidate)))
    (unless (and (string= (buffer-name) buf)
                 (eq action 'switch-to-buffer))
      (funcall action buf))
    (goto-char (nth 1 candidate))
    (recenter)))

(defun helm-comint-prompts-goto-other-window (candidate)
  (helm-comint-prompts-goto candidate 'switch-to-buffer-other-window))

(defun helm-comint-prompts-goto-other-frame (candidate)
  (helm-comint-prompts-goto candidate 'switch-to-buffer-other-frame))

(defun helm-comint-prompts-other-window ()
  (interactive)
  (with-helm-alive-p
    (helm-exit-and-execute-action 'helm-comint-prompts-goto-other-window)))
(put 'helm-comint-prompts-other-window 'helm-only t)

(defun helm-comint-prompts-other-frame ()
  (interactive)
  (with-helm-alive-p
    (helm-exit-and-execute-action 'helm-comint-prompts-goto-other-frame)))
(put 'helm-comint-prompts-other-frame 'helm-only t)

;;;###autoload
(defun helm-comint-prompts ()
  "Pre-configured `helm' to browse the prompts of the current comint buffer."
  (interactive)
  (if (apply 'derived-mode-p helm-comint-mode-list)
      (helm :sources
            (helm-build-sync-source "Comint prompts"
              :candidates (helm-comint-prompts-list major-mode)
              :candidate-transformer 'helm-comint-prompts-transformer
              :action '(("Go to prompt" . helm-comint-prompts-goto)))
            :buffer "*helm comint prompts*")
    (message "Current buffer is not a comint buffer")))

;;;###autoload
(defun helm-comint-prompts-all ()
  "Pre-configured `helm' to browse the prompts of all comint sessions."
  (interactive)
  (if (apply 'derived-mode-p helm-comint-mode-list)
      (helm :sources
            (helm-build-sync-source "All comint prompts"
              :candidates (helm-comint-prompts-list-all major-mode)
              :candidate-transformer 'helm-comint-prompts-all-transformer
              :action (quote (("Go to prompt" . helm-comint-prompts-goto)
                              ("Go to prompt in other window `C-c o`" .
                               helm-comint-prompts-goto-other-window)
                              ("Go to prompt in other frame `C-c C-o`" .
                               helm-comint-prompts-goto-other-frame)))
              :keymap helm-comint-prompts-keymap)
            :buffer "*helm comint all prompts*")
    (message "Current buffer is not a comint buffer")))

;;; Comint history
;;
;;
(defun helm-comint-input-ring-action (candidate)
  "Default action for comint history."
  (with-helm-current-buffer
    (delete-region (comint-line-beginning-position) (point-max))
    (insert candidate)))

(defvar helm-source-comint-input-ring
  (helm-build-sync-source "Comint history"
    :candidates (lambda ()
                  (with-helm-current-buffer
                    (cl-loop for elm in (ring-elements comint-input-ring)
                             unless (string= elm "")
                             collect elm)))
    :action 'helm-comint-input-ring-action
    ;; Multiline does not work for `shell' because of an Emacs bug.
    ;; It works in other REPLs like Geiser.
    :multiline 'helm-comint-max-offset)
  "Source that provides Helm completion against `comint-input-ring'.")

;;;###autoload
(defun helm-comint-input-ring ()
  "Preconfigured `helm' that provide completion of `comint' history."
  (interactive)
  (when (or (derived-mode-p 'comint-mode)
            (member major-mode helm-comint-mode-list))
    (helm :sources 'helm-source-comint-input-ring
          :input (buffer-substring-no-properties (comint-line-beginning-position)
                                                 (point-at-eol))
          :buffer "*helm comint history*")))

(provide 'helm-comint)

;;; helm-comint.el ends here