mirror of https://github.com/Chizi123/.emacs.d.git

Chizi123
2018-11-18 76bbd07de7add0f9d13c6914f158d19630fe2f62
commit | author | age
5cb5f7 1 ;;; helm-eshell.el --- pcomplete and eshell completion for helm. -*- lexical-binding: t -*-
C 2
3 ;; Copyright (C) 2012 ~ 2018 Thierry Volpiatto <thierry.volpiatto@gmail.com>
4
5 ;; This program is free software; you can redistribute it and/or modify
6 ;; it under the terms of the GNU General Public License as published by
7 ;; the Free Software Foundation, either version 3 of the License, or
8 ;; (at your option) any later version.
9
10 ;; This program is distributed in the hope that it will be useful,
11 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 ;; GNU General Public License for more details.
14
15 ;; You should have received a copy of the GNU General Public License
16 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 ;;; Commentary:
19 ;;
20 ;; Enable like this in .emacs:
21 ;; (add-hook 'eshell-mode-hook
22 ;;           (lambda ()
23 ;;               (eshell-cmpl-initialize)
24 ;;               (define-key eshell-mode-map [remap eshell-pcomplete] 'helm-esh-pcomplete)
25 ;;               (define-key eshell-mode-map (kbd "M-s f") 'helm-eshell-prompts-all)))
26 ;;               (define-key eshell-mode-map (kbd "M-r") 'helm-eshell-history)))
27
28
29 ;;; Code:
30 (require 'cl-lib)
31 (require 'helm)
32 (require 'helm-lib)
33 (require 'helm-help)
34 (require 'helm-elisp)
35
36 (declare-function eshell-read-aliases-list "em-alias")
37 (declare-function eshell-send-input "esh-mode" (&optional use-region queue-p no-newline))
38 (declare-function eshell-bol "esh-mode")
39 (declare-function eshell-parse-arguments "esh-arg" (beg end))
40 (declare-function eshell-backward-argument "esh-mode" (&optional arg))
41 (declare-function helm-quote-whitespace "helm-lib")
42 (declare-function eshell-skip-prompt "em-prompt")
43 (defvar eshell-special-chars-outside-quoting)
44
45
46 (defgroup helm-eshell nil
47   "Helm eshell completion and history."
48   :group 'helm)
49
50
51 (defcustom helm-eshell-fuzzy-match nil
52   "Enable fuzzy matching in `helm-esh-pcomplete' when non--nil."
53   :group 'helm-eshell
54   :type 'boolean)
55
56
57 (defvar helm-eshell-history-map
58   (let ((map (make-sparse-keymap)))
59     (set-keymap-parent map helm-map)
60     (define-key map (kbd "M-p") 'helm-next-line)
61     map)
62   "Keymap for `helm-eshell-history'.")
63
64 (defvar helm-esh-completion-map
65   (let ((map (make-sparse-keymap)))
66     (set-keymap-parent map helm-map)
67     (define-key map (kbd "TAB") 'helm-next-line)
68     map)
69   "Keymap for `helm-esh-pcomplete'.")
70
71 (defvar helm-eshell--quit-flag nil)
72
73
74 (defclass helm-esh-source (helm-source-sync)
75   ((init :initform (lambda ()
76                      (setq pcomplete-current-completions nil
77                            pcomplete-last-completion-raw nil)
78                      ;; Eshell-command add this hook in all minibuffers
79                      ;; Remove it for the helm one. (Fixed in Emacs24)
80                      (remove-hook 'minibuffer-setup-hook 'eshell-mode)))
81    (candidates :initform 'helm-esh-get-candidates)
82    ;(nomark :initform t)
83    (persistent-action :initform 'ignore)
84    (nohighlight :initform t)
85    (filtered-candidate-transformer
86     :initform
87     (lambda (candidates _sources)
88       (cl-loop
89        for i in candidates
90        collect
91        (cond ((string-match "\\`~/?" helm-ec-target)
92               (abbreviate-file-name i))
93              ((string-match "\\`/" helm-ec-target) i)
94              (t
95               (file-relative-name i)))
96        into lst
97        finally return (sort lst 'helm-generic-sort-fn))))
98    (action :initform 'helm-ec-insert))
99   "Helm class to define source for Eshell completion.")
100
101 ;; Internal.
102 (defvar helm-ec-target "")
103 (defun helm-ec-insert (_candidate)
104   "Replace text at point with CANDIDATE.
105 The function that call this should set `helm-ec-target' to thing at point."
106   (set (make-local-variable 'comint-file-name-quote-list)
107        eshell-special-chars-outside-quoting)
108   (let ((pt (point)))
109     (when (and helm-ec-target
110                (search-backward helm-ec-target nil t)
111                (string= (buffer-substring (point) pt) helm-ec-target))
112       (delete-region (point) pt)))
113   (when (string-match "\\`\\*" helm-ec-target) (insert "*"))
114   (let ((marked (helm-marked-candidates)))
115     (prog1 t ;; Makes helm returns t on action.
116       (insert
117        (mapconcat
118         (lambda (x)
119           (cond ((string-match "\\`~/" helm-ec-target)
120                  ;; Strip out the first escape char added by
121                  ;; `comint-quote-filename' before "~" (Issue #1803).
122                  (substring (comint-quote-filename (abbreviate-file-name x)) 1))
123                 ((string-match "\\`/" helm-ec-target)
124                  (comint-quote-filename x))
125                 (t
126                  (concat (and (string-match "\\`[.]/" helm-ec-target) "./")
127                          (comint-quote-filename
128                           (file-relative-name x))))))
129         marked " ")
130        (or (helm-aand (car (last marked))
131                       (string-match-p "/\\'" it)
132                       "")
133            " ")))))
134
135 (defun helm-esh-get-candidates ()
136   "Get candidates for eshell completion using `pcomplete'."
137   (catch 'pcompleted
138     (with-helm-current-buffer
139       (let* ((pcomplete-stub)
140              pcomplete-seen pcomplete-norm-func
141              pcomplete-args pcomplete-last pcomplete-index
142              (pcomplete-autolist pcomplete-autolist)
143              (pcomplete-suffix-list pcomplete-suffix-list)
144              (table (pcomplete-completions))
145              (entry (or (try-completion helm-pattern
146                                         (pcomplete-entries))
147                         helm-pattern)))
148         (cl-loop ;; expand entry too to be able to compare it with file-cand.
149               with exp-entry = (and (stringp entry)
150                                     (not (string= entry ""))
151                                     (file-name-as-directory
152                                      (expand-file-name entry default-directory)))
153               with comps = (all-completions pcomplete-stub table)
154               unless comps return (prog1 nil
155                                     ;; Don't add final space when
156                                     ;; there is no completion (issue #1990).
157                                     (setq helm-eshell--quit-flag t)
158                                     (message "No completions of %s" pcomplete-stub))
159               for i in comps
160               ;; Transform the related names to abs names.
161               for file-cand = (and exp-entry
162                                    (if (file-remote-p i) i
163                                      (expand-file-name
164                                       i (file-name-directory entry))))
165               ;; Compare them to avoid dups.
166               for file-entry-p = (and (stringp exp-entry)
167                                       (stringp file-cand)
168                                       ;; Fix :/tmp/foo/ $ cd foo
169                                       (not (file-directory-p file-cand))
170                                       (file-equal-p exp-entry file-cand))
171               if (and file-cand (or (file-remote-p file-cand)
172                                     (file-exists-p file-cand))
173                       (not file-entry-p))
174               collect file-cand into ls
175               else
176               ;; Avoid adding entry here.
177               unless file-entry-p collect i into ls
178               finally return
179               (if (and exp-entry
180                        (file-directory-p exp-entry)
181                        ;; If the car of completion list is
182                        ;; an executable, probably we are in
183                        ;; command completion, so don't add a
184                        ;; possible file related entry here.
185                        (and ls (not (executable-find (car ls))))
186                        ;; Don't add entry if already in prompt.
187                        (not (file-equal-p exp-entry pcomplete-stub)))
188                   (append (list exp-entry)
189                           ;; Entry should not be here now but double check.
190                           (remove entry ls))
191                 ls))))))
192
193 ;;; Eshell history.
194 ;;
195 ;;
196 (defclass helm-eshell-history-source (helm-source-sync)
197   ((init :initform
198          (lambda ()
199            ;; Same comment as in `helm-source-esh'.
200            (remove-hook 'minibuffer-setup-hook 'eshell-mode)))
201    (candidates
202     :initform
203     (lambda ()
204       (with-helm-current-buffer
205         (cl-loop for c from 0 to (ring-length eshell-history-ring)
206                  collect (eshell-get-history c)))))
207    (nomark :initform t)
208    (multiline :initform t)
209    (keymap :initform helm-eshell-history-map)
210    (candidate-number-limit :initform 9999)
211    (action :initform (lambda (candidate)
212                        (eshell-kill-input)
213                        (insert candidate))))
214   "Helm class to define source for Eshell history.")
215
216
217 ;;;###autoload
218 (defun helm-esh-pcomplete ()
219   "Preconfigured helm to provide helm completion in eshell."
220   (interactive)
221   (let* ((helm-quit-if-no-candidate t)
222          (helm-execute-action-at-once-if-one t)
223          (end (point-marker))
224          (beg (save-excursion (eshell-bol) (point)))
225          (args (catch 'eshell-incomplete
226                  (eshell-parse-arguments beg end)))
227          (target
228           (or (and (looking-back " " (1- (point))) " ")
229               (buffer-substring-no-properties
230                (save-excursion
231                  (eshell-backward-argument 1) (point))
232                end)))
233          (users-comp (string= target "~"))
234          (first (car args)) ; Maybe lisp delimiter "(".
235          last ; Will be the last but parsed by pcomplete.
236          del-space
237          del-dot)
238     (setq helm-ec-target (or target " ")
239           end (point)
240           ;; Reset beg for `with-helm-show-completion'.
241           beg (or (and target (not (string= target " "))
242                        (- end (length target)))
243                   ;; Nothing at point.
244                   (progn (insert " ") (setq del-space t) (point))))
245     (when (string-match "\\`[~.]*.*/[.]\\'" target)
246       ;; Fix completion on
247       ;; "~/.", "~/[...]/.", and "../."
248       (delete-char -1) (setq del-dot t)
249       (setq helm-ec-target (substring helm-ec-target 0 (1- (length helm-ec-target)))))
250     (cond ((eq first ?\()
251            (helm-lisp-completion-or-file-name-at-point))
252           ;; In eshell `pcomplete-parse-arguments' is called
253           ;; with `pcomplete-parse-arguments-function'
254           ;; locally bound to `eshell-complete-parse-arguments'
255           ;; which is calling `lisp-complete-symbol',
256           ;; calling it before would popup the
257           ;; *completions* buffer.
258           (t (setq last (replace-regexp-in-string
259                          "\\`\\*" ""
260                          (car (last (ignore-errors
261                                       (pcomplete-parse-arguments))))))
262              ;; Set helm-eshell--quit-flag to non-nil only on
263              ;; quit, this tells to not add final suffix when quitting
264              ;; helm.
265              (add-hook 'helm-quit-hook 'helm-eshell--quit-hook-fn)
266              (with-helm-show-completion beg end
267                (unwind-protect
268                    (or (helm :sources (helm-make-source "Eshell completions" 'helm-esh-source
269                                         :fuzzy-match helm-eshell-fuzzy-match)
270                              :buffer "*helm pcomplete*"
271                              :keymap helm-esh-completion-map
272                              :resume 'noresume
273                              :input (if (and (stringp last)
274                                              (not (string= last ""))
275                                              (not users-comp)
276                                              ;; Fix completion on
277                                              ;; "../" see #1832.
278                                              (or (file-exists-p last)
279                                                  (helm-aand
280                                                   (file-name-directory last)
281                                                   (file-directory-p it))))
282                                         (if (and (file-directory-p last)
283                                                  (string-match "\\`[~.]*.*/[.]\\'" target))
284                                             ;; Fix completion on
285                                             ;; "~/.", "~/[...]/.", and "../."
286                                             (expand-file-name
287                                              (concat (helm-basedir (file-name-as-directory last))
288                                                      (regexp-quote (helm-basename target))))
289                                           (expand-file-name last))
290                                       ;; Don't add "~" to input to
291                                       ;; provide completion on all
292                                       ;; users instead of only on
293                                       ;; current $HOME (#1832).
294                                       (unless users-comp last)))
295                        ;; Delete removed dot on quit
296                        (and del-dot (prog1 t (insert ".")))
297                        ;; A space is needed to have completion, remove
298                        ;; it when nothing found.
299                        (and del-space (looking-back "\\s-" (1- (point)))
300                             (delete-char -1))
301                        (if (and (null helm-eshell--quit-flag)
302                                 (and (stringp last) (file-directory-p last))
303                                 (looking-back "\\([.]\\{1,2\\}\\|[^/]\\)\\'"
304                                               (1- (point))))
305                            (prog1 t (insert "/"))
306                          ;; We need another flag for space here, but
307                          ;; global to pass it to `helm-quit-hook', this
308                          ;; space is added when point is just after
309                          ;; previous completion and there is no
310                          ;; more completion, see issue #1832.
311                          (unless (or helm-eshell--quit-flag
312                                      (looking-back "/\\'" (1- (point))))
313                            (prog1 t (insert " ")))
314                          (when (and helm-eshell--quit-flag
315                                     (string-match-p "[.]\\{2\\}\\'" last))
316                            (insert "/"))))
317                  (remove-hook 'helm-quit-hook 'helm-eshell--quit-hook-fn)
318                  (setq helm-eshell--quit-flag nil)))))))
319
320 (defun helm-eshell--quit-hook-fn ()
321   (setq helm-eshell--quit-flag t))
322
323 ;;;###autoload
324 (defun helm-eshell-history ()
325   "Preconfigured helm for eshell history."
326   (interactive)
327   (let* ((end   (point))
328          (beg   (save-excursion (eshell-bol) (point)))
329          (input (buffer-substring beg end))
330          flag-empty)
331     (when (eq beg end)
332       (insert " ")
333       (setq flag-empty t)
334       (setq end (point)))
335     (unwind-protect
336          (with-helm-show-completion beg end
337            (helm :sources (helm-make-source "Eshell history"
338                               'helm-eshell-history-source)
339                  :buffer "*helm eshell history*"
340                  :resume 'noresume
341                  :input input))
342       (when (and flag-empty
343                  (looking-back " " (1- (point))))
344         (delete-char -1)))))
345
346
347 ;;; Eshell prompts
348 ;;
349 (defface helm-eshell-prompts-promptidx
350   '((t (:foreground "cyan")))
351   "Face used to highlight Eshell prompt index."
352   :group 'helm-eshell-faces)
353
354 (defface helm-eshell-prompts-buffer-name
355   '((t (:foreground "green")))
356   "Face used to highlight Eshell buffer name."
357   :group 'helm-eshell-faces)
358
359 (defcustom helm-eshell-prompts-promptidx-p t
360   "Show prompt number."
361   :group 'helm-eshell
362   :type 'boolean)
363
364 (defvar helm-eshell-prompts-keymap
365   (let ((map (make-sparse-keymap)))
366     (set-keymap-parent map helm-map)
367     (define-key map (kbd "C-c o")   'helm-eshell-prompts-other-window)
368     (define-key map (kbd "C-c C-o") 'helm-eshell-prompts-other-frame)
369     map)
370   "Keymap for `helm-eshell-prompt-all'.")
371
372 (defvar eshell-prompt-regexp)
373 (defvar eshell-highlight-prompt)
374
375 (defun helm-eshell-prompts-list (&optional buffer)
376   "List the prompts in Eshell BUFFER.
377
378 Return a list of (\"prompt\" (point) (buffer-name) prompt-index))
379 e.g. (\"ls\" 162 \"*eshell*\" 3).
380 If BUFFER is nil, use current buffer."
381   (with-current-buffer (or buffer (current-buffer))
382     (when (eq major-mode 'eshell-mode)
383       (save-excursion
384         (goto-char (point-min))
385         (let (result (count 1))
386           (helm-awhile (re-search-forward eshell-prompt-regexp nil t)
387             (when (or (and eshell-highlight-prompt
388                            (get-text-property (match-beginning 0) 'read-only))
389                       (null eshell-highlight-prompt))
390               (push (list (buffer-substring-no-properties
391                            it (point-at-eol))
392                           it (buffer-name) count)
393                     result)
394               (setq count (1+ count))))
395           (nreverse result))))))
396
397 (defun helm-eshell-prompts-list-all ()
398   "List the prompts of all Eshell buffers.
399 See `helm-eshell-prompts-list'."
400   (cl-loop for b in (buffer-list)
401            append (helm-eshell-prompts-list b)))
402
403 (defun helm-eshell-prompts-transformer (candidates &optional all)
404   ;; ("ls" 162 "*eshell*" 3) => ("*eshell*:3:ls" . ("ls" 162 "*eshell*" 3))
405   (cl-loop for (prt pos buf id) in candidates
406            collect `(,(concat
407                        (when all
408                          (concat (propertize
409                                   buf
410                                   'face 'helm-eshell-prompts-buffer-name)
411                                  ":"))
412                        (when helm-eshell-prompts-promptidx-p
413                          (concat (propertize
414                                   (number-to-string id)
415                                   'face 'helm-eshell-prompts-promptidx)
416                                  ":"))
417                        prt)
418                       . ,(list prt pos buf id))))
419
420 (defun helm-eshell-prompts-all-transformer (candidates)
421   (helm-eshell-prompts-transformer candidates t))
422
423 (cl-defun helm-eshell-prompts-goto (candidate &optional (action 'switch-to-buffer))
424   ;; Candidate format: ("ls" 162 "*eshell*" 3)
425   (let ((buf (nth 2 candidate)))
426     (unless (and (string= (buffer-name) buf)
427                  (eq action 'switch-to-buffer))
428       (funcall action buf))
429     (goto-char (nth 1 candidate))
430     (recenter)))
431
432 (defun helm-eshell-prompts-goto-other-window (candidate)
433   (helm-eshell-prompts-goto candidate 'switch-to-buffer-other-window))
434
435 (defun helm-eshell-prompts-goto-other-frame (candidate)
436   (helm-eshell-prompts-goto candidate 'switch-to-buffer-other-frame))
437
438 (defun helm-eshell-prompts-other-window ()
439   (interactive)
440   (with-helm-alive-p
441     (helm-exit-and-execute-action 'helm-eshell-prompts-goto-other-window)))
442 (put 'helm-eshell-prompts-other-window 'helm-only t)
443
444 (defun helm-eshell-prompts-other-frame ()
445   (interactive)
446   (with-helm-alive-p
447     (helm-exit-and-execute-action 'helm-eshell-prompts-goto-other-frame)))
448 (put 'helm-eshell-prompts-other-frame 'helm-only t)
449
450 ;;;###autoload
451 (defun helm-eshell-prompts ()
452   "Pre-configured `helm' to browse the prompts of the current Eshell."
453   (interactive)
454   (if (eq major-mode 'eshell-mode)
455       (helm :sources
456             (helm-build-sync-source "Eshell prompts"
457               :candidates (helm-eshell-prompts-list)
458               :candidate-transformer 'helm-eshell-prompts-transformer
459               :action '(("Go to prompt" . helm-eshell-prompts-goto)))
460             :buffer "*helm Eshell prompts*")
461     (message "Current buffer is not an Eshell buffer")))
462
463 ;;;###autoload
464 (defun helm-eshell-prompts-all ()
465   "Pre-configured `helm' to browse the prompts of all Eshell sessions."
466   (interactive)
467   (helm :sources
468         (helm-build-sync-source "All Eshell prompts"
469           :candidates (helm-eshell-prompts-list-all)
470           :candidate-transformer 'helm-eshell-prompts-all-transformer
471           :action '(("Go to prompt" . helm-eshell-prompts-goto)
472                     ("Go to prompt in other window `C-c o`" .
473                      helm-eshell-prompts-goto-other-window)
474                     ("Go to prompt in other frame `C-c C-o`" .
475                      helm-eshell-prompts-goto-other-frame))
476           :keymap helm-eshell-prompts-keymap)
477         :buffer "*helm Eshell all prompts*"))
478
479 (provide 'helm-eshell)
480
481 ;; Local Variables:
482 ;; byte-compile-warnings: (not obsolete)
483 ;; coding: utf-8
484 ;; indent-tabs-mode: nil
485 ;; End:
486
487 ;;; helm-eshell ends here