-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathorg-autolist.el
232 lines (195 loc) · 9 KB
/
org-autolist.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
;;; org-autolist.el --- Improved list management in org-mode
;; Copyright (C) 2022 Calvin Young
;; Author: Calvin Young
;; Keywords: lists, checklists, org-mode
;; Homepage: https://github.com/calvinwyoung/org-autolist
;; Version: 0.16
;; This file is not part of GNU Emacs.
;;; License:
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; `org-autolist` makes org-mode lists behave more like lists in non-programming
;; editors such as Google Docs, MS Word, and OS X Notes.
;;
;; When editing a list item, pressing "Return" will insert a new list item
;; automatically. This works for both bullet points and checkboxes, so there's
;; no need to think about whether to use `M-<return>` or `M-S-<return>`. Similarly,
;; pressing "Backspace" at the beginning of a list item deletes the bullet /
;; checkbox, and moves the cursor to the end of the previous line.
;;
;; To enable org-autolist mode in the current buffer:
;;
;; (org-autolist-mode)
;;
;; To enable it whenever you open an org file, add this to your init.el:
;;
;; (add-hook 'org-mode-hook (lambda () (org-autolist-mode)))
;;
;; To disable backspace deleting the whole list item, add this:
;;
;; (setq org-autolist-enable-delete nil)
;;
;;; Code:
(require 'org)
(require 'org-element)
(defgroup org-autolist ()
"Options for `org-autolist'."
:group 'org
:prefix "org-autolist-"
:tag "Org Autolist")
;;; Customization variables
(defcustom org-autolist-enable-delete t
"Controls auto-deletion of list prefixes when typing e.g. \
`\\<org-mode-map>\\[delete-backward-char]'.
By default, hitting backspace after a list item prefix will not
just delete the character, but the whole prefix. In case of
checkboxes like “- [ ] this”, backward deletion before “this”
will remove the checkbox and the dash.
Set to nil to disable automatic removal."
:type 'boolean
:group 'org-autolist)
;;; Internal Functions
(defun org-autolist-beginning-of-item-after-bullet ()
"Return the position after the bullet of the current list item.
This function uses the same logic as `org-beginning-of-line' when
`org-special-ctrl-a/e' is enabled"
(save-excursion
(beginning-of-line 1)
(when (looking-at org-list-full-item-re)
;; Find the first white space character after bullet, and check-box, if
;; any. That point should be the return value of this function if found.
(let ((box (match-end 3)))
(if (not box) (match-end 1)
(let ((after (char-after box)))
(if (and after (= after ? )) (1+ box) box)))))))
(defun org-autolist-at-empty-item-description-p ()
"Is point at an *empty* description list item?"
(message "evaluating...")
(org-list-at-regexp-after-bullet-p "\\(\\s-*\\)::\\(\\s-*$\\)"))
(defadvice org-return (around org-autolist-return)
"Wraps the `org-return' function to allow the Return key to \
automatically insert new list items.
- Pressing Return at the end of a list item inserts a new list item.
- Pressing Return at the end of a checkbox inserts a new checkbox.
- Pressing return at the beginning of an empty list or checkbox item
outdents the item, or clears it if it's already at the outermost
indentation level."
;; We should only invoke our custom logic if we're in a list item. However,
;; there is an exception if we're on a URL in a list item, and
;; `org-return-follows-link` is enabled -- in this case, we should just let
;; org mode default to following the link.
(let* ((el (org-element-at-point))
(parent (plist-get (cadr el) :parent))
;; handle hard-wrapped list-items
(is-listitem (or (org-at-item-p)
(and (eq 'paragraph (car el))
(eq 'item (car parent)))))
(is-checkbox (plist-get (cadr parent) :checkbox)))
(if (and is-listitem
(not
(and org-return-follows-link
(eq 'org-link (get-text-property (point) 'face)))))
;; If we're at the beginning of an empty list item, then try to outdent
;; it. If it can't be outdented (b/c it's already at the outermost
;; indentation level), then delete it.
(if (and (eolp)
;; need to recheck in case we're at a hard-wrapped list-item
(org-at-item-p)
(<= (point) (org-autolist-beginning-of-item-after-bullet)))
(condition-case nil
(call-interactively 'org-outdent-item)
('error (delete-region (line-beginning-position)
(line-end-position))))
;; Now we can insert a new list item. The logic here is a little tricky
;; depending on the type of list we're dealing with.
(cond
;; If we're on a checkbox item, then insert a new checkbox
(is-checkbox
(org-insert-todo-heading nil))
;; If we're in a description list, and the point is between the start
;; of the list (after the bullet) and the end of the list, then we
;; should simply insert a newline. This is a bit weird and inconsistent
;; w/ the UX for other list types, but we do this b/c `org-meta-return'
;; has some very strange behavior when executed in the middle of a
;; description list.
((and (org-at-item-description-p)
(> (point) (org-autolist-beginning-of-item-after-bullet))
(< (point) (line-end-position)))
(newline))
;; Otherwise just let org-mode figure it out it.
(t
(org-meta-return))))
ad-do-it)))
(defadvice org-delete-backward-char (around org-autolist-delete-backward-char)
"Wraps the `org-delete-backward-char' function to allow \
`\\<org-mode-map>\\[delete-backward-char]' to automatically delete \
list prefixes if `org-autolist-enable-delete' is t.
- Pressing backspace at the beginning of a list item deletes it and
moves the cursor to the previous line.
- If the previous line is blank, then delete the previous line, and
move the current list item up one line."
;; We should only invoke our custom logic if we're at the beginning of a list
;; item right after the bullet character.
(if (and org-autolist-enable-delete
(org-at-item-p)
(<= (point) (org-autolist-beginning-of-item-after-bullet)))
;; If the previous line is empty, then just delete the previous line (i.e.,
;; shift the list up by one line).
(if (org-previous-line-empty-p)
(delete-region (line-beginning-position)
(save-excursion (forward-line -1)
(line-beginning-position)))
;; Otherwise we should delete to the end of the previous line.
(progn
;; We should ensure that the cursor starts at the point after the
;; bullet. This allows us to delete a full checkbox if the cursor is
;; initially positioned in the middle of the checkbox.
(goto-char (org-autolist-beginning-of-item-after-bullet))
;; For most lines, we want to delete from bullet point to the end of
;; the previous line. There are, however a few exceptions.
(cond
;; If we're on the first line in the buffer, then we should just
;; delete the bullet point.n
((= 1 (line-number-at-pos))
(delete-region (point) (line-beginning-position)))
;; If we're on a line with an empty description list, then
;; we should delete the full line since there's not point leaving it
;; around.
((org-autolist-at-empty-item-description-p)
(delete-region (line-end-position)
(save-excursion (forward-line -1)
(line-end-position))))
;; Otherwise we should delete to the end of the previous line by
;; default.
(t
(delete-region (point)
(save-excursion (forward-line -1)
(line-end-position)))))))
ad-do-it))
;;;###autoload
(define-minor-mode org-autolist-mode
"Enables improved list management in `org-mode'."
:lighter " Autolist"
:keymap nil
(cond
;; If enabling org-autolist-mode, then add our advice functions.
(org-autolist-mode
(ad-activate 'org-return)
(ad-activate 'org-delete-backward-char))
;; Be sure to clean up after ourselves when org-autolist-mode gets disabled.
(t
(ad-deactivate 'org-return)
(ad-deactivate 'org-delete-backward-char))))
(provide 'org-autolist)
;;; org-autolist.el ends here