Commit d20c3c95b2ed

Vincent Demeester <vincent@sbr.pm>
2025-04-07 16:00:56
tools/emacs: refine the way TAB works in org-mode
This is a bit "tangent" as it changes the default behavior but it makes more sense. See https://spepo.github.io/2025-03-29-the-tab-key-in-org-mode-reimagined.html Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 0ba8720
Changed files (2)
tools
tools/emacs/config/config-org.el
@@ -70,7 +70,10 @@
 	 ("C-c o a r" . vde/reload-org-agenda-files)
 	 ("C-c C-x i" . vde/org-clock-in-any-heading)
          ("C-c o s" . org-sort)
-         ("<f12>" . org-agenda))
+         ("<f12>" . org-agenda)
+	 (:map org-mode-map
+	  ("<tab>" . vde/org-tab)
+	  ("<backtab>" . vde/org-shifttab)))
   :hook (org-mode . vde/org-mode-hook)
   :custom
   ;; (org-reverse-note-order '((org-inbox-file . t) ;; Insert items on top of inbox
@@ -84,6 +87,7 @@
   (org-hide-emphasis-markers t)
   (org-pretty-entities t)
   (org-ellipsis "…")
+  (org-return-follows-link t)
   (org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "STARTED(s)" "IN-REVIEW(r)" "|" "DONE(d!)" "CANCELED(c@/!)")
                        (sequence "WAITING(w@/!)" "SOMEDAY(s)" "|" "CANCELED(c@/!)")
                        (sequence "IDEA(i)" "|" "CANCELED(c@/!)")))
tools/emacs/lisp/org-func.el
@@ -93,5 +93,101 @@ BEGIN and END are regexps which define the line range to use."
             (goto-char pos)
             (org-clock-in)))))))
 
+;;;###autoload
+(defun vde/org-next-visible-heading-or-link (&optional arg)
+  "Move to the next visible heading or link, whichever comes first.
+With prefix ARG and the point on a heading(link): jump over subsequent
+headings(links) to the next link(heading), respectively.  This is useful
+to skip over a long series of consecutive headings(links)."
+  (interactive "P")
+  (let ((next-heading (save-excursion
+                        (org-next-visible-heading 1)
+                        (when (org-at-heading-p) (point))))
+        (next-link (save-excursion
+                     (when (vde/org-next-visible-link) (point)))))
+    (when arg
+      (if (and (org-at-heading-p) next-link)
+          (setq next-heading nil)
+        (if (and (looking-at org-link-any-re) next-heading)
+            (setq next-link nil))))
+    (cond
+     ((and next-heading next-link) (goto-char (min next-heading next-link)))
+     (next-heading (goto-char next-heading))
+     (next-link (goto-char next-link)))))
+
+;;;###autoload
+(defun vde/org-previous-visible-heading-or-link (&optional arg)
+  "Move to the previous visible heading or link, whichever comes first.
+With prefix ARG and the point on a heading(link): jump over subsequent
+headings(links) to the previous link(heading), respectively.  This is useful
+to skip over a long series of consecutive headings(links)."
+  (interactive "P")
+  (let ((prev-heading (save-excursion
+                        (org-previous-visible-heading 1)
+                        (when (org-at-heading-p) (point))))
+        (prev-link (save-excursion
+                     (when (vde/org-next-visible-link t) (point)))))
+    (when arg
+      (if (and (org-at-heading-p) prev-link)
+          (setq prev-heading nil)
+        (if (and (looking-at org-link-any-re) prev-heading)
+            (setq prev-link nil))))
+    (cond
+     ((and prev-heading prev-link) (goto-char (max prev-heading prev-link)))
+     (prev-heading (goto-char prev-heading))
+     (prev-link (goto-char prev-link)))))
+
+;; Adapted from org-next-link to only consider visible links
+;;;###autoload
+(defun vde/org-next-visible-link (&optional search-backward)
+  "Move forward to the next visible link.
+When SEARCH-BACKWARD is non-nil, move backward."
+  (interactive)
+  (let ((pos (point))
+        (search-fun (if search-backward #'re-search-backward
+                      #'re-search-forward)))
+    ;; Tweak initial position: make sure we do not match current link.
+    (cond
+     ((and (not search-backward) (looking-at org-link-any-re))
+      (goto-char (match-end 0)))
+     (search-backward
+      (pcase (org-in-regexp org-link-any-re nil t)
+        (`(,beg . ,_) (goto-char beg)))))
+    (catch :found
+      (while (funcall search-fun org-link-any-re nil t)
+        (let ((folded (org-invisible-p nil t)))
+          (when (or (not folded) (eq folded 'org-link))
+            (let ((context (save-excursion
+                             (unless search-backward (forward-char -1))
+                             (org-element-context))))
+              (pcase (org-element-lineage context '(link) t)
+                (link
+                 (goto-char (org-element-property :begin link))
+                 (throw :found t)))))))
+      (goto-char pos)
+      ;; No further link found
+      nil)))
+
+;;;###autoload
+(defun vde/org-shifttab (&optional arg)
+  "Move to the previous visible heading or link.
+If already at a heading, move first to its beginning.  When inside a table,
+move to the previous field."
+  (interactive "P")
+  (cond
+   ((org-at-table-p) (call-interactively #'org-table-previous-field))
+   ((and (not (bolp)) (org-at-heading-p)) (beginning-of-line))
+   (t (call-interactively #'vde/org-previous-visible-heading-or-link))))
+
+;;;###autoload
+(defun vde/org-tab (&optional arg)
+  "Move to the next visible heading or link.
+When inside a table, re-align the table and move to the next field."
+  (interactive)
+  (cond
+   ((org-at-table-p) (org-table-justify-field-maybe)
+    (call-interactively #'org-table-next-field))
+   (t (call-interactively #'vde/org-next-visible-heading-or-link))))
+
 (provide 'org-func)
 ;;; org-func.el ends here