Klimi's new dotfiles with stow.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

498 lines
20 KiB

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