changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / emacs/lib/graph.el

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