feat: Progress towards comfortable Lisp editing

This commit is contained in:
Madeleine Sydney
2025-02-02 14:51:04 -07:00
parent a345b5a72d
commit fc14c41edd
11 changed files with 623 additions and 39 deletions

View File

@@ -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]]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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-sitterbased 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-sitterbased 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 "<c>")
(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 "<c>")
(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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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