diff options
Diffstat (limited to 'lisp/+org.el')
-rw-r--r-- | lisp/+org.el | 816 |
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. | ||
14 | TYPE should be an element type, like `item' or `paragraph'. | ||
15 | ELEMENT 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 | |||
24 | On headings, move point to position after entry content. In | ||
25 | lists, insert a new item or end the list, with checkbox if | ||
26 | appropriate. 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. | ||
134 | N 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. | ||
144 | Optional PREFIX argument operates on the entire buffer. | ||
145 | Drawers 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 | |||
188 | Since this function is, for some reason, pricy, the optional | ||
189 | parameter LIMIT sets a word limit at which to stop counting. | ||
190 | Once the function hits that number, it'll return -LIMIT | ||
191 | instead 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. | ||
266 | Return 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. | ||
354 | When deleting backwards, in tables this function will insert | ||
355 | whitespace in front of the next \"|\" separator, to keep the | ||
356 | table aligned. The table will still be marked for re-alignment | ||
357 | if the field did fill the entire column, because, in this case | ||
358 | the 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'. | ||
486 | This 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. | ||
494 | With 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. | ||
513 | If nil, `org-insert-tilde' after 2 tildes inserts an \"example\" | ||
514 | block. If a string, it inserts a \"src\" block with the given | ||
515 | language 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. | ||
559 | Arguments ASYNC, SUBTREEP, VISIBLE-ONLY, BODY-ONLY, EXT-PLIST, | ||
560 | and 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. | ||
672 | This 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. | ||
694 | When optional argument ALL is non-nil, align all tags in the | ||
695 | visible 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. | ||
721 | See `org-meta-return', but `+org-return-dwim' does most of the | ||
722 | stuff I would want out of that function already. | ||
723 | |||
724 | When 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. | ||
783 | WIDTH is the maximum number of characters that is available. | ||
784 | PREFIX is a prefix to be included in the returned string, | ||
785 | such as the file name. | ||
786 | SEPARATOR is inserted between the different parts of the path, | ||
787 | the 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 | ||