500 lines
19 KiB
EmacsLisp
Executable File
500 lines
19 KiB
EmacsLisp
Executable File
;;; -*- lexical-binding: t; -*-
|
||
|
||
(require 'syd/base)
|
||
(require 'syd/evil)
|
||
|
||
;;;###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.")
|
||
|
||
;; FIXME(#12): Comments should only attach to the *immediately* following sexp.
|
||
;; Consider the following snippet:
|
||
;;
|
||
;; ;; Call the continuation if non-nil. Wraps the return value in a singleton
|
||
;; ;; list for "affine" use with unquote-splicing.
|
||
;; (let ((call-cont (lambda (cont arg)
|
||
;; (if cont
|
||
;; (list (funcall cont arg))
|
||
;; nil)))
|
||
;; names)
|
||
;; ...)
|
||
;;
|
||
;; The curreny behaviour of `syd-sexp--backward-attached-comment' considers the
|
||
;; comment to be attached to both the (let ...) form, as well as the ((call-cont
|
||
;; ...)) form and the (call-cont ...) form. Not good!
|
||
(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 Ive
|
||
;; 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
|
||
(goto-char (sp-get sexp :beg))
|
||
(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 "<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 (cond ((eq delimiter-type 'opening) #'car)
|
||
((eq delimiter-type 'closing) #'cdr))
|
||
sp-pair-list))
|
||
(delimiter-regexp (rx-to-string `(or ,@delimiters)))
|
||
(forward-p (cond ((eq direction 'forward) t)
|
||
((eq direction 'backward) nil)))
|
||
(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 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)
|
||
|
||
(general-def
|
||
:keymaps 'syd-lisp-mode-map
|
||
:states 'insert
|
||
";" #'sp-comment)
|
||
|
||
;; 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 ; Probably deprecated.
|
||
"C-j" #'syd-sexp-next ; Probably deprecated.
|
||
"C-k" #'syd-sexp-previous ; Probably deprecated.
|
||
"C-l" #'sp-down-sexp ; Probably deprecated.
|
||
|
||
"M-h" #'sp-backward-up-sexp
|
||
"M-j" #'syd-sexp-next
|
||
"M-k" #'syd-sexp-previous
|
||
"M-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)
|