changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / emacs/lib/graph.el

changeset 652: 328e1ff73938
parent: af486e0a40c9
child: 65102f74d1ae
author: Richard Westhaver <ellis@rwest.io>
date: Sat, 14 Sep 2024 23:55:38 -0400
permissions: -rw-r--r--
description: graph and cli 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 (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 "LINKS"
115  "Controls how/where to insert edges. If nil edges will just be inserted
116 under the heading.")
117 
118 (defvar org-graph-edge-prefix 'org-graph-edge-prefix-timestamp
119  "Prefix to insert before the edge.
120 This can be a string, nil, or a function that takes no arguments and
121 returns a string.
122 
123 Default is the function `org-graph-edge-prefix-timestamp'
124 which returns an inactive timestamp formatted according to the variable
125 `org-time-stamp-formats' and a separator ' <- '.")
126 
127 (defvar org-graph-edge-postfix nil
128  "Postfix to insert after the edge.
129 This can be a string, nil, or a function that takes no arguments and
130 returns a string")
131 
132 (defvar org-graph-edge-related-into-drawer nil
133  "Controls how/where to insert links.
134 If non-nil a drawer will be created and links inserted there. The
135 default is `org-graph-edge-related-drawer-default-name'. If this is set to a
136 string a drawer will be created using that string. For example LINKS.
137 If nil links will just be inserted at point.")
138 
139 (defvar org-graph-edge-related-drawer-default-name "RELATED"
140  "Default name to use for link drawer.
141 If variable `org-graph-edge-related-into-drawer' is 't' use this
142 name for the drawer. See variable `org-graph-edge-related-into-drawer' for more info.")
143 
144 (defvar org-graph-edge-link-prefix nil
145  "Prefix to insert before the link.
146 This can be a string, nil, or a function that takes no arguments and
147 returns a string")
148 
149 (defvar org-graph-edge-link-postfix nil
150  "Postfix to insert after the link.
151 This can be a string, nil, or a function that takes no arguments and
152 returns a string")
153 
154 (defvar org-graph-edge-default-description-formatter org-make-link-description-function
155  "What to use if no description is provided.
156 This can be a string, nil or a function that accepts two arguments
157 LINK and DESC and returns a string.
158 
159 nil will return the default desciption or the link.
160 string will be used only as a default fall back if set.
161 function will be called for every link.
162 
163 Default is the variable `org-make-link-desciption-function'.")
164 
165 (defvar org-graph-edge-search-function 'org-graph-edge-get-location
166  "The interface to use for finding target links. If you provide a custom
167 function it will be called with the `point` at the location the link
168 should be inserted. The only other requirement is that it should call
169 the function `org-graph-edge--insert-link' with a marker to the target
170 link. AKA the place you want the edge.
171 
172 `org-graph-edge-get-location' internally uses `org-refile-get-location'.")
173 
174 (defvar org-graph-edge-pre-link-hook nil
175  "Hook called before storing the link on the link side.
176 This is called with point at the location where it was called.")
177 
178 (defvar org-graph-edge-pre-backlink-hook nil
179  "Hook called before storing the link on the backlink side.
180 This is called with point in the heading of the backlink.")
181 
182 (defun org-graph-edge-get-location ()
183  "Default for function `org-graph-edge-search-function' that reuses the `org-refile' machinery."
184  (let ((target (org-refile-get-location "Node")))
185  (org-graph-edge--insert-link (set-marker (make-marker) (car (cdddr target))
186  (get-file-buffer (car (cdr target)))))))
187 
188 (defun org-graph-edge-search-function ()
189  "Call the search interface specified in variable `org-graph-edge-search-function'."
190  (funcall org-graph-edge-search-function))
191 
192 (defun org-graph-edge-prefix ()
193  "Return an appropriate string based on variable `org-graph-edge-prefix'."
194  (cond ((equal org-graph-edge-prefix nil) "")
195  ((stringp org-graph-edge-prefix) org-graph-edge-prefix)
196  (t (funcall org-graph-edge-prefix))))
197 
198 (defun org-graph-edge-postfix ()
199  "Return an appropriate string based on variable `org-graph-edge-postfix'."
200  (cond ((equal org-graph-edge-postfix nil) "\n")
201  ((stringp org-graph-edge-postfix) org-graph-edge-postfix)
202  (t (funcall org-graph-edge-postfix))))
203 
204 (defun org-graph-edge-link-prefix ()
205  "Return an appropriate string based on variable `org-graph-edge-link-prefix'."
206  (cond ((equal org-graph-edge-link-prefix nil) "")
207  ((stringp org-graph-edge-link-prefix) org-graph-edge-link-prefix)
208  (t (funcall org-graph-edge-link-prefix))))
209 
210 (defun org-graph-edge-link-postfix ()
211  "Return an appropriate string based on variable `org-graph-edge-link-postfix'."
212  (cond ((equal org-graph-edge-link-postfix nil) "")
213  ((stringp org-graph-edge-link-postfix) org-graph-edge-link-postfix)
214  (t (funcall org-graph-edge-link-postfix))))
215 
216 (defun org-graph-edge-prefix-timestamp ()
217  "Return the default prefix string for an edge.
218 Inactive timestamp formatted according to `org-time-stamp-formats' and
219 a separator ' <- '."
220  (concat (format-time-string (org-time-stamp-format t t) (current-time))
221  " <- "))
222 
223 (defun org-graph-edge-default-description-formatter (link desc)
224  "Return a string to use as the link desciption.
225 LINK is the link target. DESC is the provided desc."
226  (let ((p org-graph-edge-default-description-formatter))
227  (cond ((equal p nil) (or desc link))
228  ((stringp p) (or desc p))
229  ((fboundp p) (funcall p link desc))
230  (t desc))))
231 
232 (defun org-graph-edge-drawer ()
233  "Name of the edge drawer, as a string, or nil.
234 This is the value of variable
235 `org-graph-edge-drawer'. However, if the current
236 entry has or inherits a EDGE_DRAWER property, it will be
237 used instead of the default value."
238  (let ((p (org-entry-get nil "EDGE_DRAWER" 'inherit t)))
239  (cond ((equal p "nil") nil)
240  ((equal p "t") "LINKS")
241  ((stringp p) p)
242  (p "LINKS")
243  ((stringp org-graph-edge-drawer) org-graph-edge-drawer)
244  (org-graph-edge-drawer "LINKS"))))
245 
246 ;; delete related functions
247 (defun org-graph-edge--find-link (id)
248  "Return link element for ID."
249  (save-restriction
250  (org-graph-edge--org-narrow-to-here)
251  (let ((link
252  (org-element-map (org-element-parse-buffer) 'link
253  (lambda (link)
254  (when (string= (org-element-property :path link) id)
255  link)))))
256  (widen)
257  (if (> (length link) 1)
258  (error "Multiple links found. Canceling delete")
259  (car link)))))
260 
261 (defun org-graph-edge--org-narrow-to-here ()
262  "Narrow to current heading, excluding subheadings."
263  (org-narrow-to-subtree)
264  (save-excursion
265  (org-next-visible-heading 1)
266  (narrow-to-region (point-min) (point))))
267 
268 
269 (defun org-graph-edge--in-drawer ()
270  "Return nil if point is not in a drawer.
271 Return element at point is in a drawer."
272  (let ((element (org-element-at-point)))
273  (while (and element
274  (not (memq (org-element-type element) '(drawer property-drawer))))
275  (setq element (org-element-property :parent element)))
276  element))
277 
278 
279 (defun org-graph-edge--delete-link (link)
280  "Delete the LINK.
281 If point is in drawer, delete the entire line."
282  (save-excursion
283  (goto-char (org-element-property :begin link))
284  (if (org-graph-edge--in-drawer)
285  (progn
286  (kill-whole-line 1)
287  (org-remove-empty-drawer-at (point)))
288  (delete-region (org-element-property :begin link) (org-element-property :end link)))))
289 
290 
291 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
292 ;; EXPERIMENTAL related into drawer
293 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
294 
295 (defun org-graph-edge-related-into-drawer ()
296  "Name of the related drawer, as a string, or nil.
297 This is the value of variable
298 `org-graph-edge-related-into-drawer'. However, if the current
299 entry has or inherits a RELATED_INTO_DRAWER property, it will be
300 used instead of the default value."
301  (let ((p (org-entry-get nil "RELATED_INTO_DRAWER" 'inherit t)))
302  (cond ((equal p "nil") nil)
303  ((equal p "t") org-graph-edge-related-drawer-default-name)
304  ((stringp p) p)
305  (p org-graph-edge-related-drawer-default-name)
306  ((stringp org-graph-edge-related-into-drawer) org-graph-edge-related-into-drawer)
307  (org-graph-edge-related-into-drawer org-graph-edge-related-drawer-default-name))))
308 
309 (defun org-graph-edge-insert-relatedlink (link desc)
310  "LINK DESC related experiment."
311  (if (org-graph-edge-related-into-drawer)
312  (let* ((org-log-into-drawer (org-graph-edge-related-into-drawer))
313  (beg (org-log-beginning t)))
314  (goto-char beg)
315  (insert (org-graph-edge-link-prefix))
316  (org-insert-link nil link desc)
317  (insert (org-graph-edge-link-postfix) "\n")
318  (org-indent-region beg (point)))
319  (insert (org-graph-edge-link-prefix))
320  (org-insert-link nil link desc)
321  (insert (org-graph-edge-link-postfix))))
322 
323 (defun org-graph-edge-link-prefix-timestamp ()
324  "Return the default prefix string for an edge.
325 Inactive timestamp formatted according to `org-time-stamp-formats' and
326 a separator ' -> '."
327  (concat (format-time-string (org-time-stamp-format t t) (current-time))
328  " -> "))
329 
330 (defun org-graph-edge-quick-insert-drawer-link ()
331  "Insert link into drawer regardless of variable `org-graph-edge-related-into-drawer' value."
332  (interactive)
333  ;; how to handle prefix here?
334  (let ((org-graph-edge-related-into-drawer (or org-graph-edge-related-into-drawer t))
335  (org-graph-edge-link-prefix 'org-graph-edge-link-prefix-timestamp))
336  (org-graph-edge-link)))
337 
338 (defun org-graph-edge-quick-insert-inline-link ()
339  "Insert inline link 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 nil)
343  (org-graph-edge-link-prefix nil))
344  (org-graph-edge-link)))
345 
346 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
347 ;; /EXPERIMENTAL related into drawer
348 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
349 
350 (defun org-graph-edge-insert (link desc)
351  "Insert edge to LINK with DESC.
352 Where the edge is placed is determined by the variable `org-graph-edge-drawer'."
353  (let* ((org-log-into-drawer (org-graph-edge-drawer))
354  (description (org-graph-edge-default-description-formatter link desc))
355  (beg (org-log-beginning t)))
356  (goto-char beg)
357  (insert (org-graph-edge-prefix))
358  (insert (org-link-make-string link description))
359  (insert (org-graph-edge-postfix))
360  (org-indent-region beg (point))))
361 
362 (defun org-graph-edge-links-action (marker hooks)
363  "Go to MARKER, run HOOKS and store a link."
364  (with-current-buffer (marker-buffer marker)
365  (save-excursion
366  (save-restriction
367  (widen) ;; buffer could be narrowed
368  (goto-char (marker-position marker))
369  (run-hooks hooks)
370  (call-interactively #'org-store-link)
371  (pop org-stored-links)))))
372 
373 (defun org-graph-edge-link-builder (link)
374  "Format link description for LINK."
375  (let* ((link-ref (car link))
376  (pre-desc (cadr link))
377  (description (org-graph-edge-default-description-formatter link-ref pre-desc)))
378  (cons link-ref description)))
379 
380 (defun org-graph-edge--insert-link (target &optional no-forward)
381  "Insert link to marker TARGET at current `point`, and create edge to here.
382 Only create edges in files in `org-mode' or a derived mode, otherwise just
383 act like a normal link.
384 
385 If NO-FORWARD is non-nil skip creating the forward link. Currently
386 only used when converting a link."
387  (let* ((source (point-marker))
388  (source-link (org-graph-edge-links-action source 'org-graph-edge-pre-link-hook))
389  (target-link (org-graph-edge-links-action target 'org-graph-edge-pre-backlink-hook))
390  (source-formatted-link (org-graph-edge-link-builder source-link))
391  (target-formatted-link (org-graph-edge-link-builder target-link)))
392  (with-current-buffer (marker-buffer target)
393  (save-excursion
394  (save-restriction
395  (widen) ;; buffer could be narrowed
396  (goto-char (marker-position target))
397  (when (derived-mode-p 'org-mode)
398  (org-graph-edge-insert (car source-formatted-link) (cdr source-formatted-link))))))
399  (unless no-forward
400  (with-current-buffer (marker-buffer source)
401  (save-excursion
402  (goto-char (marker-position source))
403  (org-graph-edge-insert-relatedlink (car target-formatted-link) (cdr target-formatted-link)))))))
404 
405 
406 ;;;###autoload
407 (defun org-graph-edge-convert-link-to-edge (arg)
408  "Convert a normal `org-mode' link at `point' to a graph link, ARG prefix.
409 If variable `org-graph-edge-related-into-drawer' is non-nil move
410 the link into drawer.
411 
412 When called interactively with a `C-u' prefix argument ignore
413 variable `org-graph-edge-related-into-drawer' configuration and
414 do not modify existing link."
415  (interactive "P")
416  (let ((from-m (point-marker))
417  (target (save-window-excursion
418  (with-current-buffer (current-buffer)
419  (save-excursion
420  (org-open-at-point)
421  (point-marker))))))
422  (org-graph-edge--insert-link target (or arg (not org-graph-edge-related-into-drawer)))
423  (goto-char (marker-position from-m)))
424 
425  (when (and (not arg) (org-graph-edge-related-into-drawer))
426  (let ((begin (org-element-property :begin (org-element-context)))
427  (end (org-element-property :end (org-element-context))))
428  (delete-region begin end))))
429 
430 ;;;###autoload
431 (defun org-graph-edge-delete-link ()
432  "Delete the link at point, and the corresponding reverse link.
433 If no reverse link exists, just delete link at point.
434 This works from either side, and deletes both sides of a link."
435  (interactive)
436  (save-window-excursion
437  (with-current-buffer (current-buffer)
438  (save-excursion
439  (let ((id (org-id-get (point))))
440  (org-open-at-point)
441  (let ((link-element (org-graph-edge--find-link id)))
442  (if link-element
443  (org-graph-edge--delete-link link-element)
444  (message "No edge found. Deleting active only.")))))))
445  (org-graph-edge--delete-link (org-element-context)))
446 
447 ;;;###autoload
448 (defun org-graph-edge-store-link (&optional GOTO KEYS)
449  "Store a point to register for use in function `org-graph-edge-insert-link'.
450 This is primarily intended to be called before `org-capture', but
451 could possibly even be used to replace `org-store-link' IF
452 function `org-graph-edge-insert-link' is used to replace
453 `org-insert-link'. This has not been thoroughly tested outside
454 of links to/form org files. GOTO and KEYS are unused."
455  (interactive "P")
456  (ignore GOTO)
457  (ignore KEYS)
458  (save-excursion
459  ;; this is a hack. if the point is at the first char of a heading
460  ;; the marker is not updated as expected when text is inserted
461  ;; above the heading. for example a capture template inserted
462  ;; above. that results in the link being to the heading above the
463  ;; expected heading.
464  (goto-char (line-end-position))
465  (let ((c1 (make-marker)))
466  (set-marker c1 (point) (current-buffer))
467  (set-register ?^ c1)
468  (message "Link copied"))))
469 
470 ;; not sure if this should be autoloaded or left to config?
471 ;;;###autoload
472 (advice-add 'org-capture :before #'org-graph-edge-store-link)
473 
474 ;;;###autoload
475 (defun org-graph-edge-insert-link ()
476  "Insert an edge link from the register."
477  (interactive)
478  (let* ((target (get-register ?^)))
479  (if target
480  (progn
481  (org-graph-edge--insert-link target)
482  (set-register ?^ nil))
483  (message "No link to insert!"))))
484 
485 ;;;###autoload
486 (defun org-graph-edge-link ()
487  "Insert a link and add a backlink to the target heading."
488  (interactive)
489  (org-graph-edge-search-function))
490 
491 (provide 'graph)
492 ;; graph.el ends here