changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / emacs/lib/graph.el

changeset 665: c60decbaae3d
parent: b499d4bcfc39
child: f15e0f021a64
author: Richard Westhaver <ellis@rwest.io>
date: Sun, 22 Sep 2024 21:29:15 -0400
permissions: -rw-r--r--
description: more graph updates
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 (defcustom org-graph-db-init-script (join-paths company-source-directory "infra/scripts/org-db-init.lisp")
70  "Path to a lisp script responsible for initializing the `org-graph-db-directory'.")
71 
72 (cl-defstruct org-graph-db-handle
73  (type :rocksdb)
74  (name "org-graph-db")
75  init
76  get
77  put
78  delete
79  merge
80  compact
81  shutdown)
82 
83 (defcustom org-graph-db (make-org-graph-db-handle)
84  "A handle to the database backend which stores nodes and edges."
85  :type 'org-graph-db-handle
86  :group 'graph)
87 
88 (defun org-graph-from-id-locations (&optional edges local)
89  "Populate the `org-graph' from `org-id-locations', filtering out any
90 entries not under a member of `org-graph-locations'. When EDGES is
91 non-nil visit each node and collect all edges found."
92  (interactive)
93  (save-excursion
94  (let* ((node-ids (org-id-locations-load))
95  (graph (make-org-graph :nodes node-ids)))
96  (maphash
97  (lambda (k v)
98  (if-let ((ok (cl-loop for l in org-graph-locations
99  when (string-prefix-p l (file-truename v))
100  return t)))
101  (let ((pos (cdr (org-id-find-id-in-file k v))))
102  (message "%s %s" k v)
103  (org-with-file-buffer v
104  (goto-char pos)
105  (org-graph-node-at-point graph)
106  (when edges (org-graph-edges-at-point graph))))
107  (remhash k (org-graph-nodes graph))))
108  (org-graph-nodes graph))
109  (if local
110  (setq-local org-graph graph)
111  (setq org-graph graph)))))
112 
113 (defun org-graph-files ()
114  (org-list-files org-graph-locations org-agenda-extensions))
115 
116 (cl-defstruct org-graph
117  ;; TODO 2024-09-17: use integers instead of string
118  (nodes (make-hash-table :test 'equal))
119  (edges (make-hash-table :test 'equal)))
120 
121 (defvar org-graph (make-org-graph)
122  "The Emacs-native org-graph. Should be assigned to an `org-graph' instance.")
123 
124 (cl-defstruct org-graph-node id name file point)
125 (cl-defstruct org-graph-edge (type 'link) in properties timestamp out)
126 
127 (defun org-graph--file-hash (file)
128  "Compute the hash of FILE."
129  (with-temp-buffer
130  (set-buffer-multibyte nil)
131  (insert-file-contents-literally file)
132  (secure-hash 'md5 (current-buffer))))
133 
134 (defun org-graph-node-at-point (&optional update)
135  "Return the `org-graph-node' at point. When UPDATE is non-nil insert or
136 update the node into the org-graph object specified or when 't' use the
137 currently active org-graph."
138  (let* ((file (buffer-file-name))
139  (node (make-org-graph-node :point (point) :file file)))
140  (if (derived-mode-p 'org-mode)
141  (progn
142  (if (org-before-first-heading-p)
143  (setf (org-graph-node-name node) (org-get-title)
144  ;; use the filename, create a hash as id
145  (org-graph-node-id node) (org-graph--file-hash file))
146  (setf (org-graph-node-id node) (org-id-get)
147  (org-graph-node-name node) (elt (org-heading-components) 4))))
148  (setf (org-graph-node-id node) (org-graph--file-hash file)
149  (org-graph-node-name node) (file-name-nondirectory file)))
150  (when update
151  (puthash (org-graph-node-id node) node (org-graph-nodes (if (eql t update) org-graph update))))
152  node))
153 
154 (defun org-graph-collect-edge ()
155  "Collect the edge at point which should be a line created with `org-graph-edge--insert'."
156  (org-with-point-at (beginning-of-line)
157  (when (org-at-timestamp-p 'lax)
158  (let ((ts (match-string-no-properties 0))
159  (end (match-end 0)))
160  (goto-char (1+ end))
161  ;; next 2 chars are the arrow
162  (let ((arrow (org-graph-edge-arrow* (buffer-substring-no-properties (point) (1+ (point))))))
163  (goto-char (+ (point) 4))
164  (make-org-graph-edge :in (org-id-get)
165  :type arrow
166  :timestamp (org-parse-time-string ts t)
167  :out (string-trim (org--link-at-point) "id:")))))))
168 
169 (defun org-graph-collect-edges-at-point (&optional update)
170  "Collect the contents of the EDGES drawer from node at point. When UPDATE
171 is non-nil insert or update the node into the org-graph object specified
172 or when 't' use the currently active org-graph."
173  (with-org-graph-edge-drawer (end)
174  (re-search-backward (rx bol ?: (literal (org-graph-edge-drawer)) ?: eol) nil t)
175  (goto-char (1+ (match-end 0)))
176  (let ((edges
177  (cl-loop while (> (point-max) end (point))
178  collect (org-graph-collect-edge)
179  do (next-line))))
180  (when update
181  (mapc (lambda (e)
182  (puthash
183  (org-graph-edge-in e)
184  e
185  (org-graph-edges (if (eql t update) org-graph update))))
186  edges))
187  edges)))
188 
189 (defun org-graph-edges-at-point (&optional update)
190  "Return a list of `org-graph-edge' instances associated with the node at
191 point. When UPDATE is non-nil insert or update the edges into the
192 currently active org-graph."
193  (interactive)
194  (when (derived-mode-p 'org-mode)
195  (org-graph-collect-edges-at-point update)))
196 
197 (defun org-graph-buffer-update (&optional buffer)
198  "Map over an org buffer adding all nodes to the active org-graph."
199  (interactive)
200  (save-excursion
201  (with-current-buffer (or buffer (current-buffer))
202  ;; capture file node
203  (goto-char (point-min))
204  (org-graph-node-at-point t)
205  (when (derived-mode-p 'org-mode)
206  (org-map-entries (lambda () (org-graph-node-at-point t)))))))
207 
208 ;;; Edges
209 ;; See https://github.com/toshism/org-super-links/blob/develop/org-super-links.el
210 (declare-function org-make-link-description-function "ext:org-mode")
211 
212 (defvar org-graph-edge-drawer "EDGES"
213  "Controls how/where to insert edges. If nil edges will just be inserted
214 under the heading.")
215 
216 ;; TODO 2024-09-16: edge properties
217 (defvar org-graph-edge-prefix 'org-graph-edge-prefix-timestamp
218  "Prefix to insert before the edge.
219 This can be a string, nil, or a function that takes no arguments and
220 returns a string.
221 
222 Default is the function `org-graph-edge-prefix-timestamp'
223 which returns an inactive timestamp formatted according to the variable
224 `org-time-stamp-formats'.")
225 
226 ;; TODO 2024-09-16: do we need this? what sort of information for a
227 ;; given edge would go in the postfix? this may be better suited as a
228 ;; per-edge value rather than global - maybe use for comments.
229 (defvar org-graph-edge-postfix nil
230  "Postfix to insert after the edge.
231 This can be a string, nil, or a function that takes no arguments and
232 returns a string")
233 
234 (defvar org-graph-edge-link-prefix nil
235  "Prefix to insert before the link.
236 This can be a string, nil, or a function that takes no arguments and
237 returns a string")
238 
239 (defvar org-graph-edge-link-postfix nil
240  "Postfix to insert after the link.
241 This can be a string, nil, or a function that takes no arguments and
242 returns a string")
243 
244 (defvar org-graph-edge-default-description-formatter org-make-link-description-function
245  "What to use if no description is provided.
246 This can be a string, nil or a function that accepts two arguments
247 LINK and DESC and returns a string.
248 
249 nil will return the default desciption or the link.
250 string will be used only as a default fall back if set.
251 function will be called for every link.
252 
253 Default is the variable `org-make-link-desciption-function'.")
254 
255 (defvar org-graph-edge-search-function 'org-graph-edge-get-location
256  "The interface to use for finding target links. If you provide a custom
257 function it will be called with the `point` at the location the link
258 should be inserted. The only other requirement is that it should call
259 the function `org-graph-edge--insert-link' with a marker to the target
260 link. AKA the place you want the edge.
261 
262 `org-graph-edge-get-location' internally uses `org-refile-get-location'.")
263 
264 (defvar org-graph-edge-pre-link-hook nil
265  "Hook called before storing the link on the link side.
266 This is called with point at the location where it was called.")
267 
268 (defvar org-graph-edge-pre-backlink-hook nil
269  "Hook called before storing the link on the backlink side.
270 This is called with point in the heading of the backlink.")
271 
272 (defvar org-graph-edge-indicator-alist
273  '((link . "->")
274  (backlink . "<-")
275  (relation . "--")
276  (parent . ">>")
277  (child . "<<"))
278  "An alist of (EDGE-TYPE . INDICATOR) pairs. Each INDICATOR is a string
279 which will be printed between the properties and backlink of the
280 associated EDGE-TYPE.")
281 
282 (defun org-graph-edge-arrow (sym)
283  (cdr (assoc sym org-graph-edge-indicator-alist)))
284 
285 (defun org-graph-edge-arrow* (str)
286  "Reverse lookup of edge arrow symbol."
287  (car (rassoc str org-graph-edge-indicator-alist)))
288 
289 (defun org-graph-edge-get-location ()
290  "Default for function `org-graph-edge-search-function' that reuses the `org-refile' machinery."
291  (let ((target (org-refile-get-location "Node")))
292  (org-graph-edge--insert-link (set-marker (make-marker) (car (cdddr target))
293  (get-file-buffer (car (cdr target)))))))
294 
295 (cl-defmacro with-org-graph-edge-drawer ((start &optional create) &rest body)
296  "START is a symbol which is bound to the start of the edge drawer."
297  (declare (indent 1))
298  `(save-excursion
299  (org-with-wide-buffer
300  (let ((org-log-into-drawer (org-graph-edge-drawer)))
301  (org-graph-edge--org-narrow-to-here)
302  (let ((,start (org-log-beginning ,create)))
303  (when (re-search-forward (rx bol ?: "END" ?: eol) nil t)
304  (goto-char ,start)
305  ,@body))))))
306 
307 (defun org-graph-edge-search-function ()
308  "Call the search interface specified in variable `org-graph-edge-search-function'."
309  (funcall org-graph-edge-search-function))
310 
311 (defun org-graph-edge-prefix ()
312  "Return an appropriate string based on variable `org-graph-edge-prefix'."
313  (cond ((equal org-graph-edge-prefix nil) "")
314  ((stringp org-graph-edge-prefix) org-graph-edge-prefix)
315  (t (funcall org-graph-edge-prefix))))
316 
317 (defun org-graph-edge-postfix ()
318  "Return an appropriate string based on variable `org-graph-edge-postfix'."
319  (cond ((equal org-graph-edge-postfix nil) "\n")
320  ((stringp org-graph-edge-postfix) org-graph-edge-postfix)
321  (t (funcall org-graph-edge-postfix))))
322 
323 (defun org-graph-edge-link-prefix ()
324  "Return an appropriate string based on variable `org-graph-edge-link-prefix'."
325  (cond ((equal org-graph-edge-link-prefix nil) "")
326  ((stringp org-graph-edge-link-prefix) org-graph-edge-link-prefix)
327  (t (funcall org-graph-edge-link-prefix))))
328 
329 (defun org-graph-edge-link-postfix ()
330  "Return an appropriate string based on variable `org-graph-edge-link-postfix'."
331  (cond ((equal org-graph-edge-link-postfix nil) "")
332  ((stringp org-graph-edge-link-postfix) org-graph-edge-link-postfix)
333  (t (funcall org-graph-edge-link-postfix))))
334 
335 ;; TODO 2024-09-16: edge-properties
336 (defun org-graph-edge-prefix-timestamp ()
337  "Return the default prefix string for an edge.
338 Inactive timestamp formatted according to `org-time-stamp-formats'."
339  (format-time-string (org-time-stamp-format t t) (current-time)))
340 
341 (defun org-graph-edge-default-description-formatter (link desc)
342  "Return a string to use as the link desciption.
343 LINK is the link target. DESC is the provided desc."
344  (let ((p org-graph-edge-default-description-formatter))
345  (cond ((equal p nil) (or desc link))
346  ((stringp p) (or desc p))
347  ((fboundp p) (funcall p link desc))
348  (t desc))))
349 
350 (defun org-graph-edge-drawer ()
351  "Name of the edge drawer, as a string, or nil.
352 This is the value of variable
353 `org-graph-edge-drawer'. However, if the current
354 entry has or inherits a EDGE_DRAWER property, it will be
355 used instead of the default value."
356  (let ((p (org-entry-get nil "EDGE_DRAWER" 'inherit t)))
357  (cond ((equal p "nil") nil)
358  ((stringp p) p)
359  (t org-graph-edge-drawer))))
360 
361 (defun org-graph-edge--org-narrow-to-here ()
362  "Narrow to current heading, excluding subheadings."
363  (org-narrow-to-subtree)
364  (save-excursion
365  (org-next-visible-heading 1)
366  (narrow-to-region (point-min) (point))))
367 
368 ;; delete related functions
369 (defun org-graph-find-edges (id)
370  "Return link elements for ID."
371  (org-graph-edge--org-narrow-to-here)
372  (let ((links
373  (org-element-map (org-element-parse-buffer) 'link
374  (lambda (link)
375  (when (string= (org-element-property :path link) id)
376  link)))))
377  (widen)
378  links))
379 
380 (defun org-graph-edge--in-drawer-p ()
381  "Return non-nil if point is in drawer. Value is element at point."
382  (let ((element (org-element-at-point)))
383  (while (and element
384  (not (memq (org-element-type element) '(drawer property-drawer))))
385  (setq element (org-element-property :parent element)))
386  element))
387 
388 (defun org-graph-edge--delete-link (link)
389  "Delete the LINK. If point is in edges drawer, delete the entire line."
390  (save-excursion
391  (goto-char (org-element-property :begin link))
392  (if (org-graph-edge--in-drawer)
393  (progn
394  (kill-whole-line 1)
395  (org-remove-empty-drawer-at (point)))
396  (delete-region (org-element-property :begin link) (org-element-property :end link)))))
397 
398 (defun org-graph-edge--insert (link desc arrow &rest props)
399  "Insert an edge at point. ARROW is a symbol representing the type of
400 arrow to insert. The rest of the arguments are parsed as :KEY VAL pairs
401 which are inserted with the edge."
402  (insert (format "%s %s " (org-graph-edge-prefix)
403  (org-graph-edge-arrow arrow)))
404  (org-insert-link nil link desc)
405  (insert (org-graph-edge-link-postfix))
406  (newline))
407 
408 (defun org-graph-edge-insert-related (link desc)
409  "Insert a relation edge."
410  (with-org-graph-edge-drawer (beg t)
411  (org-graph-edge--insert link desc 'relation)
412  (org-indent-region beg (point))))
413 
414 (defun org-graph-edge-insert-backlink (link desc)
415  "Insert edge to LINK with DESC.
416 Where the edge is placed is determined by the variable `org-graph-edge-drawer'."
417  (with-org-graph-edge-drawer (beg t)
418  (let ((description (org-graph-edge-default-description-formatter link desc)))
419  (org-graph-edge--insert link description 'backlink)
420  (org-indent-region beg (point)))))
421 
422 (defun org-graph-edge-insert-link (link desc)
423  "insert a forward link edge."
424  (with-org-graph-edge-drawer (beg t)
425  (org-graph-edge--insert link desc 'link)
426  (org-indent-region beg (point))))
427 
428 (defun org-graph-edge-links-action (marker hooks)
429  "Go to MARKER, run HOOKS and store a link."
430  (with-current-buffer (marker-buffer marker)
431  (save-excursion
432  (save-restriction
433  (widen) ;; buffer could be narrowed
434  (goto-char (marker-position marker))
435  (run-hooks hooks)
436  (call-interactively #'org-store-link)
437  (pop org-stored-links)))))
438 
439 (defun org-graph-edge-link-builder (link)
440  "Format link description for LINK."
441  (let* ((link-ref (car link))
442  (pre-desc (cadr link))
443  (description (org-graph-edge-default-description-formatter link-ref pre-desc)))
444  (cons link-ref description)))
445 
446 (defun org-graph-edge--insert-link (target &optional no-forward)
447  "Insert link to marker TARGET and create an edge.
448 Only create edges in files in `org-mode' or a derived mode, otherwise just
449 act like a normal link.
450 
451 If NO-FORWARD is non-nil skip creating the forward link. Currently
452 only used when converting a link."
453  (let* ((source (point-marker))
454  (source-link (org-graph-edge-links-action source 'org-graph-edge-pre-link-hook))
455  (target-link (org-graph-edge-links-action target 'org-graph-edge-pre-backlink-hook))
456  (source-formatted-link (org-graph-edge-link-builder source-link))
457  (target-formatted-link (org-graph-edge-link-builder target-link)))
458  (with-current-buffer (marker-buffer target)
459  (save-excursion
460  (save-restriction
461  (widen) ;; buffer could be narrowed
462  (goto-char (marker-position target))
463  (when (derived-mode-p 'org-mode)
464  (org-graph-edge-insert-backlink (car source-formatted-link) (cdr source-formatted-link))))))
465  (unless no-forward
466  (with-current-buffer (marker-buffer source)
467  (save-excursion
468  (goto-char (marker-position source))
469  (org-graph-edge-insert-link (car target-formatted-link) (cdr target-formatted-link)))))))
470 
471 ;;;###autoload
472 (defun org-graph-edge-convert-link (arg)
473  "Convert a normal `org-mode' link at `point' to a graph link, ARG prefix.
474 When called interactively with a `C-u' prefix argument do not modify existing link."
475  (interactive "P")
476  (let ((from-m (point-marker))
477  (target (save-window-excursion
478  (with-current-buffer (current-buffer)
479  (save-excursion
480  (org-open-at-point)
481  (point-marker))))))
482  (org-graph-edge--insert-link target arg)
483  (goto-char (marker-position from-m)))
484  (when (not arg)
485  (let ((begin (org-element-property :begin (org-element-context)))
486  (end (org-element-property :end (org-element-context))))
487  (delete-region begin end))))
488 
489 ;;;###autoload
490 (defun org-graph-edge-delete ()
491  "Delete the link at point, and the corresponding reverse link.
492 If no reverse link exists, just delete link at point.
493 This works from either side, and deletes both sides of a link."
494  (interactive)
495  (save-window-excursion
496  (with-current-buffer (current-buffer)
497  (save-excursion
498  (let ((id (org-id-get (point))))
499  (org-open-at-point)
500  (let ((link-elements (org-graph-find-edges id)))
501  (if link-elements
502  (if (> (length link-elements) 1)
503  (error "Multiple links found.")
504  (org-graph-edge--delete-link (car link-elements)))
505  (message "No edge found. Deleting active only.")))))))
506  (org-graph-edge--delete-link (org-element-context)))
507 
508 ;;;###autoload
509 (defun org-graph-edge-insert ()
510  "Insert an edge from `org-stored-links')"
511  (interactive)
512  (if org-stored-links
513  (progn
514  (org-link-open (pop org-stored-links))
515  (org-graph-edge--insert-link (set-marker (make-marker) (point))))
516  (org-graph-edge-link)))
517 
518 ;;;###autoload
519 (defun org-graph-edge-link ()
520  "Insert a link edge and add a backlink edge to the target heading."
521  (interactive)
522  (org-graph-edge-search-function))
523 
524 (defun org-dblock-write:links ()
525  "Generate a 'links' block for the designated node.")
526 
527 (defun org-dblock-write:graph ()
528  "Generate a 'graph' block for the designated set of nodes.")
529 
530 (provide 'graph)
531 ;; graph.el ends here