summary refs log tree commit diff stats
path: root/lisp/+org.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/+org.el')
-rw-r--r--lisp/+org.el816
1 files changed, 0 insertions, 816 deletions
diff --git a/lisp/+org.el b/lisp/+org.el deleted file mode 100644 index dc0ce1b..0000000 --- a/lisp/+org.el +++ /dev/null
@@ -1,816 +0,0 @@
1;;; +org.el -*- lexical-binding: t; -*-
2
3;;; Code:
4
5(require 'el-patch)
6(require 'org)
7(require 'org-element)
8(require 'ox)
9
10;;; org-return-dwim - [[https://github.com/alphapapa/unpackaged.el][unpackaged]] and [[http://kitchingroup.cheme.cmu.edu/blog/2017/04/09/A-better-return-in-org-mode/][kitchin]]
11
12(defun +org-element-descendant-of (type element)
13 "Return non-nil if ELEMENT is a descendant of TYPE.
14TYPE should be an element type, like `item' or `paragraph'.
15ELEMENT should be a list like that returned by `org-element-context'."
16 ;; MAYBE: Use `org-element-lineage'.
17 (when-let* ((parent (org-element-property :parent element)))
18 (or (eq type (car parent))
19 (+org-element-descendant-of type parent))))
20
21(defun +org-return-dwim (&optional prefix)
22 "A helpful replacement for `org-return'. With PREFIX, call `org-return'.
23
24On headings, move point to position after entry content. In
25lists, insert a new item or end the list, with checkbox if
26appropriate. In tables, insert a new row or end the table."
27 (interactive "P")
28 ;; Auto-fill if enabled
29 (when auto-fill-function
30 (if (listp auto-fill-function)
31 (dolist (func auto-fill-function)
32 (funcall func))
33 (funcall auto-fill-function)))
34 (if prefix
35 ;; Handle prefix args
36 (pcase prefix
37 ('(4) (newline))
38 ('(16) (newline 2))
39 ;; this is ... not ideal. but whatever.
40 (_ (newline prefix)))
41 (cond
42 ;; Act depending on context around point.
43 ((and org-return-follows-link
44 (eq 'link (car (org-element-context))))
45 ;; Link: Open it.
46 (org-open-at-point-global))
47
48 ((org-at-heading-p)
49 ;; Heading: Move to position after entry content.
50 ;; NOTE: This is probably the most interesting feature of this function.
51 (let ((heading-start (org-entry-beginning-position)))
52 (goto-char (org-entry-end-position))
53 (cond ((and (org-at-heading-p)
54 (= heading-start (org-entry-beginning-position)))
55 ;; Entry ends on its heading; add newline after
56 (end-of-line)
57 (insert "\n\n"))
58 (t
59 ;; Entry ends after its heading; back up
60 (forward-line -1)
61 (end-of-line)
62 (when (org-at-heading-p)
63 ;; At the same heading
64 (forward-line)
65 (insert "\n")
66 (forward-line -1))
67 (while (not
68 (looking-back
69 (rx (repeat 3 (seq (optional blank) "\n")))
70 nil))
71 (insert "\n"))
72 (forward-line -1)))))
73
74 ((org-at-item-checkbox-p)
75 ;; Checkbox: Insert new item with checkbox.
76 (org-insert-todo-heading nil))
77
78 ((org-in-item-p)
79 ;; Plain list
80 (let* ((context (org-element-context))
81 (first-item-p (eq 'plain-list (car context)))
82 (itemp (eq 'item (car context)))
83 (emptyp (or
84 ;; Empty list item (regular)
85 (eq (org-element-property :contents-begin context)
86 (org-element-property :contents-end context))
87 ;; Empty list item (definition)
88 ;; This seems to work, with minimal testing. -- 2022-02-17
89 (looking-at " *::")))
90 (item-child-p
91 (+org-element-descendant-of 'item context)))
92 ;; The original function from unpackaged just tested the (or ...) test
93 ;; in this cond, in an if. However, that doesn't auto-end nested
94 ;; lists. So I made this form a cond and added the (and...) test in
95 ;; the first position, which is clunky (the delete-region... stuff
96 ;; comes twice) and might not be needed. More testing, obviously, but
97 ;; for now, it works well enough.
98 (cond ((and itemp emptyp)
99 (delete-region (line-beginning-position) (line-end-position))
100 (insert "\n"))
101 ((or first-item-p
102 (and itemp (not emptyp))
103 item-child-p)
104 (org-insert-item))
105 (t (delete-region (line-beginning-position) (line-end-position))
106 (insert "\n")))))
107
108 ((when (fboundp 'org-inlinetask-in-task-p)
109 (org-inlinetask-in-task-p))
110 ;; Inline task: Don't insert a new heading.
111 (org-return))
112
113 ((org-at-table-p)
114 (cond ((save-excursion
115 (beginning-of-line)
116 ;; See `org-table-next-field'.
117 (cl-loop with end = (line-end-position)
118 for cell = (org-element-table-cell-parser)
119 always (equal (org-element-property :contents-begin cell)
120 (org-element-property :contents-end cell))
121 while (re-search-forward "|" end t)))
122 ;; Empty row: end the table.
123 (delete-region (line-beginning-position) (line-end-position))
124 (org-return))
125 (t
126 ;; Non-empty row: call `org-return'.
127 (org-return))))
128 (t
129 ;; All other cases: call `org-return'.
130 (org-return)))))
131
132(defun +org-table-copy-down (n)
133 "Call `org-table-copy-down', or `org-return' outside of a table.
134N is passed to the functions."
135 (interactive "p")
136 (if (org-table-check-inside-data-field 'noerror)
137 (org-table-copy-down n)
138 (+org-return-dwim n)))
139
140;;; org-fix-blank-lines - unpackaged.el
141
142(defun +org-fix-blank-lines (&optional prefix)
143 "Ensure blank lines around headings.
144Optional PREFIX argument operates on the entire buffer.
145Drawers are included with their headings."
146 (interactive "P")
147 (let ((org-element-use-cache nil))
148 (org-map-entries (lambda ()
149 (let ((beg (org-entry-beginning-position))
150 (end (org-entry-end-position)))
151 (org-with-wide-buffer
152 ;; `org-map-entries' narrows the buffer, which
153 ;; prevents us from seeing newlines before the
154 ;; current heading, so we do this part widened.
155 (while (not (looking-back "\n\n" nil))
156 ;; Insert blank lines before heading.
157 (insert "\n")))
158
159 ;; Insert blank lines before entry content
160 (forward-line)
161 (while (and (org-at-planning-p)
162 (< (point) (point-max)))
163 ;; Skip planning lines
164 (forward-line))
165 (while (re-search-forward
166 org-drawer-regexp end t)
167 ;; Skip drawers. You might think that
168 ;; `org-at-drawer-p' would suffice, but for
169 ;; some reason it doesn't work correctly when
170 ;; operating on hidden text. This works, taken
171 ;; from `org-agenda-get-some-entry-text'.
172 (re-search-forward "^[ \t]*:END:.*\n?" end t)
173 (goto-char (match-end 0)))
174 (unless (or (= (point) (point-max))
175 (org-at-heading-p)
176 (looking-at-p "\n"))
177 (insert "\n"))))
178 t
179 (if prefix
180 nil
181 'tree))))
182
183;;; org-count-words
184
185(defun +org-count-words-stupidly (start end &optional limit)
186 "Count words between START and END, ignoring a lot.
187
188Since this function is, for some reason, pricy, the optional
189parameter LIMIT sets a word limit at which to stop counting.
190Once the function hits that number, it'll return -LIMIT
191instead of the true count."
192 (interactive (list nil nil))
193 (cond ((not (called-interactively-p 'any))
194 (let ((words 0)
195 (continue t))
196 (save-excursion
197 (save-restriction
198 (narrow-to-region start end)
199 (goto-char (point-min))
200 (while (and continue
201 (< (point) (point-max)))
202 (cond
203 ;; Ignore comments
204 ((or (org-at-comment-p)
205 (org-in-commented-heading-p))
206 (forward-line))
207 ;; Ignore headings
208 ((or (org-at-heading-p))
209 (forward-line))
210 ;; Ignore property and log drawers
211 ((or (looking-at org-drawer-regexp)
212 (looking-at org-clock-drawer-re))
213 (search-forward ":END:" nil :noerror)
214 (forward-line))
215 ;; Ignore DEADLINE and SCHEDULED keywords
216 ((or (looking-at org-deadline-regexp)
217 (looking-at org-scheduled-regexp)
218 (looking-at org-closed-time-regexp))
219 (forward-line))
220 ;; Ignore tables
221 ((org-at-table-p) (forward-line))
222 ;; Ignore hyperlinks, but count the descriptions
223 ((looking-at org-link-bracket-re)
224 (when-let ((desc (match-string-no-properties 5)))
225 (save-match-data
226 (setq words (+ words
227 (length (remove ""
228 (org-split-string
229 desc "\\W")))))))
230 (goto-char (match-end 0)))
231 ;; Ignore source blocks
232 ((org-in-src-block-p) (forward-line))
233 ;; Ignore blank lines
234 ((looking-at "^$")
235 (forward-line))
236 ;; Count everything else
237 (t
238 ;; ... unless it's in a few weird contexts
239 (let ((contexts (org-context)))
240 (cond ((or (assoc :todo-keyword contexts)
241 (assoc :priority contexts)
242 (assoc :keyword contexts)
243 (assoc :checkbox contexts))
244 (forward-word-strictly))
245
246 (t (setq words (1+ words))
247 (if (and limit
248 (> words limit))
249 (setq words (- limit)
250 continue nil))
251 (forward-word-strictly)))))))))
252 words))
253 ((use-region-p)
254 (message "%d words in region"
255 (+org-count-words-stupidly (region-beginning)
256 (region-end))))
257 (t
258 (message "%d words in buffer"
259 (+org-count-words-stupidly (point-min)
260 (point-max))))))
261
262;;; org-insert-link-dwim - https://xenodium.com/emacs-dwim-do-what-i-mean/
263
264(defun +org-insert--get-title-and-headings (url)
265 "Retrieve title and headings from URL.
266Return as a list."
267 (with-current-buffer (url-retrieve-synchronously url)
268 (let ((dom (libxml-parse-html-region (point-min) (point-max))))
269 (cl-remove-if
270 (lambda (i) (string= i ""))
271 (apply #'append (mapcar (lambda (tag)
272 (mapcar #'dom-text
273 (dom-by-tag dom tag)))
274 '(title h1 h2 h3 h4 h5 h6)))))))
275
276(defun +org-insert-link-dwim (&optional interactivep)
277 "Like `org-insert-link' but with personal dwim preferences."
278 (interactive '(t))
279 (let* ((point-in-link (org-in-regexp org-link-any-re 1))
280 (clipboard-url (when (string-match-p
281 (rx (sequence bos
282 (or "http"
283 "gemini"
284 "gopher"
285 "tel"
286 "mailto")))
287 (current-kill 0))
288 (current-kill 0)))
289 (region-content (when (region-active-p)
290 (buffer-substring-no-properties (region-beginning)
291 (region-end))))
292 (org-link (when (and clipboard-url (not point-in-link))
293 (org-link-make-string
294 (string-trim clipboard-url)
295 (or region-content
296 (let ((clipboard-headings
297 (+org-insert--get-title-and-headings clipboard-url)))
298 (read-string "title (edit): "
299 (completing-read
300 "title: " clipboard-headings
301 nil nil nil nil (car clipboard-headings)))))))))
302 (if interactivep
303 (cond ((and region-content clipboard-url (not point-in-link))
304 (delete-region (region-beginning) (region-end))
305 (insert org-link))
306 ((and clipboard-url (not point-in-link))
307 (insert org-link))
308 (t
309 (call-interactively 'org-insert-link)))
310 org-link)))
311
312;;; Navigate headings with widening
313
314(defun +org-next-heading-widen (arg)
315 "Find the ARGth next org heading, widening if necessary."
316 (interactive "p")
317 (let ((current-point (point))
318 (point-target (if (> arg 0) (point-max) (point-min))))
319 (org-next-visible-heading arg)
320 (when (and (buffer-narrowed-p)
321 (= (point) point-target)
322 (or (and (> arg 0))
323 (and (< arg 0)
324 (= (point) current-point))))
325 (widen)
326 (org-next-visible-heading arg))))
327
328(defun +org-previous-heading-widen (arg)
329 "Find the ARGth previous org heading, widening if necessary."
330 (interactive "p")
331 (+org-next-heading-widen (- arg)))
332
333;;; Hooks & Advice
334
335(defvar +org-before-save-prettify-buffer t
336 "Prettify org buffers before saving.")
337
338(put '+org-before-save-prettify-buffer 'safe-local-variable #'booleanp)
339
340(defun +org-before-save@prettify-buffer ()
341 (when +org-before-save-prettify-buffer
342 (save-mark-and-excursion
343 (+org-unsmartify)
344 (+org-fix-blank-lines t)
345 (org-align-tags t)
346 (org-hide-drawer-all)
347 (when (buffer-narrowed-p)
348 (goto-char (point-min))
349 (forward-line 1)
350 (org-narrow-to-subtree)))))
351
352(defun +org-delete-backward-char (N)
353 "Keep tables aligned while deleting N characters backward.
354When deleting backwards, in tables this function will insert
355whitespace in front of the next \"|\" separator, to keep the
356table aligned. The table will still be marked for re-alignment
357if the field did fill the entire column, because, in this case
358the deletion might narrow the column."
359 (interactive "p")
360 (save-match-data
361 (org-check-before-invisible-edit 'delete-backward)
362 (if (and (= N 1)
363 (not overwrite-mode)
364 (not (org-region-active-p))
365 (not (eq (char-before) ?|))
366 (save-excursion (skip-chars-backward " \t") (not (bolp)))
367 (looking-at-p ".*?|")
368 (org-at-table-p))
369 (progn (forward-char -1) (org-delete-char 1))
370 (backward-delete-char-untabify N)
371 (org-fix-tags-on-the-fly))))
372
373;;; Smarter {super,sub}scripts
374;; https://old.reddit.com/r/emacs/comments/qzlzm0/what_are_your_top_key_bindings_rebindings_minor/hmwyhm3/
375;; I don't use this currently because I found out about
376;; `org-pretty-entities-include-sub-superscripts', which really does exactly
377;; what I wanted.
378
379(defface +org-script-markers '((t (:inherit shadow)))
380 "Face to be used for sub/superscripts markers i.e., ^, _, {, }.")
381
382;; Hiding the super and subscript markers is extremely annoying
383;; since any remotely complex equation becomes a chore. And leaving
384;; it not raised is jarring to the eye. So this fontifies the
385;; buffer just like how auctex does -- use a muted colour to
386;; highlight the markup and raise the script.
387(defun +org-raise-scripts (limit)
388 "Differences from `org-raise-scripts' are:
389
390- It doesn't actually hide the markup used for super and subscript.
391- It uses a custom face to highlight the markup: +org-script-markers.
392- It doesn't require `org-pretty-entities' to be t."
393 (when (and org-pretty-entities-include-sub-superscripts
394 (re-search-forward
395 (if (eq org-use-sub-superscripts t)
396 org-match-substring-regexp
397 org-match-substring-with-braces-regexp)
398 limit t))
399 (let* ((pos (point)) table-p comment-p
400 (mpos (match-beginning 3))
401 (emph-p (get-text-property mpos 'org-emphasis))
402 (link-p (get-text-property mpos 'mouse-face))
403 (keyw-p (eq 'org-special-keyword (get-text-property mpos 'face))))
404 (goto-char (point-at-bol))
405 (setq table-p (looking-at-p org-table-dataline-regexp)
406 comment-p (looking-at-p "^[ \t]*#[ +]"))
407 (goto-char pos)
408 ;; Handle a_b^c
409 (when (member (char-after) '(?_ ?^)) (goto-char (1- pos)))
410 (unless (or comment-p emph-p link-p keyw-p)
411 (put-text-property (match-beginning 3) (match-end 0)
412 'display
413 (if (equal (char-after (match-beginning 2)) ?^)
414 ;; (nth (if table-p 3 1) org-script-display)
415 (nth 3 org-script-display)
416 ;; (nth (if table-p 2 0) org-script-display)
417 (nth 2 org-script-display)))
418 (put-text-property (match-beginning 2) (match-end 2)
419 'face '+org-script-markers)
420 (when (and (eq (char-after (match-beginning 3)) ?{)
421 (eq (char-before (match-end 3)) ?}))
422 (put-text-property (match-beginning 3) (1+ (match-beginning 3))
423 'face '+org-script-markers)
424 (put-text-property (1- (match-end 3)) (match-end 3)
425 'face '+org-script-markers)))
426 t)))
427
428;; Extra link types
429
430(defun +org-tel-open (number _)
431 "Notify the user of what phone NUMBER to call."
432 (message "Call: %s" number))
433
434(defun +org-sms-open (number _)
435 "Notify the user of what phone NUMBER to text."
436 (message "SMS: %s" number))
437
438;; Make a horizontal rule!
439
440(defun +org-horizontal-rule ()
441 "Make a horizontal rule after the current line."
442 (interactive nil org-mode)
443 (unless (eq (line-beginning-position) (line-end-position))
444 (end-of-line)
445 (newline))
446 (dotimes (_ fill-column)
447 (insert "-")))
448
449;; Follow links, DWIM style
450
451(defun +org-open-at-point-dwim (&optional arg)
452 "Open thing at point, or if there isn't something, list things."
453 (interactive "P")
454 (save-excursion
455 (let* ((this-char-type (org-element-type (org-element-context)))
456 (prev-char-type (ignore-errors
457 (save-excursion
458 (backward-char)
459 (org-element-type (org-element-context)))))
460 (types '(citation citation-reference clock comment comment-block
461 footnote-definition footnote-reference headline
462 inline-src-block inlinetask keyword link
463 node-property planning src-block timestamp))
464 (type this-char-type))
465 (when (and (memq this-char-type types) (memq prev-char-type types))
466 (backward-char)
467 (setq type prev-char-type)) ; what the fuckckckckck
468 ;; Okay, so this ^ is pretty janky and doesn't /really/ work that well,
469 ;; especially on DEADLINE (and probably SCHEDULED) lines. However, since
470 ;; I really just want to open the list of URLs /most of the time/, I'm
471 ;; fixing it like this instead.
472 (unless (and (memq type types)
473 (ignore-errors (org-open-at-point arg)
474 t))
475 (while (not
476 (progn
477 (org-back-to-heading)
478 (car (org-offer-links-in-entry (current-buffer) (point) 1))))
479 (org-up-heading-all 1))
480 (org-open-at-point arg)))))
481
482;;; Open local HTML files with `browse-url'
483
484(defun +org-open-html (file-path link-string)
485 "Open FILE-PATH with `browse-url'.
486This function is intended to use with `org-file-apps'. See the
487 documentation of that function for a description of the two
488 arguments here, FILE-PATH and LINK-STRING."
489 (message "Opening %s (%s)..." file-path link-string)
490 (browse-url file-path))
491
492(defun +org-insert-horizontal-rule (prefix)
493 "Insert a horizontal rule (-----) after the current line.
494With PREFIX, insert before the current line."
495 (interactive "P")
496 (if prefix
497 (move-beginning-of-line nil)
498 (move-end-of-line nil)
499 (forward-line 1))
500 (insert "-----\n"))
501
502;;; Make code snippets in org-mode easier to type
503;; http://mbork.pl/2022-01-17_Making_code_snippets_in_Org-mode_easier_to_type
504
505(defun +org-insert-backtick ()
506 "Insert a backtick using `org-self-insert-command'."
507 (interactive)
508 (setq last-command-event ?`)
509 (call-interactively #'org-self-insert-command))
510
511(defvar-local +org-insert-tilde-language nil
512 "Default language name in the current Org file.
513If nil, `org-insert-tilde' after 2 tildes inserts an \"example\"
514block. If a string, it inserts a \"src\" block with the given
515language name.")
516
517(defun +org-insert-tilde ()
518 "Insert a tilde using `org-self-insert-command'."
519 (interactive)
520 (if (string= (buffer-substring-no-properties (- (point) 3) (point))
521 "\n~~")
522 (progn (delete-char -2)
523 (if +org-insert-tilde-language
524 (insert (format "#+begin_src %s\n#+end_src"
525 +org-insert-tilde-language))
526 (insert "#+begin_example\n#+end_example"))
527 (forward-line -1)
528 (if (string= +org-insert-tilde-language "")
529 (move-end-of-line nil)
530 ;;(org-edit-special) ; Useful really only with splits.
531 ))
532 (setq last-command-event ?~)
533 (call-interactively #'org-self-insert-command)))
534
535;;; Better org faces
536;; see `org-emphasis-alist'
537
538(defface org-bold '((t (:weight bold)))
539 "Bold face in `org-mode' documents.")
540
541(defface org-italic '((t (:slant italic)))
542 "Italic face in `org-mode' documents.")
543
544(defface org-underline '((t (:underline t)))
545 "Underline face in `org-mode' documents.")
546
547(defface org-strikethrough '((t (:strike-through t)))
548 "Strike-through face for `org-mode' documents.")
549
550;; `org-verbatim' and `org-code' are apparently already things, so we skip them
551;; here.
552
553;;; Copy org trees as HTML
554
555;; Thanks to Oleh Krehel, via [[https://emacs.stackexchange.com/questions/54292/copy-results-of-org-export-directly-to-clipboard][this StackExchange question]].
556(defun +org-export-clip-to-html
557 (&optional async subtreep visible-only body-only ext-plist post-process)
558 "Export region to HTML, and copy it to the clipboard.
559Arguments ASYNC, SUBTREEP, VISIBLE-ONLY, BODY-ONLY, EXT-PLIST,
560and POST-PROCESS are passed to `org-export-to-file'."
561 (interactive) ; XXX: hould this be interactive?
562 (message "Exporting Org to HTML...")
563 (let ((org-tmp-file "/tmp/org.html"))
564 (org-export-to-file 'html org-tmp-file
565 async subtreep visible-only body-only ext-plist post-process)
566 (start-process "xclip" "*xclip*"
567 "xclip" "-verbose"
568 "-i" org-tmp-file
569 "-t" "text/html"
570 "-selection" "clipboard"))
571 (message "Exporting Org to HTML...done."))
572
573;; Specialized functions
574(defun +org-export-clip-subtree-to-html ()
575 "Export current subtree to HTML."
576 (interactive)
577 (+org-export-clip-to-html nil :subtree))
578
579;;; Unsmartify quotes and dashes and stuff.
580(defun +org-unsmartify ()
581 "Replace \"smart\" punctuation with their \"dumb\" counterparts."
582 (interactive)
583 (save-excursion
584 (goto-char (point-min))
585 (while (re-search-forward "[“”‘’–—]" nil t)
586 (let ((replace (pcase (match-string 0)
587 ((or "“" "”") "\"")
588 ((or "‘" "’") "'")
589 ("–" "--")
590 ("—" "---"))))
591 (replace-match replace nil nil)))))
592
593;;; go forward and backward in the tree, ~ cleanly ~
594;; https://stackoverflow.com/a/25201697/10756297
595
596(defun +org-show-next-heading-tidily ()
597 "Show next entry, keeping other entries closed."
598 (interactive)
599 (if (save-excursion (end-of-line) (outline-invisible-p))
600 (progn (org-show-entry) (show-children))
601 (outline-next-heading)
602 (unless (and (bolp) (org-on-heading-p))
603 (org-up-heading-safe)
604 (hide-subtree)
605 (user-error "Boundary reached"))
606 (org-overview)
607 (org-reveal t)
608 (org-show-entry)
609 (recenter-top-bottom)
610 (show-children)
611 (recenter-top-bottom 1)))
612
613(defun +org-show-previous-heading-tidily ()
614 "Show previous entry, keeping other entries closed."
615 (interactive)
616 (let ((pos (point)))
617 (outline-previous-heading)
618 (unless (and (< (point) pos) (bolp) (org-on-heading-p))
619 (goto-char pos)
620 (hide-subtree)
621 (user-error "Boundary reached"))
622 (org-overview)
623 (org-reveal t)
624 (org-show-entry)
625 (recenter-top-bottom)
626 (show-children)
627 (recenter-top-bottom 1)))
628
629;;; Make `org-flag-region' (which folds subtrees) recognize
630;; [[https://teddit.net/r/orgmode/comments/u3du0v/how_to_make_orgcycle_respect_and_always_show_the/][from u/yantar92]]
631
632;; (advice-add 'org-flag-region :around #'org-flag-region@unfold-page-breaks)
633(defun org-flag-region@unfold-page-breaks (oldfun from to flag &optional spec)
634 "ADVICE to unfold all the page-break lines inside a folded region."
635 (funcall oldfun from to flag spec)
636 (when (and flag (not (eq 'visible spec)))
637 (org-with-point-at from
638 (while (re-search-forward "\n\u000c\n" to t)
639 (org-flag-region (match-beginning 0) (match-end 0) t 'visible)))))
640
641;;; Emacs 28+: wrap on hyphens
642;; https://emacs.stackexchange.com/a/71342/37239
643
644(defcustom +org-category-table (let ((table (copy-category-table)))
645 (modify-category-entry ?- ?| table)
646 table)
647 "Character category table for `org-mode'."
648 :type 'sexp)
649
650(defun +org-wrap-on-hyphens ()
651 "Soft-wrap `org-mode' buffers on spaces and hyphens."
652 (set-category-table +org-category-table)
653 (setq-local word-wrap-by-category t))
654
655
656;;; Inhibit hooks on `org-agenda'
657;; It's really annoying when I call `org-agenda' and five hundred Ispell
658;; processes are created because I have `flyspell-mode' in the hook. This mode
659;; inhibits those hooks when entering the agenda, but runs them when opening the
660;; actual buffer.
661
662(defun +org-agenda-inhibit-hooks (fn &rest r)
663 "Advice to inhibit hooks when entering `org-agenda'."
664 (dlet ((org-mode-hook nil)) ; I'm not sure if `dlet' is strictly needed
665 (apply fn r)))
666
667(defvar-local +org-hook-has-run-p nil
668 "Whether `org-mode-hook' has run in the current buffer.")
669
670(defun +org-agenda-switch-run-hooks (&rest _)
671 "Advice to run `org-mode-hook' when entering org-mode.
672This should only fire when switching to a buffer from `org-agenda'."
673 (unless +org-hook-has-run-p
674 (run-mode-hooks 'org-mode-hook)
675 (setq +org-hook-has-run-p t)))
676
677(define-minor-mode +org-agenda-inhibit-hooks-mode
678 "Inhibit `org-mode-hook' when opening `org-agenda'."
679 :lighter ""
680 :global t
681 (if +org-agenda-inhibit-hooks-mode
682 (progn ; Enable
683 (advice-add 'org-agenda :around #'+org-agenda-inhibit-hooks)
684 (advice-add 'org-agenda-switch-to :after #'+org-agenda-switch-run-hooks))
685 (progn ; Disable
686 (advice-remove 'org-agenda #'+org-agenda-inhibit-hooks)
687 (advice-remove 'org-agenda-switch-to #'+org-agenda-switch-run-hooks))))
688
689
690;;; "Fix" `org-align-tags'
691
692(el-patch-defun org-align-tags (&optional all)
693 "Align tags in current entry.
694When optional argument ALL is non-nil, align all tags in the
695visible part of the buffer."
696 (let ((get-indent-column
697 (lambda ()
698 (let ((offset (el-patch-swap
699 (if (bound-and-true-p org-indent-mode)
700 (* (1- org-indent-indentation-per-level)
701 (1- (org-current-level)))
702 0)
703 0)))
704 (+ org-tags-column
705 (if (> org-tags-column 0) (- offset) offset))))))
706 (if (and (not all) (org-at-heading-p))
707 (org--align-tags-here (funcall get-indent-column))
708 (save-excursion
709 (if all
710 (progn
711 (goto-char (point-min))
712 (while (re-search-forward org-tag-line-re nil t)
713 (org--align-tags-here (funcall get-indent-column))))
714 (org-back-to-heading t)
715 (org--align-tags-here (funcall get-indent-column)))))))
716
717;;; Meta-return
718
719(defun +org-meta-return (&optional arg)
720 "Insert a new line, or wrap a region in a table.
721See `org-meta-return', but `+org-return-dwim' does most of the
722stuff I would want out of that function already.
723
724When called with a prefix ARG, will still unconditionally call
725`org-insert-heading'."
726 (interactive "P")
727 (org-fold-check-before-invisible-edit 'insert)
728 (or (run-hook-with-args-until-success 'org-metareturn-hook) ; Allow customizations
729 (call-interactively (cond (arg #'org-insert-heading)
730 ((org-at-table-p) #'org-table-wrap-region)
731 (t #'org-return)))))
732
733
734;;; move org archives to a dedicated file
735;; (defun +org-archive-monthwise (archive-file)
736;; (if (file-exists-p archive-file)
737;; (with-current-buffer (find-file-noselect archive-file)
738;; (let ((dir (file-name-directory (file-truename archive-file)))
739;; (prog (make-progress-reporter (format "Archiving from %s..." archive-file)))
740;; (keep-going t))
741;; (goto-char (point-min))
742;; (while keep-going
743;; (when-let* ((time (or (org-entry-get (point) "ARCHIVE_TIME")
744;; (org-get-deadline-time (point))))
745;; (parsed-time (and time
746;; (org-parse-time-string time)))
747;; (refile-target (format "%s%02d-%02d.org"
748;; dir
749;; (decoded-time-year parsed-time)
750;; (decoded-time-month parsed-time)))
751;; (title-str (format "#+title: Archive for %02d-%02d (%s)\n\n"
752;; (decoded-time-year parsed-time)
753;; (decoded-time-month parsed-time)
754;; (file-truename archive-file))))
755;; (unless (file-exists-p refile-target)
756;; (with-current-buffer (find-file-noselect refile-target)
757;; (insert title-str)
758;; (save-buffer)))
759;; (org-refile nil nil (list ""
760;; refile-target
761;; nil
762;; 0)))
763;; (progress-reporter-update prog)
764;; (org-next-visible-heading 1)
765;; (when (>= (point) (point-max))
766;; (setq keep-going nil)))))
767;; (message "Archive file %s does not exist!" archive-file)))
768
769
770;;; +org-toggle-view-emphasis
771;; I thought this function was already written somewhere...
772(defun +org-toggle-view-emphasis ()
773 "Toggle `org-hide-emphasis-markers' and redraw the buffer."
774 (interactive)
775 (setq-local org-hide-emphasis-markers (not org-hide-emphasis-markers))
776 (font-lock-update))
777
778
779;;; el-patch
780
781(el-patch-defun org-format-outline-path (path &optional width prefix separator)
782 "Format the outline path PATH for display.
783WIDTH is the maximum number of characters that is available.
784PREFIX is a prefix to be included in the returned string,
785such as the file name.
786SEPARATOR is inserted between the different parts of the path,
787the default is \"/\"."
788 (setq width (or width 79))
789 (setq path (delq nil path))
790 (unless (> width 0)
791 (user-error "Argument `width' must be positive"))
792 (setq separator (or separator "/"))
793 (let* ((org-odd-levels-only nil)
794 (fpath (concat
795 prefix (and prefix path separator)
796 (mapconcat
797 (lambda (s) (replace-regexp-in-string "[ \t]+\\'" "" s))
798 (cl-loop for head in path
799 for n from 0
800 collect (el-patch-swap
801 (org-add-props
802 head nil 'face
803 (nth (% n org-n-level-faces) org-level-faces))
804 head))
805 separator))))
806 (when (> (length fpath) width)
807 (if (< width 7)
808 ;; It's unlikely that `width' will be this small, but don't
809 ;; waste characters by adding ".." if it is.
810 (setq fpath (substring fpath 0 width))
811 (setf (substring fpath (- width 2)) "..")))
812 fpath))
813
814
815(provide '+org)
816;;; +org.el ends here