-
Notifications
You must be signed in to change notification settings - Fork 0
/
aggressive-fill-paragraph.el
500 lines (397 loc) · 18.9 KB
/
aggressive-fill-paragraph.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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
;;; -*- lexical-binding: t; -*-
;;; aggressive-fill-paragraph.el --- A mode to automatically keep paragraphs filled
;; Author: David Shepherd <[email protected]>
;; Version: 0.0.1
;; Package-Version: 20160301.1414
;; Package-Requires: ((dash "2.10.0"))
;; URL: https://github.com/davidshepherd7/aggressive-fill-paragraph-mode
;; Keywords: fill-paragraph, automatic, comments
;;; Commentary:
;; An emacs minor-mode for keeping paragraphs filled in both comments and prose.
;;; Code:
(require 'dash)
;; Helpers
(defun afp-inside-comment? ()
(nth 4 (syntax-ppss)))
(defun afp-outside-comment? ()
(not (afp-inside-comment?)))
(defun afp-get-comment-bounds ()
"Return a list containing the current comment's start and end points.
Return `nil' if point is not currently inside a comment.
This returns nil in single-line comment blocks if point is on
a comment-start sequence, because syntax-ppss doesn't think those are
comment characters (and I have to admit it has a point). At first I
thought that was an issue but I'm no longer convinced of that.
TODO Factor this out to a standalone package, because it isn't specific to
aggressive-fill-paragraph."
(when (afp-inside-comment?)
(save-excursion
(let ((comment-start-pos)
(comment-end-pos))
;; Move to beginning of current comment. In a multiline comment block,
;; this puts point directly after the comment starter - in a single-line
;; comment block, it does the same, but since each line of the block is
;; considered an independent comment, you may be in the middle of the
;; comment.
;;
;; However, since we're at *a* comment start either way...
(goto-char (comment-beginning))
;; ...comment-forward with a negative buffer-size get us all the way
;; back to the first non-comment-or-whitespace character before the
;; comment block we were in to start with.
(forward-comment (* -1 (buffer-size)))
;; At this point, if we search to the first non-whitespace character
;; (including newlines) then go back one character, we should be at the
;; actual start of the current comment.
(re-search-forward "[^[:space:]\n\r]")
(backward-char)
(setq comment-start-pos (point))
;; Finally, we can now use forward-comment to move to the first
;; non-whitespace-or-comment character *after* the current
;; comment-block.
(forward-comment (buffer-size))
;; ...which means searching backward for the first non-whitespace
;; character takes us to the comment-closer (and we have to move
;; forward a char afterwards so we're actually after it).
(re-search-backward "[^[:space:]\n\r]")
(forward-char)
(setq comment-end-pos (point))
;; Return the comment's start and end positions.
(list comment-start-pos comment-end-pos)))))
(defun afp-comment-only-mode? ()
(apply #'derived-mode-p afp-fill-comments-only-mode-list))
(defun afp-current-line ()
(buffer-substring-no-properties (point-at-bol) (point-at-eol)))
;; Functions for testing conditions to suppress fill-paragraph
(defun afp-point-in-blank-lines? ()
"Return true if point is on a blank line.
This is a workaround to avoid filling of the next paragraph after
deleting the current one, because in its current state the super
aggressive fill winds up filling the next paragraph right after a
delete of the current one, which is not what we want.
TODO Look for a cleaner solution. This is only needed after
deletion, so it definitely feels like a workaround."
;; TODO Rename this as afp-point-on-blank-line? if this logic works out.
(looking-at "^$"))
(defun afp-in-email-headers? ()
(and (derived-mode-p 'message-mode)
(save-excursion (search-forward "--text follows this line--" nil t))))
(defun afp-outside-comment-and-comment-only-mode? ()
(and (afp-comment-only-mode?) (afp-outside-comment?)))
(defun afp-markdown-inside-code-block? ()
"""Basic test for indented code blocks in markdown."""
(and (string-equal major-mode "markdown-mode")
(string-match-p "^ " (afp-current-line))))
(defun afp-start-of-paragraph? ()
"Return non-nil if we are starting a new paragraph in a comment.
In programming modes that don't include a trailing space as part
of the fill prefix (e.g. when it's just '#', not '# '), this
makes it possible to start new paragraphs while
`afp-fill-on-self-insert' is non-nil.
Without this function in afp-suppress-fill-pfunction-list, it's
annoying to do that, because pressing the spacebar to add the
leading space manually results in a fill, deleting the new lines.
TODO Figure out if this is just a hack. The problem lies in how
we treat space characters while writing comments, and I'm not
convinced this is a sane solution."
(string-match-p (concat "^\\s-*" comment-start "\\s-*$") (afp-current-line)))
(defun afp-in-bulleted-list? ()
"Guess whether we are editing a bulleted list.
Tries to handle comments and regular text, which may be a poor decision.
TODO: handle modes with multi-line comment syntaxes correctly.
This does not work in C-style comments."
(if (afp-inside-comment?)
;; TODO: extend to match any line in paragraph
(string-match-p (concat "^\\s-*" comment-start "\\s-*[-\\*\\+]")
(afp-current-line))
(string-match-p (concat "^\\s-*[-\\*\\+]") (afp-current-line))))
(defun afp-in-formatted-paragraph? ()
"Return non-nil if we are in a paragraph that looks hand-formatted.
The current heuristic is just 'Does it have some lines that are
significantly narrower than the fill-column?'
There may be a better one awaiting discovery."
(save-excursion
(forward-paragraph)
(let* ((end (point))
(start (progn (backward-paragraph)
(point)))
(lengths))
(narrow-to-region start end)
;; Line length calculation code yanked from
;; https://emacs.stackexchange.com/a/17848/351
;;
;; It would be better to ignore lines that are just a fill-prefix, as
;; empty comment lines don't tell you much about whether text is being
;; formatted.
(while (not (eobp))
(push (- (line-end-position)
(line-beginning-position))
lengths)
(forward-line))
(widen)
;; FIXME Remove dependency on dash.el? Just trying to get this working
;; for now.
(and (> (length lengths) 1)
;; Are all lines other than the last one more than 10 characters shy
;; of fill-column? The last line of a paragraph tends to be a widow,
;; and thus very short, which is why we ignore it.
;;
;; This logic does not work in source code comments, at least not in
;; sh-mode or emacs-lisp-mode. Looks like I'd need a
;; forward/backward paragraph that accounted for fill-prefixes when
;; computing paragraph boundaries.
;;
;; ...it's peculiar that fill-paragraph works in comments but
;; forward-paragraph and backward-paragraph don't.
(-all? (lambda (x)
(< x (- fill-column 10)))
;; Ignore the last line, since it's usually a widow.
(-slice lengths 0 -1))
;; FIXME Make this logic work in comments. For the moment I'm
;; disabling it because it seems to cause difficulties if I don't.
(not (afp-inside-comment?))))))
;; Org mode tables have their own filling behaviour which results in the
;; cursor being moved to the start of the table element, which is no good
;; for us! See issue #6.
(require 'org)
(declare-function org-element-type "org-element" element)
(defun afp-in-org-table? ()
(interactive)
(and (derived-mode-p 'org-mode)
(or (eql (org-element-type (org-element-at-point)) 'table)
(eql (org-element-type (org-element-at-point)) 'table-row))))
(defcustom afp-fill-on-self-insert
nil
"If non-nil, `afp-fill-paragraph' fills after a character is inserted by
typing it directly.
This is in contrast to the default behavior, which is to fill only after
characters in `afp-fill-keys' are typed."
:group 'aggressive-fill-paragraph)
(defcustom afp-fill-after-functions
nil
"A list of functions that should fill the paragraph after running.
Note that `delete-region' will have no effect if entered here - see
`afp-advise-filled-functions' for an explanation of why."
:group 'aggressive-fill-paragraph
:type '(repeat function))
(defcustom afp-suppress-fill-pfunction-list
(list
#'afp-markdown-inside-code-block?
#'afp-point-in-blank-lines?
#'afp-start-of-paragraph?
#'afp-in-email-headers?
#'afp-in-bulleted-list?
#'afp-in-formatted-paragraph?
#'afp-in-org-table?
#'afp-outside-comment-and-comment-only-mode?
)
"List of predicate functions of no arguments, if any of these
functions returns false then paragraphs will not be
automatically filled."
:group 'aggressive-fill-paragraph)
(defcustom afp-fill-comments-only-mode-list
(list 'emacs-lisp-mode 'sh-mode 'python-mode 'js-mode)
;; TODO Maybe throw prog-mode in as a default? Not all modes need this, but a
;; whole lot do, and I don't know that it would hurt any of them.
;;
;; ...other than ones that have slow fill logic, I guess. I think I've seen
;; the approach I'm taking slag php-mode.
"List of major modes in which only comments should be filled."
:group 'aggressive-fill-paragraph)
(defcustom afp-fill-keys
(list ?\ ?.)
"List of keys after which to fill paragraph."
:group 'agressive-fill-paragraph)
;; The main functions
;; TODO Rework this to be afp-only-fill-strings-and-comments. Some languages
;; support multi-line strings, and even in ones that don't, you could write a
;; fill function that handles concatenation for you, making the writing
;; experience a lot more streamlined...
(defun afp-only-fill-comments (&optional justify)
"Replacement fill-paragraph function which only fills comments
and leaves everything else alone."
(when (afp-inside-comment?)
;; Some major-modes have fill logic that can fill outside of comments even
;; when triggered inside a comment, which is unbelievably inconvenient for
;; aggressive-fill-mode.
;;
;; So, we're narrowing the buffer to just the actual comment, to prevent
;; such modes from mangling our code.
;;
;; Narrowing is a bit subtle, so per the docs we first save-excursion then
;; save-restriction:
;;
;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Narrowing.html
;;
;; ...on further review it looks like fill-comment-paragraph is probably
;; supposed to be doing this narrowing itself, and is just buggy. Read its
;; code and see the relevant comments (more notes on this in todo.txt).
;;
;; If I get that fixed, I can probably ditch all this clever logic I've
;; written myself. That's what I get for not actually reading
;; fill-comment-paragraph.
;;
;; Note to self: just because it doesn't do what you want does not mean it
;; was not intended to.
(save-excursion
(save-restriction
(let* ((comment-region (afp-get-comment-bounds))
(start (first comment-region))
(end (second comment-region)))
(narrow-to-region start end)
(fill-comment-paragraph justify))))
;; Having to indent region for the comment we're currently editing feels
;; stupid, but without it wraps triggered in the first line of a comment
;; get indented wrongly if the comment does not begin in column 0, because
;; in the narrowed region the first line *does* begin in column 0, even
;; though the following lines don't.
;;
;; There's probably a smarter way to deal with it, but I'm not worrying
;; about it for now as I've discovered that fill-comment-paragraph should
;; probably be taking care of all of this for us regardless, so there's no
;; point in trying to fix this fine detail.
(apply #'indent-region (afp-get-comment-bounds)))
;; returning true says we are done with filling, don't fill anymore
t)
(defun afp-suppress-fill? ()
"Check all functions in afp-suppress-fill-pfunction-list"
(-any? #'funcall afp-suppress-fill-pfunction-list))
;; Tell the byte compiler that these functions exist
(declare-function ess-roxy-entry-p "ess-roxy" nil)
(declare-function ess-roxy-fill-field "ess-roxy" nil)
(defun afp-ess-fill-comments ()
"Fill comments in ess-mode (for R and related languages),
taking care with special cases for documentation comments."
;; Make sure we have the required libraries (this function is only run
;; when (derived-mode-p 'ess-mode) so we should!)
(require 'ess-mode)
(require 'ess-roxy)
(if (ess-roxy-entry-p)
(ess-roxy-fill-field)
(afp-only-fill-comments)))
(defun afp-choose-fill-function ()
"Select which fill paragraph function to use"
(cond
;; In certain modes it is better to use afp-only-fill-comments to avoid
;; strange behaviour in code.
;;
;; I have commented this out and hacked up an implementation of the
;; only-fill-comments behavior that works in js2-mode based on a suppression
;; function, because this logic overrides the use of fill-paragraph-function
;; in js2-mode, which means that /*-style comments are not filled correctly
;; (unless you run fill-paragraph by hand).
;;
;; In some sense doing this via a suppression function is more elegant.
;;
;; However, I'm not confident that it has the desired behavior in all the
;; modes that rely on afp-fill-comments-only-mode-list. Will have to figure
;; that out. Only triggering a fill inside a comment may not be the same
;; thing as only filling the comment - I can imagine a (poorly-written?)
;; fill function that fills both the comment and the code immediately
;; following it.
;;
((apply #'derived-mode-p afp-fill-comments-only-mode-list)
#'afp-only-fill-comments)
;; For python we could also do something with let-binding
;; python-fill-paren-function so that code is left alone. This would
;; allow docstrings to be filled, but unfortunately filling of strings
;; and docstrings are both handled by the same chunk of code, so even
;; normal strings would be filled.
((derived-mode-p 'ess-mode) #'afp-ess-fill-comments)
;; Use the buffer local fill function if it's set
((not (null fill-paragraph-function)) fill-paragraph-function)
;; Otherwise just use the default one
(t #'fill-paragraph)))
(defun afp-fill-paragraph (&rest args)
"If this mode is active, fill a paragraph with the appropriate fill function.
Primarily intended for use as advice to commonly-used functions like
`kill-region' and `yank', to keep things properly filled all the time.
Note, however, that `delete-region' cannot be consistently advised.
See `afp-advise-filled-functions' for a discussion of why."
(when (and aggressive-fill-paragraph-mode (not (afp-suppress-fill?)))
(funcall (afp-choose-fill-function))))
(defun aggressive-fill-paragraph-post-self-insert-function ()
"Fill paragraph when space is inserted and fill is not disabled
for any reason."
(when (and (or (and afp-fill-on-self-insert
;; do not fill after whitespace, so that making new
;; paragraphs always works. Just a dumb hack to make my
;; life a little better - still need to think more about
;; how best to do this.
(not (-contains? '(?\n ?\s ?\t) last-command-event)))
(-contains? afp-fill-keys last-command-event))
(not (afp-suppress-fill?)))
;; If the new character is whitespace, delete it before filling and
;; reinserting the characters. This works around cases where filling
;; removes whitespace.
;;
;; TODO Find more robust way to check "is it whitespace". There must be one
;; built into Emacs.
(when (memq last-command-event '(?\s ?\t))
(backward-delete-char 1))
(afp-fill-paragraph)
(when (memq last-command-event '(?\s ?\t))
(insert last-command-event))))
;; Minor mode set up
;;;###autoload
(define-minor-mode aggressive-fill-paragraph-mode
"Toggle automatic paragraph fill when spaces are inserted in comments."
:global nil
:group 'electricity
(if aggressive-fill-paragraph-mode
(add-hook 'post-self-insert-hook
#'aggressive-fill-paragraph-post-self-insert-function nil t)
(remove-hook 'post-self-insert-hook
#'aggressive-fill-paragraph-post-self-insert-function t)))
(defun afp-advise-filled-functions ()
"Advise each function in `afp-fill-after-functions' to fill after running.
This makes it possible to work like you're in a word processor, by
having deletes and pastes trigger filling.
Note, however, that advising `delete-region' does not work reliably,
because advice cannot be applied to native functions with their
own bytecode operation, at least not in the face of byte-compiled
elisp:
http://nullprogram.com/blog/2013/01/22
The obvious workaround I for this is re-implementing
`delete-region' in Emacs Lisp, as it would then be an advisable
function even in byte-compiled code.
However, that sounds like crazy talk.
A smarter workaround would be to prevent byte-compilation of
`delete-region', or more generally functions in
`afp-fill-after-functions', as we know we want to advise them."
(dolist (target-function afp-fill-after-functions)
(advice-add target-function :after #'afp-fill-paragraph)))
(defun afp-set-default-fill-after-functions ()
"Set `afp-fill-after-functions' to the standard values for
word-processor-style filling.
TODO Actually install the hooks after this function is called?
It's mainly intended as an easy setup aid, so that might be the
smart thing to do."
(setq afp-fill-after-functions '(backward-delete-char
backward-delete-char-untabify
kill-region
yank
yank-pop)))
;;;###autoload
(defun afp-setup-recommended-hooks ()
"Install hooks to enable aggressive-fill-paragraph-mode in recommended major modes."
(interactive)
(add-hook 'text-mode-hook #'aggressive-fill-paragraph-mode)
(add-hook 'prog-mode-hook #'aggressive-fill-paragraph-mode))
;; FIXME Remove this. Just here for debugging some issues with js2-mode.
;;
;; So far it doesn't reproduce the problem, but if you manually type each
;; character in the (insert) call in an empty js2-mode buffer, you'll see the
;; first leading space get swallowed. I don't know why and I would very much
;; like to.
(defun afp-js-comment-fill-suckage-test-case ()
(interactive)
(switch-to-buffer "testing.js")
(js2-mode)
(delete-region (point-min) (point-max))
(insert "var foo;
// This is a function and it does not work so well.")
(self-insert-command 1 " ")
(fill-paragraph))
(provide 'aggressive-fill-paragraph)
;;; aggressive-fill-paragraph.el ends here