changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / emacs/lib/graph.el

changeset 655: 65102f74d1ae
parent: 328e1ff73938
child: b499d4bcfc39
author: Richard Westhaver <ellis@rwest.io>
date: Mon, 16 Sep 2024 21:28:33 -0400
permissions: -rw-r--r--
description: some optimizations, may have muddied the waters with cli-opt a bit though.. tbd
1 ;;; graph.el --- Graph-oriented Extensions -*- lexical-binding: t; -*-
2 
3 ;; Copyright (C) 2024 The Compiler Company
4 ;; Version: "0.2.0"
5 ;; Author: Richard Westhaver <richard.westhaver@gmail.com>
6 ;; Keywords: docs, maint, outlines, extensions
7 
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
12 
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
17 
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
20 
21 ;;; Commentary:
22 
23 ;;
24 
25 ;;; Code:
26 (require 'org)
27 (require 'org-agenda)
28 (require 'default)
29 (require 'ulang)
30 
31 (defgroup graph nil
32  "CC Graph")
33 
34 (defcustom org-graph-db-directory (join-paths user-org-stash-directory "graph")
35  "graph database storage directory."
36  :type 'directory
37  :group 'graph)
38 
39 (defcustom org-graph-locations (list (join-paths company-org-directory "notes"))
40  "List of directories to check for nodes."
41  :type '(list directory)
42  :group 'graph)
43 
44 (defcustom org-graph-include-agenda-files nil
45  "When non-nil, include `org-agenda-files' in the graph."
46  :type 'boolean
47  :group 'graph)
48 
49 (defcustom org-graph-include-archive nil
50  "When non-nil, include `org-arhive-location' in the graph."
51  :type 'boolean
52  :group 'graph)
53 
54 (defcustom org-graph-include-org-directory nil
55  "When non-nil, include `org-directory' files in the graph."
56  :type 'boolean
57  :group 'graph)
58 
59 (defcustom org-graph-compaction-hook nil
60  "Hook run when a graph is compacted to `org-graph-db'."
61  :type 'hook
62  :group 'graph)
63 
64 (defcustom org-graph-capture-hook nil
65  "Hook run when a node is added to the graph."
66  :type 'hook
67  :group 'graph)
68 
69 (defvar-local org-graph nil
70  "The currently active graph of org nodes.")
71 
72 (defcustom org-graph-db-init-script (join-paths company-source-directory "infra/scripts/org-db-init.lisp")
73  "Path to a lisp script responsible for initializing the `org-graph-db-directory'.")
74 
75 (cl-defstruct org-graph-db-handle
76  (type :rocksdb)
77  (name "org-graph-db")
78  get
79  put
80  delete
81  merge
82  compact
83  shutdown)
84 
85 (defcustom org-graph-db (make-org-graph-db-handle)
86  "A handle to the database backend which stores nodes and edges."
87  :type 'org-graph-db-handle
88  :group 'graph)
89 
90 (defun org-graph-from-id-locations ()
91  "Populate the `org-graph' from `org-id-locations', filtering out any
92 entries not under a member of `org-graph-locations'."
93  (interactive)
94  (setq-local org-graph (copy-hash-table (org-id-locations-load)))
95  (maphash
96  (lambda (k v)
97  (mapc
98  (lambda (x)
99  (unless (string-prefix-p x (file-truename v))
100  (remhash k org-graph)))
101  org-graph-locations))
102  org-graph))
103 
104 (defun org-dblock-write:links ()
105  "Generate a 'links' block for the designated node.")
106 
107 (defun org-dblock-write:graph ()
108  "Generate a 'graph' block for the designated set of nodes.")
109 
110 ;;; Links
111 ;; See https://github.com/toshism/org-super-links/blob/develop/org-super-links.el
112 (declare-function org-make-link-description-function "ext:org-mode")
113 
114 (defvar org-graph-edge-drawer "EDGES"
115  "Controls how/where to insert edges. If nil edges will just be inserted
116 under the heading.")
117 
118 ;; TODO 2024-09-16: edge properties
119 (defvar org-graph-edge-prefix 'org-graph-edge-prefix-timestamp
120  "Prefix to insert before the edge.
121 This can be a string, nil, or a function that takes no arguments and
122 returns a string.
123 
124 Default is the function `org-graph-edge-prefix-timestamp'
125 which returns an inactive timestamp formatted according to the variable
126 `org-time-stamp-formats' and a separator ' <- '.")
127 
128 ;; TODO 2024-09-16: do we need this? what sort of information for a
129 ;; given edge would go in the postfix? this may be better suited as a
130 ;; per-edge value rather than global - maybe use for comments.
131 (defvar org-graph-edge-postfix nil
132  "Postfix to insert after the edge.
133 This can be a string, nil, or a function that takes no arguments and
134 returns a string")
135 
136 (defvar org-graph-edge-related-into-drawer t
137  "Controls how/where to insert links.
138 If non-nil a drawer will be created and links inserted there. The
139 default is `org-graph-edge-related-drawer-default-name'. If this is set to a
140 string a drawer will be created using that string. For example LINKS.
141 If nil links will just be inserted at point.")
142 
143 (defvar org-graph-edge-related-drawer-default-name "EDGES"
144  "Default name to use for link drawer.
145 If variable `org-graph-edge-related-into-drawer' is 't' use this
146 name for the drawer. See variable `org-graph-edge-related-into-drawer' for more info.")
147 
148 (defvar org-graph-edge-link-prefix nil
149  "Prefix to insert before the link.
150 This can be a string, nil, or a function that takes no arguments and
151 returns a string")
152 
153 (defvar org-graph-edge-link-postfix nil
154  "Postfix to insert after the link.
155 This can be a string, nil, or a function that takes no arguments and
156 returns a string")
157 
158 (defvar org-graph-edge-default-description-formatter org-make-link-description-function
159  "What to use if no description is provided.
160 This can be a string, nil or a function that accepts two arguments
161 LINK and DESC and returns a string.
162 
163 nil will return the default desciption or the link.
164 string will be used only as a default fall back if set.
165 function will be called for every link.
166 
167 Default is the variable `org-make-link-desciption-function'.")
168 
169 (defvar org-graph-edge-search-function 'org-graph-edge-get-location
170  "The interface to use for finding target links. If you provide a custom
171 function it will be called with the `point` at the location the link
172 should be inserted. The only other requirement is that it should call
173 the function `org-graph-edge--insert-link' with a marker to the target
174 link. AKA the place you want the edge.
175 
176 `org-graph-edge-get-location' internally uses `org-refile-get-location'.")
177 
178 (defvar org-graph-edge-pre-link-hook nil
179  "Hook called before storing the link on the link side.
180 This is called with point at the location where it was called.")
181 
182 (defvar org-graph-edge-pre-backlink-hook nil
183  "Hook called before storing the link on the backlink side.
184 This is called with point in the heading of the backlink.")
185 
186 (defvar org-graph-edge-indicator-alist
187  '((link . "->")
188  (backlink . "<-")
189  (sibling . "--")
190  (parent . ">>")
191  (child . "<<"))
192  "An alist of (EDGE-TYPE . INDICATOR) pairs. Each INDICATOR is a string
193 which will be printed between the properties and backlink of the
194 associated EDGE-TYPE.")
195 
196 (defun org-graph-edge-get-location ()
197  "Default for function `org-graph-edge-search-function' that reuses the `org-refile' machinery."
198  (let ((target (org-refile-get-location "Node")))
199  (org-graph-edge--insert-link (set-marker (make-marker) (car (cdddr target))
200  (get-file-buffer (car (cdr target)))))))
201 
202 (defun org-graph-edge-search-function ()
203  "Call the search interface specified in variable `org-graph-edge-search-function'."
204  (funcall org-graph-edge-search-function))
205 
206 (defun org-graph-edge-prefix ()
207  "Return an appropriate string based on variable `org-graph-edge-prefix'."
208  (cond ((equal org-graph-edge-prefix nil) "")
209  ((stringp org-graph-edge-prefix) org-graph-edge-prefix)
210  (t (funcall org-graph-edge-prefix))))
211 
212 (defun org-graph-edge-postfix ()
213  "Return an appropriate string based on variable `org-graph-edge-postfix'."
214  (cond ((equal org-graph-edge-postfix nil) "\n")
215  ((stringp org-graph-edge-postfix) org-graph-edge-postfix)
216  (t (funcall org-graph-edge-postfix))))
217 
218 (defun org-graph-edge-link-prefix ()
219  "Return an appropriate string based on variable `org-graph-edge-link-prefix'."
220  (cond ((equal org-graph-edge-link-prefix nil) "")
221  ((stringp org-graph-edge-link-prefix) org-graph-edge-link-prefix)
222  (t (funcall org-graph-edge-link-prefix))))
223 
224 (defun org-graph-edge-link-postfix ()
225  "Return an appropriate string based on variable `org-graph-edge-link-postfix'."
226  (cond ((equal org-graph-edge-link-postfix nil) "")
227  ((stringp org-graph-edge-link-postfix) org-graph-edge-link-postfix)
228  (t (funcall org-graph-edge-link-postfix))))
229 
230 ;; TODO 2024-09-16: edge-properties
231 (defun org-graph-edge-prefix-timestamp ()
232  "Return the default prefix string for an edge.
233 Inactive timestamp formatted according to `org-time-stamp-formats' and
234 a separator ' <- '."
235  (concat (format-time-string (org-time-stamp-format t t) (current-time))
236  " <- "))
237 
238 (defun org-graph-edge-default-description-formatter (link desc)
239  "Return a string to use as the link desciption.
240 LINK is the link target. DESC is the provided desc."
241  (let ((p org-graph-edge-default-description-formatter))
242  (cond ((equal p nil) (or desc link))
243  ((stringp p) (or desc p))
244  ((fboundp p) (funcall p link desc))
245  (t desc))))
246 
247 (defun org-graph-edge-drawer ()
248  "Name of the edge drawer, as a string, or nil.
249 This is the value of variable
250 `org-graph-edge-drawer'. However, if the current
251 entry has or inherits a EDGE_DRAWER property, it will be
252 used instead of the default value."
253  (let ((p (org-entry-get nil "EDGE_DRAWER" 'inherit t)))
254  (cond ((equal p "nil") nil)
255  ((stringp p) p)
256  (t org-graph-edge-drawer))))
257 
258 ;; delete related functions
259 (defun org-graph-edge--find-link (id)
260  "Return link element for ID."
261  (save-restriction
262  (org-graph-edge--org-narrow-to-here)
263  (let ((link
264  (org-element-map (org-element-parse-buffer) 'link
265  (lambda (link)
266  (when (string= (org-element-property :path link) id)
267  link)))))
268  (widen)
269  (if (> (length link) 1)
270  (error "Multiple links found. Canceling delete")
271  (car link)))))
272 
273 (defun org-graph-edge--org-narrow-to-here ()
274  "Narrow to current heading, excluding subheadings."
275  (org-narrow-to-subtree)
276  (save-excursion
277  (org-next-visible-heading 1)
278  (narrow-to-region (point-min) (point))))
279 
280 
281 (defun org-graph-edge--in-drawer ()
282  "Return nil if point is not in a drawer.
283 Return element at point is in a drawer."
284  (let ((element (org-element-at-point)))
285  (while (and element
286  (not (memq (org-element-type element) '(drawer property-drawer))))
287  (setq element (org-element-property :parent element)))
288  element))
289 
290 (defun org-graph-edge--delete-link (link)
291  "Delete the LINK.
292 If point is in drawer, delete the entire line."
293  (save-excursion
294  (goto-char (org-element-property :begin link))
295  (if (org-graph-edge--in-drawer)
296  (progn
297  (kill-whole-line 1)
298  (org-remove-empty-drawer-at (point)))
299  (delete-region (org-element-property :begin link) (org-element-property :end link)))))
300 
301 ;;; EXPERIMENTAL 'related into drawer'
302 (defun org-graph-edge-related-into-drawer ()
303  "Name of the related drawer, as a string, or nil.
304 This is the value of variable
305 `org-graph-edge-related-into-drawer'. However, if the current
306 entry has or inherits a RELATED_INTO_DRAWER property, it will be
307 used instead of the default value."
308  (let ((p (org-entry-get nil "RELATED_INTO_DRAWER" 'inherit t)))
309  (cond ((equal p "nil") nil)
310  ((equal p "t") org-graph-edge-related-drawer-default-name)
311  ((stringp p) p)
312  (p org-graph-edge-related-drawer-default-name)
313  ((stringp org-graph-edge-related-into-drawer) org-graph-edge-related-into-drawer)
314  (org-graph-edge-related-into-drawer org-graph-edge-related-drawer-default-name))))
315 
316 (defun org-graph-edge-link-prefix-timestamp ()
317  "Return the default prefix string for an edge.
318 Inactive timestamp formatted according to `org-time-stamp-formats' and
319 a separator ' -> '."
320  (concat (format-time-string (org-time-stamp-format t t) (current-time))
321  (format " %s " (cdr (assoc 'link org-graph-edge-indicator-alist)))))
322 
323 (defun org-graph-edge-insert-related-link (link desc)
324  "LINK DESC related experiment."
325  (if (org-graph-edge-related-into-drawer)
326  (let* ((org-log-into-drawer (org-graph-edge-related-into-drawer))
327  (beg (org-log-beginning t)))
328  (goto-char beg)
329  (insert (org-graph-edge-link-prefix))
330  (insert (org-graph-edge-link-prefix-timestamp))
331  (org-insert-link nil link desc)
332  (insert (org-graph-edge-link-postfix) "\n")
333  (org-indent-region beg (point)))
334  (insert (org-graph-edge-link-prefix))
335  (org-insert-link nil link desc)
336  (insert (org-graph-edge-link-postfix))))
337 
338 (defun org-graph-edge-quick-insert-drawer-link ()
339  "Insert link into drawer regardless of variable `org-graph-edge-related-into-drawer' value."
340  (interactive)
341  ;; how to handle prefix here?
342  (let ((org-graph-edge-related-into-drawer (or org-graph-edge-related-into-drawer t))
343  (org-graph-edge-link-prefix 'org-graph-edge-link-prefix-timestamp))
344  (org-graph-edge-link)))
345 
346 (defun org-graph-edge-quick-insert-inline-link ()
347  "Insert inline link regardless of variable `org-graph-edge-related-into-drawer' value."
348  (interactive)
349  ;; how to handle prefix here?
350  (let ((org-graph-edge-related-into-drawer nil)
351  (org-graph-edge-link-prefix nil))
352  (org-graph-edge-link)))
353 
354 ;; end
355 
356 (defun org-graph-edge-insert (link desc)
357  "Insert edge to LINK with DESC.
358 Where the edge is placed is determined by the variable `org-graph-edge-drawer'."
359  (let* ((org-log-into-drawer (org-graph-edge-drawer))
360  (description (org-graph-edge-default-description-formatter link desc))
361  (beg (org-log-beginning t)))
362  (goto-char beg)
363  (insert (org-graph-edge-prefix))
364  (insert (org-link-make-string link description))
365  (insert (org-graph-edge-postfix))
366  (org-indent-region beg (point))))
367 
368 (defun org-graph-edge-links-action (marker hooks)
369  "Go to MARKER, run HOOKS and store a link."
370  (with-current-buffer (marker-buffer marker)
371  (save-excursion
372  (save-restriction
373  (widen) ;; buffer could be narrowed
374  (goto-char (marker-position marker))
375  (run-hooks hooks)
376  (call-interactively #'org-store-link)
377  (pop org-stored-links)))))
378 
379 (defun org-graph-edge-link-builder (link)
380  "Format link description for LINK."
381  (let* ((link-ref (car link))
382  (pre-desc (cadr link))
383  (description (org-graph-edge-default-description-formatter link-ref pre-desc)))
384  (cons link-ref description)))
385 
386 (defun org-graph-edge--insert-link (target &optional no-forward)
387  "Insert link to marker TARGET at current `point`, and create edge to here.
388 Only create edges in files in `org-mode' or a derived mode, otherwise just
389 act like a normal link.
390 
391 If NO-FORWARD is non-nil skip creating the forward link. Currently
392 only used when converting a link."
393  (let* ((source (point-marker))
394  (source-link (org-graph-edge-links-action source 'org-graph-edge-pre-link-hook))
395  (target-link (org-graph-edge-links-action target 'org-graph-edge-pre-backlink-hook))
396  (source-formatted-link (org-graph-edge-link-builder source-link))
397  (target-formatted-link (org-graph-edge-link-builder target-link)))
398  (with-current-buffer (marker-buffer target)
399  (save-excursion
400  (save-restriction
401  (widen) ;; buffer could be narrowed
402  (goto-char (marker-position target))
403  (when (derived-mode-p 'org-mode)
404  (org-graph-edge-insert (car source-formatted-link) (cdr source-formatted-link))))))
405  (unless no-forward
406  (with-current-buffer (marker-buffer source)
407  (save-excursion
408  (goto-char (marker-position source))
409  (org-graph-edge-insert-related-link (car target-formatted-link) (cdr target-formatted-link)))))))
410 
411 ;;;###autoload
412 (defun org-graph-edge-convert-link (arg)
413  "Convert a normal `org-mode' link at `point' to a graph link, ARG prefix.
414 If variable `org-graph-edge-related-into-drawer' is non-nil move
415 the link into drawer.
416 
417 When called interactively with a `C-u' prefix argument ignore
418 variable `org-graph-edge-related-into-drawer' configuration and
419 do not modify existing link."
420  (interactive "P")
421  (let ((from-m (point-marker))
422  (target (save-window-excursion
423  (with-current-buffer (current-buffer)
424  (save-excursion
425  (org-open-at-point)
426  (point-marker))))))
427  (org-graph-edge--insert-link target (or arg (not org-graph-edge-related-into-drawer)))
428  (goto-char (marker-position from-m)))
429 
430  (when (and (not arg) (org-graph-edge-related-into-drawer))
431  (let ((begin (org-element-property :begin (org-element-context)))
432  (end (org-element-property :end (org-element-context))))
433  (delete-region begin end))))
434 
435 ;;;###autoload
436 (defun org-graph-edge-delete ()
437  "Delete the link at point, and the corresponding reverse link.
438 If no reverse link exists, just delete link at point.
439 This works from either side, and deletes both sides of a link."
440  (interactive)
441  (save-window-excursion
442  (with-current-buffer (current-buffer)
443  (save-excursion
444  (let ((id (org-id-get (point))))
445  (org-open-at-point)
446  (let ((link-element (org-graph-edge--find-link id)))
447  (if link-element
448  (org-graph-edge--delete-link link-element)
449  (message "No edge found. Deleting active only.")))))))
450  (org-graph-edge--delete-link (org-element-context)))
451 
452 ;;;###autoload
453 (defun org-graph-edge-store-link (&optional GOTO KEYS)
454  "Store a point to register for use in function `org-graph-edge-insert-link'.
455 This is primarily intended to be called before `org-capture', but
456 could possibly even be used to replace `org-store-link' IF
457 function `org-graph-edge-insert-link' is used to replace
458 `org-insert-link'. This has not been thoroughly tested outside
459 of links to/form org files. GOTO and KEYS are unused."
460  (interactive "P")
461  (ignore GOTO)
462  (ignore KEYS)
463  (save-excursion
464  ;; this is a hack. if the point is at the first char of a heading
465  ;; the marker is not updated as expected when text is inserted
466  ;; above the heading. for example a capture template inserted
467  ;; above. that results in the link being to the heading above the
468  ;; expected heading.
469  (goto-char (line-end-position))
470  (let ((c1 (make-marker)))
471  (set-marker c1 (point) (current-buffer))
472  (set-register ?^ c1)
473  (message "Link copied"))))
474 
475 ;;;###autoload
476 (defun org-graph-edge-insert-link ()
477  "Insert an edge link from the register."
478  (interactive)
479  (let* ((target (get-register ?^)))
480  (if target
481  (progn
482  (org-graph-edge--insert-link target)
483  (set-register ?^ nil))
484  (message "No link to insert!"))))
485 
486 ;;;###autoload
487 (defun org-graph-edge-link ()
488  "Insert a link edge and add a backlink edge to the target heading."
489  (interactive)
490  (org-graph-edge-search-function))
491 
492 (provide 'graph)
493 ;; graph.el ends here