Introduce more opinionated backspace/del/newline behavior

+ Instead of remapping delete-backward-char to doom/delete-backward-char
  (which was unreliable, depending on the mode), it is now overridden
  with it, without sacrificing its original functionality. The new
  behavior is as follows:
  + Fall back to sp-backward-delete-char when it makes sense to delete
    the adjacent pair: {|} => |
  + Collapse an indented pair block, if at bolp in between: {
      |
    } => {|}
  + Refresh a pair's :post-handlers when deleting into pair: {
      |
    } => {|} => {
      |
    } (can be repeated)
  + When cursor is preceded by whitespace, delete in increments of
    tab-width.
+ newline-and-indent has been advised to:
  + Only newline when in a string.
  + Continue comment lines consistently (needs more testing!)
  + Falls back to basic newline-and-indent, without affecting whitespace
    in the origin line (it would originally delete-horizontal-space
    before creating a new line).
+ Incorporates a set of reasonable defaults for brace expansion on RET
  or SPC, as mentioned in #343 and #413.

Affects #343, #413
This commit is contained in:
Henrik Lissner 2018-02-14 05:10:48 -05:00
parent b43743d565
commit 57adae5ec6
No known key found for this signature in database
GPG key ID: 5F6C0EA160557395
3 changed files with 181 additions and 113 deletions

View file

@ -1,5 +1,33 @@
;;; core/autoload/editor.el -*- lexical-binding: t; -*- ;;; core/autoload/editor.el -*- lexical-binding: t; -*-
;;;###autoload
(defun doom-surrounded-p (&optional pair inline balanced)
"Returns t if point is surrounded by a brace delimiter: {[(
If INLINE is non-nil, only returns t if braces are on the same line, and
whitespace is balanced on either side of the cursor.
If INLINE is nil, returns t if the opening and closing braces are on adjacent
lines, above and below, with only whitespace in between."
(when-let* ((pair (or pair (sp-get-thing))))
(let ((op (plist-get pair :op))
(cl (plist-get pair :cl)))
(and op cl
(not (string-empty-p op))
(not (string-empty-p cl))
(let ((beg (+ (length op) (plist-get pair :beg)))
(end (- (plist-get pair :end) (length cl)))
(pt (point)))
(let ((content (buffer-substring-no-properties beg end)))
(and (string-match-p (format "[ %s]*" (if inline "" "\n")) content)
(or (not balanced)
(= (- pt beg) (- end pt))))))))))
;;
;; Commands
;;
;;;###autoload ;;;###autoload
(defun doom/backward-to-bol-or-indent () (defun doom/backward-to-bol-or-indent ()
"Jump between the indentation column (first non-whitespace character) and the "Jump between the indentation column (first non-whitespace character) and the
@ -43,12 +71,6 @@ true end of the line. The opposite of `doom/backward-to-bol-or-indent'."
((/= bol boc) ((/= bol boc)
(goto-char boc))))))) (goto-char boc)))))))
(defun doom--surrounded-p ()
(and (looking-back "[[{(]\\(\s+\\|\n\\)?\\(\s\\|\t\\)*" (line-beginning-position))
(let* ((whitespace (match-string 1))
(match-str (concat whitespace (match-string 2) "[])}]")))
(looking-at-p match-str))))
;;;###autoload ;;;###autoload
(defun doom/dumb-indent () (defun doom/dumb-indent ()
"Inserts a tab character (or spaces x tab-width)." "Inserts a tab character (or spaces x tab-width)."
@ -78,7 +100,7 @@ true end of the line. The opposite of `doom/backward-to-bol-or-indent'."
;;;###autoload ;;;###autoload
(defun doom/backward-kill-to-bol-and-indent () (defun doom/backward-kill-to-bol-and-indent ()
"Kill line to the first non-blank character. If invoked again "Kill line to the first non-blank character. If invoked again
afterwards, kill line to column 1." afterwards, kill line to beginning of line."
(interactive) (interactive)
(let ((empty-line-p (save-excursion (beginning-of-line) (let ((empty-line-p (save-excursion (beginning-of-line)
(looking-at-p "[ \t]*$")))) (looking-at-p "[ \t]*$"))))
@ -94,109 +116,97 @@ afterwards, kill line to column 1."
"Delete back to the previous column of whitespace, or as much whitespace as "Delete back to the previous column of whitespace, or as much whitespace as
possible, or just one char if that's not possible." possible, or just one char if that's not possible."
(interactive) (interactive)
(let* ((delete-backward-char (if (derived-mode-p 'org-mode) (let* ((context (sp-get-thing))
#'org-delete-backward-char (op (plist-get context :op))
#'delete-backward-char)) (cl (plist-get context :cl))
(context (sp--get-pair-list-context 'navigate))
(open-pair-re (sp--get-opening-regexp context))
(close-pair-re (sp--get-closing-regexp context))
open-len close-len) open-len close-len)
(cond ;; When in strings (sp acts weird with quotes; this is the fix) (cond ;; When in strings (sp acts weird with quotes; this is the fix)
;; Also, skip closing delimiters ;; Also, skip closing delimiters
((and (and (sp--looking-back open-pair-re) ((and (string= op cl)
(setq open-len (- (match-beginning 0) (match-end 0)))) (and (string= (char-to-string (char-before)) op)
(and (looking-at close-pair-re) (setq open-len (length op)))
(setq close-len (- (match-beginning 0) (match-end 0)))) (and (string= (char-to-string (char-after)) cl)
(string= (plist-get (sp-get-thing t) :op) (setq close-len (length cl))))
(plist-get (sp-get-thing) :cl))) (delete-char (- open-len))
(delete-char (- 0 open-len))
(delete-char close-len)) (delete-char close-len))
;; Delete up to the nearest tab column IF only whitespace between ;; Delete up to the nearest tab column IF only whitespace between
;; point and bol. ;; point and bol.
((save-match-data (looking-back "^[\\t ]*" (line-beginning-position))) ((and (not indent-tabs-mode)
(let ((movement (% (current-column) tab-width)) (not (bolp))
(p (point))) (not (sp-point-in-string))
(save-excursion (>= (- (skip-chars-backward " \t")) tab-width)))
(let ((movement (% (current-column) tab-width)))
(when (= movement 0) (when (= movement 0)
(setq movement tab-width)) (setq movement tab-width))
(save-match-data (delete-char (- movement)))
(if (string-match "\\w*\\(\\s-+\\)$" (unless (memq (char-before) (list ?\n ?\ ))
(buffer-substring-no-properties (max (point-min) (- p movement)) p)) (insert " ")))
(sp-delete-char
(- 0 (- (match-end 1)
(match-beginning 1))))
(call-interactively delete-backward-char)))))
;; Otherwise do a regular delete ;; Otherwise do a regular delete
(t (call-interactively delete-backward-char))))) (t (delete-char -1)))))
;;;###autoload ;;;###autoload
(defun doom/inflate-space-maybe () (defun doom/delete-backward-char (n &optional killflag)
"Checks if point is surrounded by {} [] () delimiters and adds a "Same as `delete-backward-char', but preforms these additional checks:
space on either side of the point if so."
(interactive)
(let ((command (or (command-remapping #'self-insert-command)
#'self-insert-command)))
(cond ((doom--surrounded-p)
(call-interactively command)
(save-excursion (call-interactively command)))
(t
(call-interactively command)))))
;;;###autoload + If point is surrounded by (balanced) whitespace and a brace delimiter ({} []
(defun doom/deflate-space-maybe () ()), delete a space on either side of the cursor.
"Checks if point is surrounded by {} [] () delimiters, and deletes + If point is at BOL and surrounded by braces on adjacent lines, collapse
spaces on either side of the point if so. Resorts to newlines:
`doom/backward-delete-whitespace-to-column' otherwise." {
(interactive) |
(save-match-data } => {|}
(if (doom--surrounded-p) + Otherwise, resort to `doom/backward-delete-whitespace-to-column'.
(let ((whitespace-match (match-string 1))) + Resorts to `delete-char' if n > 1"
(cond ((not whitespace-match) (interactive "p\nP")
(call-interactively #'delete-backward-char)) (unless (integerp n)
((string-match "\n" whitespace-match) (signal 'wrong-type-argument (list 'integerp n)))
(funcall (if (featurep 'evil) (cond ((and (use-region-p)
#'evil-delete delete-active-region
#'delete-region) (= n 1))
(point-at-bol) (point)) ;; If a region is active, kill or delete it.
(call-interactively #'delete-backward-char) (if (eq delete-active-region 'kill)
(save-excursion (call-interactively #'delete-char))) (kill-region (region-beginning) (region-end) 'region)
(t (just-one-space 0)))) (funcall region-extract-function 'delete-only)))
(doom/backward-delete-whitespace-to-column)))) ;; In Overwrite mode, maybe untabify while deleting
((null (or (null overwrite-mode)
;;;###autoload (<= n 0)
(defun doom/newline-and-indent () (memq (char-before) '(?\t ?\n))
"Inserts a newline and possibly indents it. Also continues comments if (eobp)
executed from a commented line; handling special cases for certain languages (eq (char-after) ?\n)))
with weak native support." (let ((ocol (current-column)))
(interactive) (delete-char (- n) killflag)
(cond ((sp-point-in-string) (save-excursion
(newline)) (insert-char ?\s (- ocol (current-column)) nil))))
((sp-point-in-comment) ;;
(pcase major-mode ((and (= n 1) (not (minibufferp)))
((or 'js2-mode 'rjsx-mode) (let* ((pair (sp-get-thing))
(call-interactively #'js2-line-break)) (op (plist-get pair :op))
((or 'java-mode 'php-mode) (cl (plist-get pair :cl))
(c-indent-new-comment-line)) (beg (plist-get pair :beg))
((or 'c-mode 'c++-mode 'objc-mode 'css-mode 'scss-mode 'js2-mode) (end (plist-get pair :end)))
(newline-and-indent) (cond ((and end beg (= end (+ beg (length op) (length cl))))
(insert "* ") (sp-backward-delete-char 0))
(indent-according-to-mode)) ((doom-surrounded-p pair :inline :balanced)
(_ (delete-char -1 killflag)
;; Fix an off-by-one cursor-positioning issue (delete-char 1)
;; with `indent-new-comment-line' (when (= (point) (+ (length cl) beg))
(let ((col (save-excursion (comment-beginning) (current-column)))) (sp-backward-delete-char 1)
(indent-new-comment-line) (sp-insert-pair op)))
(unless (= col (current-column)) ((and (bolp) (doom-surrounded-p pair))
(insert " ")))))) (delete-region beg end)
(t (sp-insert-pair op))
(newline nil t) (t
(indent-according-to-mode)))) (doom/backward-delete-whitespace-to-column)))))
;; Otherwise, do simple deletion.
(t (delete-char (- n) killflag))))
;;;###autoload ;;;###autoload
(defun doom/retab (&optional beg end) (defun doom/retab (&optional beg end)
"Changes all tabs to spaces or spaces to tabs, so that indentation is "Converts tabs-to-spaces or spaces-to-tabs within BEG and END (defaults to
consistent throughout a selected region, depending on `indent-tab-mode'." buffer start and end, to make indentation consistent. Which it does depends on
the value of `indent-tab-mode'."
(interactive "r") (interactive "r")
(unless (and beg end) (unless (and beg end)
(setq beg (point-min) (setq beg (point-min)
@ -228,7 +238,33 @@ Inspired from http://demonastery.org/2013/04/emacs-evil-narrow-region/"
(t (t
(widen)))) (widen))))
;;
;; Advice
;;
;;;###autoload
(defun doom*newline-and-indent (orig-fn)
"Inserts a newline and possibly indents it. Also continues comments if
executed from a commented line; handling special cases for certain languages
with weak native support."
(interactive)
(cond ((sp-point-in-string)
(newline))
((and (sp-point-in-comment)
comment-line-break-function)
(funcall comment-line-break-function))
(t
(newline nil t)
(indent-according-to-mode))))
;;
;; Hooks
;;
;;;###autoload ;;;###autoload
(defun doom|enable-delete-trailing-whitespace () (defun doom|enable-delete-trailing-whitespace ()
"Attaches `delete-trailing-whitespace' to a buffer-local `before-save-hook'." "Enables the automatic deletion of trailing whitespaces upon file save, by
attaching `delete-trailing-whitespace' to a buffer-local `before-save-hook'."
(add-hook 'before-save-hook #'delete-trailing-whitespace nil t)) (add-hook 'before-save-hook #'delete-trailing-whitespace nil t))

View file

@ -9,6 +9,7 @@
(map! [remap evil-jump-to-tag] #'projectile-find-tag (map! [remap evil-jump-to-tag] #'projectile-find-tag
[remap find-tag] #'projectile-find-tag [remap find-tag] #'projectile-find-tag
[remap newline] #'newline-and-indent
;; Ensure there are no conflicts ;; Ensure there are no conflicts
:nmvo doom-leader-key nil :nmvo doom-leader-key nil
@ -698,27 +699,13 @@
:i "C-u" #'doom/backward-kill-to-bol-and-indent :i "C-u" #'doom/backward-kill-to-bol-and-indent
;; textmate-esque newline insertion ;; textmate-esque newline insertion
:i [M-return] #'evil-open-below :i [M-return] #'evil-open-below
:i [S-M-return] #'evil-open-above :i [S-M-return] #'evil-open-above
;; textmate-esque deletion ;; textmate-esque deletion
[M-backspace] #'doom/backward-kill-to-bol-and-indent :ig [M-backspace] #'doom/backward-kill-to-bol-and-indent
:i [backspace] #'delete-backward-char
:i [M-backspace] #'doom/backward-kill-to-bol-and-indent
;; Emacsien motions for insert mode ;; Emacsien motions for insert mode
:i "C-b" #'backward-word :i "C-b" #'backward-word
:i "C-f" #'forward-word :i "C-f" #'forward-word
;; Highjacks space/backspace to:
;; a) balance spaces inside brackets/parentheses ( | ) -> (|)
;; b) delete space-indented blocks intelligently
;; c) do none of this when inside a string
:i "SPC" #'doom/inflate-space-maybe
:i [remap delete-backward-char] #'doom/deflate-space-maybe
:i [remap newline] #'doom/newline-and-indent
(:after org
(:map org-mode-map
:i [remap doom/inflate-space-maybe] #'org-self-insert-command))
;; Restore common editing keys (and ESC) in minibuffer ;; Restore common editing keys (and ESC) in minibuffer
(:map (minibuffer-local-map (:map (minibuffer-local-map

View file

@ -23,6 +23,51 @@
epa-pinentry-mode 'loopback)) epa-pinentry-mode 'loopback))
;; disable :unless predicates with (sp-pair "'" nil :unless nil)
;; disable :post-handlers with (sp-pair "{" nil :post-handlers nil)
;; ...or specific :post-handlers with (sp-pair "{" nil :post-handlers '(:rem ("| " "SPC")))
(after! smartparens
;; Autopair quotes more conservatively; if I'm next to a word/before another
;; quote, I likely don't want another pair.
(let ((unless-list '(sp-point-before-word-p
sp-point-after-word-p
sp-point-before-same-p)))
(sp-pair "'" nil :unless unless-list)
(sp-pair "\"" nil :unless unless-list))
;; Expand {|} => { | }
;; Expand {|} => {
;; |
;; }
(dolist (brace '("(" "{" "["))
(sp-pair brace nil
:post-handlers '(("||\n[i]" "RET") ("| " "SPC"))
;; I likely don't want a new pair if adjacent to a word or opening brace
:unless '(sp-point-before-word-p sp-point-before-same-p)))
;; Don't do square-bracket space-expansion where it doesn't make sense to
(sp-local-pair '(emacs-lisp-mode org-mode markdown-mode gfm-mode)
"[" nil :post-handlers '(:rem ("| " "SPC")))
;; Highjacks backspace to:
;; a) balance spaces inside brackets/parentheses ( | ) -> (|)
;; b) delete space-indented `tab-width' steps at a time
;; c) close empty multiline brace blocks in one step:
;; {
;; |
;; }
;; becomes {|}
;; d) refresh smartparens' :post-handlers, so SPC and RET expansions work
;; even after a backspace.
;; e) properly delete smartparen pairs when they are encountered, without the
;; need for strict mode.
;; f) do none of this when inside a string
(advice-add #'delete-backward-char :override #'doom/delete-backward-char)
;; Makes `newline-and-indent' smarter when dealing with comments
(advice-add #'newline-and-indent :around #'doom*newline-and-indent))
(when (featurep 'evil) (when (featurep 'evil)
(when (featurep! +evil-commands) (when (featurep! +evil-commands)
(load! +evil-commands)) (load! +evil-commands))