From fc14c41eddf654005aa190541ed4a5cc415a02a0 Mon Sep 17 00:00:00 2001 From: Madeleine Sydney Date: Sun, 2 Feb 2025 14:51:04 -0700 Subject: [PATCH] feat: Progress towards comfortable Lisp editing --- README.org | 12 +- users/crumb/programs/emacs/init.el | 5 +- users/crumb/programs/emacs/lib/clj-lib.el | 15 + .../crumb/programs/emacs/lib/syd-lisp-lib.el | 485 ++++++++++++++++++ users/crumb/programs/emacs/lib/syd-prelude.el | 8 + users/crumb/programs/emacs/lib/syd-text.el | 20 +- .../emacs/modules/lang/syd-lang-emacs-lisp.el | 42 +- .../programs/emacs/modules/syd-editing.el | 11 + .../programs/emacs/modules/syd-eshell.el | 10 +- .../crumb/programs/emacs/modules/syd-evil.el | 47 +- .../programs/emacs/modules/syd-keybinds.el | 7 - 11 files changed, 623 insertions(+), 39 deletions(-) create mode 100644 users/crumb/programs/emacs/lib/clj-lib.el create mode 100644 users/crumb/programs/emacs/lib/syd-lisp-lib.el create mode 100644 users/crumb/programs/emacs/modules/syd-editing.el diff --git a/README.org b/README.org index a3decd9..b3394e0 100755 --- a/README.org +++ b/README.org @@ -179,6 +179,8 @@ sydnix-cli is a command-line utility written in Clojure wrapping various sydnix- ** Emacs from scratch +*** TODO Block escaping with ~jk~ whilst recording a macro + *** TODO Many editing commands should re-indent after use Particularly in Lisps where indentation is reliable. @@ -263,18 +265,14 @@ Seems old and broken? **** TODO Evil stuff -**** TODO Text objects +**** DONE Text objects **** DONE [[https://github.com/Malabarba/speed-of-thought-lisp][speed-of-thought]] -**** TODO [[https://github.com/Lindydancer/lisp-extra-font-lock][lisp-extra-font-lock]] - -**** TODO rainbow-delimiters +**** DONE rainbow-delimiters **** TODO [[https://github.com/Wilfred/emacs-refactor][emacs-refactor]] -**** TODO [[https://github.com/Malabarba/aggressive-indent-mode][aggressive-indent-mode]] - **** TODO [[https://github.com/riscy/elfmt][elfmt]] **** TODO [[https://github.com/magnars/string-edit.el][string-edit]] @@ -648,3 +646,5 @@ Beloved Faye's Wishsys is an incredibly impressive 3-kloc NixOS config with seve - [[https://github.com/neeasade/emacs.d][neeasade/emacs.d]] — Has an interesting 'module' system. - [[https://github.com/oantolin/emacs-config][oantolin/emacs-config]] — Has some internal packages. - [[https://github.com/noctuid/evil-guide][noctuid/evil-guide]] +- [[https://github.com/drym-org/symex.el][symex.el]] +- [[https://github.com/Fuco1/smartparens][Smartparens]] diff --git a/users/crumb/programs/emacs/init.el b/users/crumb/programs/emacs/init.el index 1e01de1..40403e4 100755 --- a/users/crumb/programs/emacs/init.el +++ b/users/crumb/programs/emacs/init.el @@ -2,8 +2,6 @@ ;; Initialise Straight.el -(+ 1 2) - (load (locate-user-emacs-file "init-straight")) (syd-initialise-straight) @@ -53,8 +51,9 @@ (require 'syd-completion) (require 'syd-custom) (require 'syd-display-startup-time) -(require 'syd-evil) +(require 'syd-editing) (require 'syd-eshell) +(require 'syd-evil) (require 'syd-keybinds) (require 'syd-lang) (require 'syd-org) diff --git a/users/crumb/programs/emacs/lib/clj-lib.el b/users/crumb/programs/emacs/lib/clj-lib.el new file mode 100644 index 0000000..874e8f4 --- /dev/null +++ b/users/crumb/programs/emacs/lib/clj-lib.el @@ -0,0 +1,15 @@ +;;; clj-lib.el -*- lexical-binding: t; -*- + +(require 'dash) + +(defmacro clj-condp (pred expr &rest clauses) + "TODO: Very unfinished." + (declare (indent defun)) + (unless (symbolp pred) + (signal 'wrong-type-argument `(symbolp ,pred))) + (let ((expr* (gensym "expr"))) + `(let ((,expr* ,expr)) + (cond ,@(mapcar (lambda (x) `((,pred ,expr ,(car x)) ,(nth 1 x))) + clauses))))) + +(provide 'clj-lib) diff --git a/users/crumb/programs/emacs/lib/syd-lisp-lib.el b/users/crumb/programs/emacs/lib/syd-lisp-lib.el new file mode 100644 index 0000000..17f2165 --- /dev/null +++ b/users/crumb/programs/emacs/lib/syd-lisp-lib.el @@ -0,0 +1,485 @@ +;;; syd-lisp-lib.el -*- lexical-binding: t; -*- + +(require 'general) +(require 'clj-lib) + +(use-package smartparens + :defer t) + +(use-package evil-surround + :defer t) + +;; Include various lispy symbols as word constituents. +(dolist (c '(?- ?_ ?? ?! ?+ ?* ?/ ?: ?> ?< ?= ?&)) + (modify-syntax-entry c "w" lisp-data-mode-syntax-table)) + +;;;###autoload +(defvar-keymap syd-lisp-mode-map + :doc "Keymap for `syd-lisp-mode'.") + +;;;###autoload +(define-minor-mode syd-lisp-mode + "A minor mode for editing lispy languages." + :keymap syd-lisp-mode-map) + +;;;###autoload +(defun syd-wrap-sexp (char) + "Wrap the sexp at point (using `smartparens') with the pair corresponding to +CHAR (using `evil-surround'). Unlike other `evil-surround' operations, the +point will be preserved and the wrapped region will be re-indented." + (interactive (evil-surround-input-char)) + (sp-get (sp-get-thing) + (save-excursion + (evil-surround-region :beg :end 'inclusive char) + (indent-region :beg :end)))) + +;;;###autoload +(evil-define-motion syd-get-enclosing-sexp () + "Like `sp-get-enclosing-sexp', but with a slightly different meaning of +\"enclosing sexp\" that matches Vim-sexp's" + (or (let ((sexp-at-point (sp-get-sexp))) + (sp-get sexp-at-point + (when (or (and :beg (= (point) :beg)) + (and :end (= (point) (- :end 1)))) + sexp-at-point))) + (let ((sp-enclosing-sexp (sp-get-enclosing-sexp))) + (sp-get sp-enclosing-sexp + (when :beg + sp-enclosing-sexp))))) + +;;;###autoload +(evil-define-motion syd-backward-up-sexp (count) + "Move point to the opening bracket of the enclosing sexp. The precise meaning +of \"enclosing sexp\" differs slightly from that used by Smartparens for the +sake of a more Vim-like feel inspired by vim-sexp." + :type exclusive + (dotimes (_ (or count 1)) + ;; REVIEW: Is there a better way to do this? I'm slightly uncomfortable + ;; calling two different `sp-get-*' functions. + (or (sp-get (sp-get-sexp) + (when (and :end (= (point) (- :end 1))) + (goto-char :beg))) + (sp-get (sp-get-enclosing-sexp) + (when :beg + (goto-char :beg)))))) + +;;;###autoload +(evil-define-motion syd-forward-up-sexp (&optional count) + "Move point to the closing bracket of the enclosing sexp. See +`syd-backward-up-sexp'." + :type exclusive + (dotimes (_ (or count 1)) + (or (sp-get (sp-get-sexp) + (when (and :beg (= (point) :beg)) + (goto-char (- :end 1)))) + (sp-get (sp-get-enclosing-sexp) + (when :end + (if (= (point) (- :end 1)) + (sp-get (save-excursion (forward-char) + (sp-get-enclosing-sexp)) + (when :end + (goto-char (- :end 1)))) + (goto-char (- :end 1)))))))) + +;;;###autoload +(defun syd-get-top-level-sexp () + "Get the top-level sexp enclosing point. Destructure with `sp-get'.'" + ;; The end position returned by `bounds-of-thing-at-point' includes an + ;; unpredictable amount of trailing whitespace, so we discard it and compute + ;; our own figure. + (let ((original-point (point))) + (-when-let ((beg . _) (bounds-of-thing-at-point 'defun)) + (save-excursion + (goto-char beg) + ;; We can trust Smarparents to get the desired end position. + (-let* ((top-level-sexp (sp-get-sexp)) + ((_ . end) (sp-get top-level-sexp (cons :beg :end)))) + ;; If the sexp is behind point, we aren't interested in it; find one + ;; /ahead/ of point. + (if (< original-point end) + top-level-sexp + (goto-char end) + (sp-next-sexp) + (sp-get-sexp))))))) + +;;;###autoload +(defun syd-get-top-level-sexp-and-attached-comment-bounds () + "Get the bounds of top-level sexp enclosing point and the \"attached\" +comment, if there is one. Returns nil or a pair (BEG . END)." + (-when-let ((beg . end) (sp-get (syd-get-top-level-sexp) (cons :beg :end))) + (let ((attached-comment-beg (save-excursion + (goto-char beg) + (syd-sexp--backward-attached-comment)))) + (cons (or attached-comment-beg beg) + end)))) + +(evil-define-motion syd-forward-defun (count) + :jump t + (sp-get (syd-get-top-level-sexp) + (goto-char :beg) + (dotimes (_ (or count 1)) + (sp-next-sexp)))) + +(defvar syd-sexp-cleanup-operators '(evil-delete) + "When `syd-evil-a-defun' is used in combination with one of these operators, +some cleanup will be performed.") + +(defun syd-sexp--backward-attached-comment () + "Assuming point is on the opening delimiter of a sexp, move point backward to +the beginning of the \"attached\" comment." + (let ((sexp-line (line-number-at-pos)) + (sexp-column (current-column))) + (-when-let ((beg . _end) (save-excursion + (goto-line (- sexp-line 1)) + (evil-forward-char sexp-column t) + (sp-get-comment-bounds))) + (goto-char beg)))) + +;;;###autoload +(evil-define-text-object syd-evil-a-defun (count _beg _end _type) + "Selects the enclosing top-level sexp. With a COUNT of N, that many +consequtive top-level sexps will be selected. TODO: Special care will be taken +to clean up whitespace following certain operators." + :type inclusive + (when (< count 0) + (user-error "TODO: Negative count")) + (-let ((cleanup-p (memq evil-this-operator syd-sexp-cleanup-operators)) + ((beg-0 . end-0) + (syd-get-top-level-sexp-and-attached-comment-bounds))) + (if (or (null count) (= count 1)) + (list beg-0 end-0) + (goto-char end-0) + (dotimes (_ (- count 1)) + (sp-next-sexp)) + (sp-get (sp-get-sexp) + (list beg-0 :end))))) + +;; IDEA: How about the inner-defun text object selects the defun /without/ the +;; comment? Is that more useful, or less? I can't think of the last time I've +;; needed the top-level sexp without the brackets. + +;;;###autoload +(evil-define-text-object syd-evil-inner-defun (_count _beg _end _type) + "Select the *content* of the enclosing top-level sexp, i.e. without the +delimiters." + :type inclusive + (sp-get (syd-get-top-level-sexp) + (list (+ :beg 1) + (- :end 1)))) + +(defun syd-sexp--forward-trailing-whitespace (sexp) + "Move point to the end of the whitespace trailing after SEXP." + (goto-char (sp-get sexp :end)) + (skip-chars-forward "[:blank:]") + (when (= (char-after) ?\n) + (forward-char) + (skip-chars-forward "[:blank:]"))) + +(defun syd-sexp--backward-leading-whitespace (sexp) + "Move point to the beginning of the whitespace preceding SEXP." + (goto-char (sp-get sexp :beg)) + (skip-chars-backward "[:blank:]") + (when (= (char-before) ?\n) + (backward-char) + (skip-chars-backward "[:blank:]"))) + +;;;###autoload +(evil-define-text-object syd-evil-a-form (count _beg _end _type) + (let* ((cleanup-p (memq evil-this-operator syd-sexp-cleanup-operators)) + (sexp (syd-get-enclosing-sexp))) + (if cleanup-p + (save-excursion + (if (syd-sexp--looking-at-last-p) + (progn (syd-sexp--backward-leading-whitespace sexp) + (list (point) (sp-get sexp :end))) + (syd-sexp--forward-trailing-whitespace sexp) + (list (sp-get sexp :beg) (point)))) + (sp-get sexp (list :beg :end))))) + +;;;###autoload +(evil-define-text-object syd-evil-inner-form (count _beg _end _type) + (sp-get (syd-get-enclosing-sexp) + (list (+ :beg 1) (- :end 1)))) + +;;;###autoload +(evil-define-command syd-open-sexp-below () + "Insert a newline with appropriate indentation after the enclosing sexp. A +sexp-wise analogue to Evil's line-wise `evil-open-below'." + :suppress-operator t + (evil-with-single-undo + ;; We want to add an additional blank line when operating at the top level. + ;; Instead of parsing upward until we can no longer find an enclosing sexp, we + ;; simply check if the opening bracket is on the first column. This is not + ;; very correct, but it's way less work (for myself and the CPU). If we + ;; switch to a tree-sitter–based parser, I'd love to switch to the correct + ;; algorithm. + (-let* (((beg . end) (sp-get (syd-get-enclosing-sexp) (cons :beg :end))) + (col (save-excursion (goto-char beg) (current-column)))) + (goto-char end) + (if (= col 0) + (newline 2) + (newline-and-indent)))) + (evil-insert-state 1)) + +;;;###autoload +(evil-define-command syd-open-sexp-above () + "Insert a newline with appropriate indentation above the enclosing sexp. A +sexp-wise analogue to Evil's line-wise `evil-open-above'." + :suppress-operator t + (evil-with-single-undo + (let ((beg (sp-get (syd-get-enclosing-sexp) :beg))) + (goto-char beg) + (syd-sexp--backward-attached-comment) + (let ((col (current-column))) + (save-excursion + ;; We want to add an additional blank line when operating at the top + ;; level. Instead of parsing upward until we can no longer find an + ;; enclosing sexp, we simply check if the opening bracket is on the + ;; first column. This is not very correct, but it's way less work (for + ;; myself and the CPU). If we switch to a tree-sitter–based parser, I'd + ;; love to switch to the correct algorithm. + (if (= col 0) + (newline 2) + (newline-and-indent))) + (indent-to col) + (evil-insert-state 1))))) + +(defun syd-sexp-get-last-thing () + (-let (((enclosing-beg . enclosing-end) + (sp-get (syd-get-enclosing-sexp) (cons :beg :end)))) + (save-excursion + ;; Imperative andy. }:\ + (let (thing) + (while (sp-get (syd-get-thing) + (and (< enclosing-beg :beg enclosing-end) + (< enclosing-beg :end enclosing-end)))))))) + +(defun syd-sexp--looking-at-last-p () + "Return non-nil if the sexp beginning at point is the last element of its +enclosing sexp." + (save-excursion + (let ((point-0 (point)) + (sexp (sp-get-enclosing-sexp))) + (sp-next-sexp) + (if sexp + (or + ;; If `sp-next-sexp' moved backwards, `point-0' was the last + ;; element. + (<= (point) point-0) + ;; If `sp-next-sexp' moved outside of the previously-enclosing + ;; sexp, `point-0' was final. + (<= (sp-get sexp :end) (point))) + ;; No enclosing sexp — we're looking at a top-level sexp. + (= (point) point-0))))) + +(defun syd-sexp--next-thing () + "Helper for `syd-sexo->'. Find the next thing relative to the sexp assumed to +begin at point, and the region covering the closing delimiters." + (save-excursion + (condition-case err + (cl-loop for relative-height from 0 + while (syd-sexp--looking-at-last-p) + do (or (sp-backward-up-sexp) + ;; Nothing to slurp! + (signal 'top)) + finally return (cons (sp-next-sexp) relative-height)) + (top nil)))) + +(defun syd-sexp--slurp-forward () + "Slurp forward. Do not call this function directly; see `syd-sexp->'." + ;; REVIEW: This is rather unoptimised when used with a count. + (when-let* ((consumer (sp-get-sexp))) + (goto-char (sp-get consumer :beg)) + (-if-let ((next-thing . relative-height) (syd-sexp--next-thing)) + (progn (goto-char (sp-get consumer :beg-in)) + (sp-forward-slurp-sexp (+ 1 relative-height)) + (sp-get (sp-get-enclosing-sexp) + (goto-char (- :end 1)))) + (user-error "ra")))) + +(defun syd-sexp--barf-forward () + "Barf forward. Do not call this function directly; see `syd-sexp-<'." + (sp-forward-barf-sexp)) + +;;;###autoload +(evil-define-command syd-sexp-> (&optional count) + (interactive "") + (evil-with-single-undo + (when-let* ((sexp (sp-get-sexp))) + (let ((fn (cond ((= (point) (sp-get sexp (- :end 1))) + #'syd-sexp--slurp-forward)))) + (dotimes (_ (or count 1)) + (funcall fn)))))) + +;;;###autoload +(evil-define-command syd-sexp-< (&optional count) + (interactive "") + (evil-with-single-undo + (when-let* ((sexp (sp-get-sexp))) + (let ((fn (cond ((= (point) (sp-get sexp (- :end 1))) + #'syd-sexp--barf-forward)))) + (dotimes (_ (or count 1)) + (funcall fn)))))) + +(defun syd-sexp--looking-at-delimiter-p () + (sp-get (sp-get-sexp) + (and (not (sp-point-in-string-or-comment)) + (or (= (point) :beg) + (= (point) (- :end 1)))))) + +;; REVIEW: It might be neat to, iff the point is already in a comment/string, +;; goto delimiters that are also in comments/strings. For now, let's just +;; ignore comments. +(defun syd-sexp--goto-delimiter (delimiter-type direction count) + (let* ((point-0 (point)) + (delimiters (mapcar (clj-condp eq delimiter-type + ('opening #'car) + ('closing #'cdr)) + sp-pair-list)) + (delimiter-regexp (rx-to-string `(or ,@delimiters))) + (forward-p (clj-condp eq direction + ('forward t) + ('backward nil) + (t (error "todo errrrare")))) + (move (lambda () + ;; `forward-p' never changes between calls to `move'; we are + ;; doing many more checks than we need to. + (and (condition-case er + (prog1 t (when forward-p + (forward-char))) + (end-of-buffer (throw 'no-move 'no-move))) + (if (if forward-p + (re-search-forward delimiter-regexp nil t) + (re-search-backward delimiter-regexp nil t)) + (goto-char (match-beginning 0)) + (throw 'no-move 'no-move)))))) + ;; If `syd-sexp--looking-at-delimiter-p' returns nil, we may be looking at + ;; the right string of characters, but we are likely inside of a string, + ;; or a comment, or something. If we aren't at a "real" delimiter, move + ;; again. + (let ((r (catch 'no-move + (dotimes (_ count) + (while (and (funcall move) + (not (syd-sexp--looking-at-delimiter-p)))))))) + (if (eq r 'no-move) + (progn (goto-char point-0) + (user-error "Nowhere to go")) + r)))) + +(evil-define-motion syd-sexp-forward-opening (count) + (syd-sexp--goto-delimiter 'opening 'forward (or count 1))) + +(evil-define-motion syd-sexp-backward-opening (count) + (syd-sexp--goto-delimiter 'opening 'backward (or count 1))) + +(evil-define-motion syd-sexp-forward-closing (count) + (syd-sexp--goto-delimiter 'closing 'forward (or count 1))) + +(evil-define-motion syd-sexp-backward-closing (count) + (syd-sexp--goto-delimiter 'closing 'backward (or count 1))) + +(defun syd-sexp-get-sexp-with-prefix () + (-when-let* ((thing (sp-get-thing)) + ;; TODO: Rewrite using :beg-prf + ((beg . prefix) (sp-get thing (cons :beg :prefix))) + (prefix-beg (- beg (length prefix)))) + ;; HACK: Relies on Smartparen's internal representation, which + ;; they explicitly recommend against. This could break at any + ;; time! + ;; Reminder that `plist-put' is an in-place update. }:) + (plist-put thing :beg prefix-beg) + (plist-put thing :prefix "") + (goto-char prefix-beg) + thing)) + +(evil-define-motion syd-sexp-next (count) + "Like `sp-next-sexp', but prefixes will be considered as part of the sexp." + ;; If point is resting on a prefix when `syd-sexp-next' is called, + ;; `sp-next-sexp' will move to the beginning of the prefixed form. This is + ;; undesirable, as `syd-sexp-next' considers the prefix and the prefixed form + ;; to be a single thing. To get around this, we make sure to move point past + ;; the prefixed sexp. + (let ((count* (or count 1))) + (when-let* ((_ (<= 0 count*)) + (first-prefixed-sexp (syd-sexp-get-sexp-with-prefix))) + (sp-get first-prefixed-sexp + (when (<= :beg (point) :end) + (goto-char :end)))) + (let ((current-prefix-arg count*)) + (call-interactively #'sp-next-sexp))) + (syd-sexp-get-sexp-with-prefix)) + +(evil-define-motion syd-sexp-previous (count) + "Like `sp-next-sexp' (as if called with a negative count), but prefixes will +be considered as part of the sexp." + (syd-sexp-next (- (or count 1)))) + +;;;###autoload +(evil-define-command syd-sexp-insert () + (evil-with-single-undo + (sp-get (syd-get-enclosing-sexp) + (goto-char (+ 1 :beg)) + (save-excursion (insert-char ?\s)) + (evil-insert-state 1)))) + +;;;###autoload +(evil-define-command syd-sexp-append () + (evil-with-single-undo + (sp-get (syd-get-enclosing-sexp) + (goto-char (- :end 1)) + (evil-insert-state 1)))) + +;; Text objects. +(general-def + :keymaps 'syd-lisp-mode-map + :states '(visual operator) + "ad" #'syd-evil-a-defun + "id" #'syd-evil-inner-defun + "af" #'syd-evil-a-form + "if" #'syd-evil-inner-form) + +;; Bind editing commands in normal node, and motion commands in motion +;; mode. +(general-def + :keymaps 'syd-lisp-mode-map + :states 'normal + ">" #'syd-sexp-> + "<" #'syd-sexp-< + "M-w" #'syd-wrap-sexp + "M-r" #'sp-raise-sexp + "M-c" #'sp-clone-sexp + "M-S" #'sp-split-sexp + "M-J" #'sp-join-sexp + "M-u" #'sp-splice-sexp-killing-backward + "M-U" #'sp-splice-sexp-killing-around + "M-v" #'sp-convolute-sexp + "M-o" #'syd-open-sexp-below + "M-O" #'syd-open-sexp-above + "M-i" #'syd-sexp-insert + "M-a" #'syd-sexp-append) + +;; Bind editing commands in normal node, and motion commands in motion +;; mode. +(general-def + :keymaps 'syd-lisp-mode-map + :states 'motion + "C-h" #'sp-backward-up-sexp + "C-j" #'syd-sexp-next + "C-k" #'syd-sexp-previous + "C-l" #'sp-down-sexp + "(" #'syd-backward-up-sexp + ")" #'syd-forward-up-sexp + "{" #'syd-sexp-backward-opening + "}" #'syd-sexp-forward-opening + "M-{" #'syd-sexp-backward-closing + "M-}" #'syd-sexp-forward-closing) + +(with-eval-after-load 'smartparens + (setq + ;; By default, Smartparens will move backwards to the initial character of + ;; the enclosing expression, and only move forwards when the point is already + ;; on that initial character. This is not expected behaviour for an ex-Vim + ;; user. + sp-navigate-interactive-always-progress-point t)) + +(provide 'syd-lisp-lib) diff --git a/users/crumb/programs/emacs/lib/syd-prelude.el b/users/crumb/programs/emacs/lib/syd-prelude.el index 7d3225e..01dcc66 100644 --- a/users/crumb/programs/emacs/lib/syd-prelude.el +++ b/users/crumb/programs/emacs/lib/syd-prelude.el @@ -66,4 +66,12 @@ (put ',hook-name 'permanent-local-hook t) (add-hook ,hook-or-function* #',hook-name)))))) +(defun syd-plist-put (plist prop new-val) + "Immutably update a single property of PLIST. Like `plist-put', but PLIST is +not mutated; a new plist is returned." + (cl-loop for (prop* old-val) on plist by #'cddr + appending (if (eq prop prop*) + (list prop* new-val) + (list prop* old-val)))) + (provide 'syd-prelude) diff --git a/users/crumb/programs/emacs/lib/syd-text.el b/users/crumb/programs/emacs/lib/syd-text.el index 57762c5..d28df2e 100644 --- a/users/crumb/programs/emacs/lib/syd-text.el +++ b/users/crumb/programs/emacs/lib/syd-text.el @@ -69,16 +69,24 @@ in some cases." (read-string (if (stringp prompt) prompt ""))))) ;;;###autoload -(defun syd-insert-newline-above () +(defun syd-insert-newline-above (count) "Insert a blank line below the current line." - (interactive) - (save-excursion (evil-insert-newline-above))) + (interactive "p") + (dotimes (_ count) + (let ((point-was-at-bol-p (= (current-column) 0))) + (save-excursion + (evil-insert-newline-above)) + ;; Special case: with `syd-insert-newline-above' is called with point at + ;; BOL, the point unexpectedly fails to "stick" to its original position. + (when point-was-at-bol-p + (next-line))))) ;;;###autoload -(defun syd-insert-newline-below () +(defun syd-insert-newline-below (count) "Insert a blank line below the current line." - (interactive) - (save-excursion (evil-insert-newline-below))) + (interactive "p") + (dotimes (_ count) + (save-excursion (evil-insert-newline-below)))) ;;;###autoload (defun syd-render-ansi-escape-codes (beg end) diff --git a/users/crumb/programs/emacs/modules/lang/syd-lang-emacs-lisp.el b/users/crumb/programs/emacs/modules/lang/syd-lang-emacs-lisp.el index 22c04ba..1dbcff3 100644 --- a/users/crumb/programs/emacs/modules/lang/syd-lang-emacs-lisp.el +++ b/users/crumb/programs/emacs/modules/lang/syd-lang-emacs-lisp.el @@ -3,6 +3,7 @@ (require 'syd-handle-repl) (require 'syd-handle-lookup) (require 'syd-handle-eval) +(require 'syd-lisp-lib) ;; (require 'handle) ;; Don't `use-package' `ielm', since it's loaded by Emacs. You'll get weird @@ -12,6 +13,7 @@ :custom ((ielm-history-file-name ; Stay out of my config dir! (file-name-concat syd-cache-dir "ielm-history.eld")))) +;;;###autoload (defun syd/open-emacs-lisp-repl () (interactive) (pop-to-buffer @@ -23,21 +25,19 @@ (bury-buffer b) b))))) +;;;###autoload (defun syd-emacs-lisp-lookup-documentation (identifier) "Lookup IDENTIFIER with `describe-symbol'" - ;; HACK: Much to my frustration, `describe-symbol' has no defined - ;; return value. To test if the call was successful or not, we - ;; check if any window is displaying the help buffer. This probably - ;; breaks if `syd-emacs-lisp-lookup-documentation' is called while - ;; the help buffer is already open. + ;; HACK: Much to my frustration, `describe-symbol' has no defined return + ;; value. To test if the call was successful or not, we check if any window + ;; is displaying the help buffer. This probably breaks if + ;; `syd-emacs-lisp-lookup-documentation' is called while the help buffer is + ;; already open. (describe-symbol (intern identifier)) (let ((buffer (get-buffer (help-buffer)))) (and (get-buffer-window-list buffer) buffer))) -(set-repl-handler! 'emacs-lisp-mode - #'syd/open-emacs-lisp-repl) - ;;;###autoload (defun syd-emacs-lisp-eval (beg end) "Evaluate a region and print it to the echo area (if one line long), otherwise @@ -60,13 +60,35 @@ to a pop up buffer." :source-buffer (current-buffer) :force-popup current-prefix-arg)) +(set-repl-handler! 'emacs-lisp-mode + #'syd/open-emacs-lisp-repl) + (set-eval-handler! 'emacs-lisp-mode #'syd-emacs-lisp-eval) -(defun syd-emacs-set-handlers () +(add-hook 'emacs-lisp-mode-hook #'syd-lisp-mode) + +(defun syd-emacs-set-handlers-h () (setq-local syd-lookup-documentation-handlers (list #'syd-emacs-lisp-lookup-documentation))) -(add-hook 'emacs-lisp-mode-hook #'syd-emacs-set-handlers) +(add-hook 'emacs-lisp-mode-hook #'syd-emacs-set-handlers-h) +(add-hook 'help-mode-hook #'syd-emacs-set-handlers-h) + +;; Semantic highlighting for Elisp. +(use-package highlight-defined + :hook (emacs-lisp-mode-hook . highlight-defined-mode)) + +;; Automatically and inteligently expand abbreviations. E.g. `wcb` will be +;; expanded to `(with-current-buffer)`, but only where it makes sense for a +;; function/macro call to be. +(use-package sotlisp + :straight (:host github + :repo "Malabarba/speed-of-thought-lisp") + :hook (emacs-lisp-mode . speed-of-thought-mode)) + +;; Give different pairs of delimiters different colours. +(use-package rainbow-delimiters + :hook (emacs-lisp-mode . rainbow-delimiters-mode)) (provide 'syd-lang-emacs-lisp) diff --git a/users/crumb/programs/emacs/modules/syd-editing.el b/users/crumb/programs/emacs/modules/syd-editing.el new file mode 100644 index 0000000..28e70ee --- /dev/null +++ b/users/crumb/programs/emacs/modules/syd-editing.el @@ -0,0 +1,11 @@ +;;; syd-editing.el -*- lexical-binding: t; -*- + +(use-package emacs + :hook ((on-init-ui-hook . whitespace-mode)) + :custom ((fill-column 80) + (indent-tabs-mode nil) + (whitespace-style '(face tabs tab-mark)) + ;; Disable synchronization between the kill ring and clipboard. + (select-enable-clipboard nil))) + +(provide 'syd-editing) diff --git a/users/crumb/programs/emacs/modules/syd-eshell.el b/users/crumb/programs/emacs/modules/syd-eshell.el index 420b464..d81dc71 100644 --- a/users/crumb/programs/emacs/modules/syd-eshell.el +++ b/users/crumb/programs/emacs/modules/syd-eshell.el @@ -1,5 +1,8 @@ ;;; syd-eshell.el -*- lexical-binding: t; -*- +(require 'ring) +(require 'cl-lib) + (defvar eshell-buffer-name "*eshell*") (defvar syd-eshell-buffers (make-ring 25) @@ -72,7 +75,11 @@ (eshell-kill-processes-on-exit t) (eshell-hist-ignoredups t) (eshell-glob-case-insensitive t) - (eshell-error-if-no-glob t)) + (eshell-error-if-no-glob t) + (eshell-history-file-name (file-name-concat + syd-data-dir "eshell" "history")) + (eshell-last-dir-ring-file-name (file-name-concat + syd-data-dir "eshell" "lastdir"))) :general (:keymaps 'syd-leader-open-map "e" #'syd-eshell/toggle) @@ -80,6 +87,7 @@ :states '(normal insert) "C-j" #'eshell-next-matching-input-from-input "C-k" #'eshell-previous-matching-input-from-input) + :config (require 'syd-buffers) diff --git a/users/crumb/programs/emacs/modules/syd-evil.el b/users/crumb/programs/emacs/modules/syd-evil.el index 8b9d5e8..03bd8ce 100755 --- a/users/crumb/programs/emacs/modules/syd-evil.el +++ b/users/crumb/programs/emacs/modules/syd-evil.el @@ -1,5 +1,9 @@ ;;; syd-evil.el -*- lexical-binding: t; -*- +;; More sensible undo functionality. Emacs' default is very weird, not +;; maintaining a proper history. +(use-package undo-fu) + ;; Vim emulation. (use-package evil :preface @@ -60,18 +64,24 @@ Otherwise, nil." nil))) (add-hook 'syd-escape-hook #'syd-evil-nohl-h) + (general-def + :states 'motion + "/" #'evil-ex-search-forward + "?" #'evil-ex-search-backward + "n" #'evil-ex-search-next + "N" #'evil-ex-search-previous + "*" #'evil-ex-search-word-forward) + (evil-mode 1)) -(defvar evil-collection-key-blacklist) - - ;; A large, community-sourced collection of preconfigured Evil-mode ;; integrations. (use-package evil-collection - :after evil - :defer t + ;; :after evil + ;; :defer t :custom (evil-collection-setup-minibuffer t) - :config + :preface + (defvar evil-collection-key-blacklist) (unless noninteractive (defvar syd-evil-collection-disabled-list '(anaconda-mode buff-menu calc comint company custom eldoc elisp-mode ert @@ -314,4 +324,29 @@ modules." :bind (:map evil-visual-state-map ("*" . evil-visualstar/begin-search-forward))) +(defvar syd-evil-last-eval-expression-register ?e + "An Evil-mode register in which the last expression evaluated with an +interactive call to `eval-expression' is stored.") + +(with-eval-after-load 'evil + (defun syd-set-eval-expression-register-a (expr &rest _) + "If called interactively, set the register +`syd-evil-last-eval-expression-register' to a printed form of EXPR." + (when (called-interactively-p 'interactive) + (->> (pp-to-string expr) + (string-remove-suffix "\n") + (evil-set-register syd-evil-last-eval-expression-register)))) + (advice-add #'eval-expression + :after #'syd-set-eval-expression-register-a)) + +;; HACK: '=' unpredictably moves the cursor when it really doesn't need to. +(defun syd-evil-dont-move-point-a (fn &rest args) + "Used as :around advice on Evil operators to avoid moving the point." + ;; We don't use `save-excursion', as we /only/ want to restore the point. + (save-excursion (apply fn args))) + +(with-eval-after-load 'evil + (advice-add #'evil-indent + :around #'syd-evil-dont-move-point-a)) + (provide 'syd-evil) diff --git a/users/crumb/programs/emacs/modules/syd-keybinds.el b/users/crumb/programs/emacs/modules/syd-keybinds.el index 393177f..12f8aff 100755 --- a/users/crumb/programs/emacs/modules/syd-keybinds.el +++ b/users/crumb/programs/emacs/modules/syd-keybinds.el @@ -108,13 +108,6 @@ all hooks after it are ignored.") (global-set-key [remap keyboard-quit] #'syd/escape) - (general-def - :states 'motion - "/" #'evil-ex-search-forward - "?" #'evil-ex-search-backward - "n" #'evil-ex-search-next - "N" #'evil-ex-search-previous) - ;; Buffer (require 'syd-buffers) (general-def