diff --git a/modules/emacs/eshell/autoload/commands.el b/modules/emacs/eshell/autoload/commands.el new file mode 100644 index 000000000..021764857 --- /dev/null +++ b/modules/emacs/eshell/autoload/commands.el @@ -0,0 +1,14 @@ +;;; emacs/eshell/autoload/commands.el -*- lexical-binding: t; -*- + +;;;###autoload +(defun eshell/cd-to-project () + "Change to the project root of the current directory." + (let* ((default-directory (eshell/pwd)) + (project-root (doom-project-root 'nocache))) + (eshell/cd project-root))) + +;;;###autoload +(defun eshell/quit-and-close (&rest _) + "Quit the current eshell buffer and close the window it's in." + (setq-local +eshell-kill-window-on-exit t) + (throw 'eshell-terminal t)) diff --git a/modules/emacs/eshell/autoload/eshell.el b/modules/emacs/eshell/autoload/eshell.el index 0c85dca54..d6b9fbc8e 100644 --- a/modules/emacs/eshell/autoload/eshell.el +++ b/modules/emacs/eshell/autoload/eshell.el @@ -1,29 +1,17 @@ ;;; emacs/eshell/autoload/eshell.el -*- lexical-binding: t; -*- -;;;###autoload -(defface +eshell-prompt-pwd '((t :inherit font-lock-constant-face)) - "TODO" - :group 'eshell) - -;;;###autoload -(defface +eshell-prompt-git-branch '((t :inherit font-lock-builtin-face)) - "TODO" - :group 'eshell) - +(defvar eshell-buffer-name "*doom eshell*") (defvar +eshell-buffers (make-ring 25) "List of open eshell buffers.") -(defvar +eshell-buffer-name "*doom eshell*" - "The name to use for custom eshell buffers. This only affects `+eshell/open', -`+eshell/open-popup' and `+eshell/open-workspace'.") -(defvar +eshell-last-buffer nil - "TODO") +(defvar +eshell--last-buffer nil) +(defvar-local +eshell--wconf nil) ;; -;; Library +;; Helpers ;; (defun +eshell--add-buffer (buf) @@ -34,37 +22,55 @@ (ring-remove +eshell-buffers idx) t)) -(defun +eshell--current-git-branch () - (let ((branch (car (cl-loop for match in (split-string (shell-command-to-string "git branch") "\n") - if (string-match-p "^\*" match) - collect match)))) - (if (not (eq branch nil)) - (format " [%s]" (substring branch 2)) - ""))) +(defun +eshell--bury-buffer () + (unless (switch-to-prev-buffer nil 'bury) + (switch-to-buffer (doom-fallback-buffer)) + (when +eshell-enable-new-shell-on-split + (+eshell/open t)))) -(defun +eshell--buffer (&optional new-p) - (or (unless new-p - (cl-loop for buf in (ring-elements +eshell-buffers) - if (and (buffer-live-p buf) - (not (get-buffer-window buf))) - return buf)) - (generate-new-buffer +eshell-buffer-name))) - -(defun +eshell--set-window (window &optional flag) - (when window +(defun +eshell--setup-window (window &optional flag) + (when (window-live-p window) (set-window-parameter window 'no-other-window flag) (set-window-parameter window 'visible flag))) +(defun +eshell--unused-buffer (&optional new-p) + (or (unless new-p + (cl-loop for buf in (ring-elements +eshell-buffers) + if (and (buffer-live-p buf) + (not (get-buffer-window buf t))) + return buf)) + (generate-new-buffer eshell-buffer-name))) + ;;;###autoload -(defun +eshell-prompt () - "Generate the prompt string for eshell. Use for `eshell-prompt-function'." - (concat (if (bobp) "" "\n") - (propertize (abbreviate-file-name (shrink-path-file (eshell/pwd))) - 'face '+eshell-prompt-pwd) - (propertize (+eshell--current-git-branch) - 'face '+eshell-prompt-git-branch) - (propertize " λ" 'face (if (zerop eshell-last-command-status) 'success 'error)) - " ")) +(defun +eshell-last-buffer (&optional noerror) + "Return the last opened eshell buffer." + (let ((buffer (cl-find-if #'buffer-live-p (ring-elements +eshell-buffers)))) + (cond ((buffer-live-p buffer) buffer) + (noerror nil) + ((user-error "No live eshell buffers remaining"))))) + +;;;###autoload +(defun +eshell-buffers () + "TODO" + (ring-elements +eshell-buffers)) + +;;;###autoload +(defun +eshell-run-command (command &optional buffer) + "TODO" + (let ((buffer + (or buffer + (if (eq major-mode 'eshell-mode) + (current-buffer) + (cl-find-if #'buffer-live-p (ring-elements +eshell-buffers)))))) + (unless buffer + (user-error "No living eshell buffers available")) + (unless (buffer-live-p buffer) + (user-error "Cannot operate on a dead buffer")) + (with-current-buffer buffer + (goto-char eshell-last-output-end) + (goto-char (line-end-position)) + (insert command) + (eshell-send-input nil t)))) ;; @@ -75,57 +81,64 @@ (defun +eshell/open (arg &optional command) "Open eshell in the current buffer." (interactive "P") + (when (eq major-mode 'eshell-mode) + (user-error "Already in an eshell buffer")) (let* ((default-directory (if arg default-directory (doom-project-root))) - (buf (+eshell--buffer (eq major-mode 'eshell-mode)))) - (with-current-buffer buf - (unless (eq major-mode 'eshell-mode) (eshell-mode))) - (switch-to-buffer buf) - (+eshell--set-window (get-buffer-window buf) t) - (when command - (+eshell-run-command command)))) + (buf (+eshell--unused-buffer))) + (with-current-buffer (switch-to-buffer buf) + (eshell-mode) + (if command (+eshell-run-command command buf))) + buf)) ;;;###autoload (defun +eshell/open-popup (arg &optional command) "Open eshell in a popup window." (interactive "P") (let* ((default-directory (if arg default-directory (doom-project-root))) - (buf (+eshell--buffer))) - (with-current-buffer buf - (unless (eq major-mode 'eshell-mode) (eshell-mode))) - (pop-to-buffer buf) - (+eshell--set-window (get-buffer-window buf) t) - (when command - (+eshell-run-command command)))) + (buf (+eshell--unused-buffer))) + (with-current-buffer (pop-to-buffer buf) + (eshell-mode) + (if command (+eshell-run-command command buf))) + buf)) ;;;###autoload -(defun +eshell/open-workspace (arg &optional command) +(defun +eshell/open-fullscreen (arg &optional command) "Open eshell in a separate workspace. Requires the (:feature workspaces) module to be loaded." (interactive "P") (let ((default-directory (if arg default-directory (doom-project-root)))) - (unless (featurep! :feature workspaces) - (user-error ":feature workspaces is required, but disabled")) - (unless (+workspace-get "eshell" t) - (+workspace/new "eshell")) - (if-let* ((buf (cl-find-if (lambda (buf) (eq 'eshell-mode (buffer-local-value 'major-mode buf))) - (doom-visible-windows) - :key #'window-buffer))) - (select-window (get-buffer-window buf)) - (+eshell/open arg)) - (when command - (+eshell-run-command command)) - (+eshell--set-window (selected-window) t) - (doom/workspace-display))) + (setq +eshell--wconf (current-window-configuration)) + (delete-other-windows) + (with-current-buffer (+eshell/open arg command) + (setq-local +eshell--wconf (current-window-configuration))))) -(defun +eshell-run-command (command) - (unless (cl-remove-if-not #'buffer-live-p +eshell-buffers) - (user-error "No living eshell buffers available")) - (with-current-buffer (car +eshell-buffers) - (goto-char eshell-last-output-end) - (when (bound-and-true-p evil-mode) - (call-interactively #'evil-append-line)) - (insert command) - (eshell-send-input nil t))) + +;; +;; Keybinds +;; + +;;;###autoload +(defun +eshell/search-history () + "Search the eshell command history with helm, ivy or `eshell-list-history'." + (interactive) + (cond ((featurep! :completion ivy) + (require 'em-hist) + (let* ((ivy-completion-beg (eshell-bol)) + (ivy-completion-end (point-at-eol)) + (input (buffer-substring-no-properties + ivy-completion-beg + ivy-completion-end))) + ;; Better than `counsel-esh-history' because that doesn't + ;; pre-populate the initial input or selection. + (ivy-read "Command: " + (delete-dups + (when (> (ring-size eshell-history-ring) 0) + (ring-elements eshell-history-ring))) + :initial-input input + :action #'ivy-completion-in-region-action))) + ((featurep! :completion helm) + (helm-eshell-history)) + ((eshell-list-history)))) ;;;###autoload (defun +eshell/pcomplete () @@ -135,41 +148,6 @@ bottom. This ties pcomplete into ivy or helm, if they are enabled." (require 'pcomplete) (pcomplete-std-complete)) - -;; -;; Hooks -;; - -;;;###autoload -(defun +eshell|init () - "Keep track of eshell buffers." - (let ((buf (current-buffer))) - (dolist (buf (ring-elements +eshell-buffers)) - (unless (buffer-live-p buf) - (+eshell--remove-buffer buf))) - (+eshell--add-buffer buf) - (setq +eshell-last-buffer buf))) - -;;;###autoload -(defun +eshell|cleanup () - "Close window (or workspace) on quit." - (let ((buf (current-buffer))) - (when (+eshell--remove-buffer buf) - (+eshell--set-window (get-buffer-window buf) nil) - (cond ((and (featurep! :feature workspaces) - (string= "eshell" (+workspace-current-name))) - (+workspace/delete "eshell")) - ((one-window-p) - (unless (doom-real-buffer-p (progn (previous-buffer) (current-buffer))) - (switch-to-buffer (doom-fallback-buffer)))) - ((and (fboundp '+popup-window-p) (+popup-window-p)) - (delete-window)))))) - - -;; -;; Keybinds -;; - ;;;###autoload (defun +eshell/quit-or-delete-char (arg) "Delete a character (ahead of the cursor) or quit eshell if there's nothing to @@ -179,10 +157,6 @@ delete." (eshell-life-is-too-much) (delete-char arg))) -(defsubst +eshell--bury-buffer () - (unless (switch-to-prev-buffer nil 'bury) - (switch-to-buffer (doom-fallback-buffer)))) - ;;;###autoload (defun +eshell/split-below () "Create a new eshell window below the current one." @@ -197,13 +171,8 @@ delete." (select-window (split-window-horizontally)) (+eshell--bury-buffer)) -;; `make-ring' -;; `ring-ref' -;; `ring-empty-p' -;; `ring-remove' - ;;;###autoload -(defun +eshell/next () +(defun +eshell/switch-to-next () "Switch to the next eshell buffer." (interactive) (when (ring-empty-p +eshell-buffers) @@ -211,7 +180,7 @@ delete." (switch-to-buffer (ring-next +eshell-buffers (current-buffer)))) ;;;###autoload -(defun +eshell/previous () +(defun +eshell/switch-to-previous () "Switch to the previous eshell buffer." (interactive) (when (ring-empty-p +eshell-buffers) @@ -219,30 +188,72 @@ delete." (switch-to-buffer (ring-previous +eshell-buffers (current-buffer)))) ;;;###autoload -(defun +eshell/open-last () +(defun +eshell/switch-to-last () "Switch to the last eshell buffer that was open (and is still alive)." (interactive) - (unless (buffer-live-p +eshell-last-buffer) - (setq +eshell-last-buffer nil) + (unless (buffer-live-p +eshell--last-buffer) + (setq +eshell--last-buffer nil) (user-error "No last eshell buffer to jump to")) - (switch-to-buffer +eshell-last-buffer)) + (switch-to-buffer +eshell--last-buffer)) ;;;###autoload -(defun +eshell/switch (buffer) +(defun +eshell/switch-to (buffer) "Interactively switch to another eshell buffer." (interactive (let ((buffers (doom-buffers-in-mode 'eshell-mode (delq (current-buffer) (ring-elements +eshell-buffers))))) (if (not buffers) (user-error "No eshell buffers are available") - (list (completing-read - "Eshell buffers" - (mapcar #'buffer-name buffers) - #'get-buffer - 'require-match - nil nil - (when (eq major-mode 'eshell-mode) - (buffer-name (current-buffer)))))))) + (list + (completing-read "Eshell buffers" + (mapcar #'buffer-name buffers) + #'get-buffer + 'require-match + nil nil + (when (eq major-mode 'eshell-mode) + (buffer-name (current-buffer)))))))) (if-let* ((window (get-buffer-window buffer))) (select-window window) (switch-to-buffer buffer))) + +;;;###autoload +(defun +eshell/kill-and-close () + "Kill the current eshell buffer and close its window." + (interactive) + (unless (eq major-mode 'eshell-mode) + (user-error "Not in an eshell buffer")) + (let ((+eshell-kill-window-on-exit t)) + (kill-this-buffer))) + + +;; +;; Hooks +;; + +;;;###autoload +(defun +eshell|init () + "Initialize and track this eshell buffer in `+eshell-buffers'." + (let ((buf (current-buffer))) + (dolist (buf (ring-elements +eshell-buffers)) + (unless (buffer-live-p buf) + (+eshell--remove-buffer buf))) + (+eshell--setup-window (get-buffer-window buf)) + (+eshell--add-buffer buf) + (setq +eshell--last-buffer buf))) + +;;;###autoload +(defun +eshell|cleanup () + "Close window (or workspace) on quit." + (let ((buf (current-buffer))) + (when (+eshell--remove-buffer buf) + (+eshell--setup-window (get-buffer-window buf) nil) + (cond (+eshell--wconf + (set-window-configuration +eshell--wconf)) + ((one-window-p) + (let ((prev (save-window-excursion (previous-buffer)))) + (unless (and prev (doom-real-buffer-p prev)) + (switch-to-buffer (doom-fallback-buffer))))) + ((or (and (fboundp '+popup-window-p) (+popup-window-p)) + +eshell-kill-window-on-exit) + (delete-window)))))) + diff --git a/modules/emacs/eshell/autoload/evil.el b/modules/emacs/eshell/autoload/evil.el index f6bc1f3d9..a6fd3c911 100644 --- a/modules/emacs/eshell/autoload/evil.el +++ b/modules/emacs/eshell/autoload/evil.el @@ -1,6 +1,27 @@ ;;; emacs/eshell/autoload/evil.el -*- lexical-binding: t; -*- ;;;###if (featurep! :feature evil) +;;;###autoload +(defun +eshell|init-evil () + "Replace `evil-collection-eshell-next-prompt-on-insert' with +`+eshell|goto-prompt-on-insert', which ensures the point is on the prompt when +changing to insert mode." + (dolist (hook '(evil-replace-state-entry-hook evil-insert-state-entry-hook)) + (remove-hook hook 'evil-collection-eshell-next-prompt-on-insert t) + (add-hook hook '+eshell|goto-prompt-on-insert nil t))) + +;;;###autoload (autoload '+eshell:run "emacs/eshell/autoload/evil" nil t) +(evil-define-command +eshell:run (command bang) + "TODO" + (interactive "") + (let ((buffer (+eshell-last-buffer)) + (command (+evil*resolve-vim-path command))) + (cond (buffer + (select-window (get-buffer-window buffer)) + (+eshell-run-command command buffer)) + (bang (+eshell/open nil command)) + ((+eshell/open-popup nil command))))) + ;;;###autoload (defun +eshell|goto-prompt-on-insert () "Move cursor to the prompt when switching to insert mode (if point isn't @@ -19,14 +40,6 @@ already there)." (goto-char (point-max)) (evil-append 1)) -;;;###autoload (autoload '+eshell:run "emacs/eshell/autoload/evil" nil t) -(evil-define-command +eshell:run (command bang) - "TODO" - (interactive "") - (if bang - (+eshell/open nil command) - (+eshell/open-popup nil command))) - ;;;###autoload (autoload '+eshell/evil-change "emacs/eshell/autoload/evil" nil t) (evil-define-operator +eshell/evil-change (beg end type register yank-handler delete-func) "Like `evil-change' but will not delete/copy the prompt." diff --git a/modules/emacs/eshell/autoload/prompts.el b/modules/emacs/eshell/autoload/prompts.el new file mode 100644 index 000000000..8509486ed --- /dev/null +++ b/modules/emacs/eshell/autoload/prompts.el @@ -0,0 +1,31 @@ +;;; emacs/eshell/autoload/prompts.el -*- lexical-binding: t; -*- + +;;;###autoload +(defface +eshell-prompt-pwd '((t :inherit font-lock-constant-face)) + "TODO" + :group 'eshell) + +;;;###autoload +(defface +eshell-prompt-git-branch '((t :inherit font-lock-builtin-face)) + "TODO" + :group 'eshell) + + +(defun +eshell--current-git-branch () + (let ((branch (car (cl-loop for match in (split-string (shell-command-to-string "git branch") "\n") + if (string-match-p "^\*" match) + collect match)))) + (if (not (eq branch nil)) + (format " [%s]" (substring branch 2)) + ""))) + +;;;###autoload +(defun +eshell-default-prompt () + "Generate the prompt string for eshell. Use for `eshell-prompt-function'." + (concat (if (bobp) "" "\n") + (propertize (abbreviate-file-name (shrink-path-file (eshell/pwd))) + 'face '+eshell-prompt-pwd) + (propertize (+eshell--current-git-branch) + 'face '+eshell-prompt-git-branch) + (propertize " λ" 'face (if (zerop eshell-last-command-status) 'success 'error)) + " ")) diff --git a/modules/emacs/eshell/autoload/settings.el b/modules/emacs/eshell/autoload/settings.el new file mode 100644 index 000000000..1bec69ce6 --- /dev/null +++ b/modules/emacs/eshell/autoload/settings.el @@ -0,0 +1,16 @@ +;;; emacs/eshell/autoload/settings.el -*- lexical-binding: t; -*- + +;;;###autodef +(defun set-eshell-alias! (&rest aliases) + "Define aliases for eshell." + (or (cl-evenp (length aliases)) + (signal 'wrong-number-of-arguments (list 'even (length aliases)))) + (after! eshell + (while aliases + (map-put +eshell-aliases (pop aliases) (list (pop aliases)))) + (when (boundp 'eshell-command-aliases-list) + (if +eshell--default-aliases + (setq eshell-command-aliases-list + (append +eshell--default-aliases + +eshell-aliases)) + (setq eshell-command-aliases-list +eshell-aliases))))) diff --git a/modules/emacs/eshell/config.el b/modules/emacs/eshell/config.el index cf6457222..092f4b894 100644 --- a/modules/emacs/eshell/config.el +++ b/modules/emacs/eshell/config.el @@ -6,11 +6,35 @@ ;; + `+eshell/open-workspace': open in separate tab (requires :feature ;; workspaces) -(defvar eshell-directory-name - (let ((dir (expand-file-name "eshell" doom-private-dir))) - (if (file-directory-p dir) - dir - "~/.eshell"))) +(defvar eshell-directory-name (concat doom-etc-dir "eshell")) + +(defvar eshell-aliases-file + (expand-file-name "eshell_aliases" doom-private-dir) + "The path to your eshell aliases file, where you may declare alises. This is +here as an alternative to `set-eshell-alias!'.") + +;; +(defvar +eshell-enable-new-shell-on-split t + "If non-nil, spawn a new eshell session after splitting from an eshell +buffer.") + +(defvar +eshell-kill-window-on-exit nil + "If non-nil, eshell will close windows along with its eshell buffers.") + +(defvar +eshell-aliases + '(("q" "exit") ; built-in + ("z" "cd =$1") ; built-in + ("bd" "eshell-up $1") ; `eshell-up' + ("rg" "rg --color=always") + ("ag" "ag --color=always")) + "An alist of default eshell aliases, meant to emulate useful shell utilities, +like fasd and bd. Note that you may overwrite these in your +`eshell-aliases-file'. This is here to provide an alternative, elisp-centric way +to define your aliases. + +You should use `det-eshell-alias!' to change this.") + +(defvar +eshell--default-aliases nil) ;; @@ -31,7 +55,7 @@ eshell-hist-ignoredups t ;; em-prompt eshell-prompt-regexp "^.* λ " - eshell-prompt-function #'+eshell-prompt + eshell-prompt-function #'+eshell-default-prompt ;; em-glob eshell-glob-case-insensitive t eshell-error-if-no-glob t) @@ -39,6 +63,10 @@ ;; Consider eshell buffers real (add-hook 'eshell-mode-hook #'doom|mark-buffer-as-real) + ;; Keep track of open eshell buffers + (add-hook 'eshell-mode-hook #'+eshell|init) + (add-hook 'eshell-exit-hook #'+eshell|cleanup) + ;; UI enhancements (defun +eshell|replace-fringes-with-margins () "Remove eshell's fringes and give it a margin of 1." @@ -47,28 +75,25 @@ (add-hook 'eshell-mode-hook #'+eshell|replace-fringes-with-margins) (add-hook 'eshell-mode-hook #'hide-mode-line-mode) - ;; Keep track of open eshell buffers - (add-hook 'eshell-mode-hook #'+eshell|init) - (add-hook 'eshell-exit-hook #'+eshell|cleanup) - - (after! em-alias - ;; Emulates popular shell utilities - (map-put eshell-command-aliases-list "z" '("cd =$1")) - (map-put eshell-command-aliases-list "bd" '("eshell-up $1"))) + ;; Don't auto-write our aliases! Let us manage our own `eshell-aliases-file' + ;; or configure `+eshell-aliases' via elisp. + (advice-add #'eshell-write-aliases-list :override #'ignore) + ;; Visual commands require a proper terminal. Eshell can't handle that, so + ;; it delegates these commands to a term buffer. (after! em-term - ;; Visual commands require a proper terminal. Eshell can't handle that, so - ;; it delegates these commands to a term buffer. (dolist (cmd '("tmux" "htop" "bash" "zsh" "fish" "vim" "nvim" "ncmpcpp")) - (cl-pushnew cmd eshell-visual-commands))) + (add-to-list 'eshell-visual-commands cmd))) - (defun +eshell|init-evil () - "Replace `evil-collection-eshell-next-prompt-on-insert' with -`+eshell|goto-prompt-on-insert'." - (dolist (hook '(evil-replace-state-entry-hook evil-insert-state-entry-hook)) - (remove-hook hook 'evil-collection-eshell-next-prompt-on-insert t) - (add-hook hook '+eshell|goto-prompt-on-insert nil t))) - (add-hook 'eshell-mode-hook #'+eshell|init-evil) + (defun +eshell|init-aliases () + (setq +eshell--default-aliases eshell-command-aliases-list + eshell-command-aliases-list + (append eshell-command-aliases-list + +eshell-aliases))) + (add-hook 'eshell-alias-load-hook #'+eshell|init-aliases) + + (when (featurep! :feature evil +everywhere) + (add-hook 'eshell-mode-hook #'+eshell|init-evil)) (defun +eshell|init-keymap () "Setup eshell keybindings. This must be done in a hook because eshell-mode @@ -90,6 +115,10 @@ redefines its keys every time `eshell-mode' is enabled." "\C-p" #'eshell-previous-input "\C-n" #'eshell-next-input)) (define-key! eshell-mode-map + (kbd "C-s") #'+eshell/search-history + (kbd "C-c s") #'+eshell/split-below + (kbd "C-c v") #'+eshell/split-right + (kbd "C-c x") #'+eshell/kill-and-close [remap split-window-below] #'+eshell/split-below [remap split-window-right] #'+eshell/split-right [remap doom/backward-to-bol-or-indent] #'eshell-bol