changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / emacs/lib/uml-mode.el

changeset 698: 96958d3eb5b0
parent: 74a55d5decce
author: Richard Westhaver <ellis@rwest.io>
date: Fri, 04 Oct 2024 22:04:59 -0400
permissions: -rw-r--r--
description: fixes
1 ;;; uml-mode.el --- Minor mode for ascii uml sequence diagrams -*- lexical-binding: t -*-
2 
3 ;; Copyright (C) 2015-2020 Ian Martins
4 
5 ;; Author: Ian Martins <ianxm@jhu.edu>
6 ;; URL: http://github.com/ianxm/emacs-uml
7 ;; Version: 0.0.4
8 ;; Keywords: docs
9 ;; Package-Requires: ((emacs "24.4") seq)
10 
11 ;; This file is not part of GNU Emacs.
12 
13 ;; This program is free software: you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
17 
18 ;; This program is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
22 
23 ;; For a full copy of the GNU General Public License
24 ;; see <http://www.gnu.org/licenses/>.
25 
26 ;;; Commentary:
27 
28 ;; provides functions that help in writing ascii uml sequence diagrams.
29 
30 ;;; Code:
31 
32 (require 'seq)
33 (require 'subr-x)
34 
35 (defun uml-forward-timeline ()
36  "Move the point to the next timeline bar."
37  (interactive)
38  (let ((start (point))
39  word)
40  (forward-word)
41  (setq word (point))
42  (goto-char start)
43  (forward-char)
44  (while (and
45  (not (eq ?| (char-after)))
46  (< (point) word))
47  (forward-char))))
48 
49 (defun uml-back-timeline ()
50  "Move the point to the previous timeline bar."
51  (interactive)
52  (let ((start (point))
53  word)
54  (forward-word -1)
55  (setq word (point))
56  (goto-char start)
57  (forward-char -1)
58  (while (and
59  (not (eq ?| (char-after)))
60  (> (point) word))
61  (forward-char -1))))
62 
63 (defun uml-swap-left ()
64  "Swap the timeline at the point with the timeline to its left."
65  (interactive)
66  (uml--redraw-sequence-diagram (list 'name :swapleft 'col (current-column))))
67 
68 (defun uml-swap-right ()
69  "Swap the timeline at the point with the timeline to its right."
70  (interactive)
71  (uml--redraw-sequence-diagram (list 'name :swapright 'col (current-column))))
72 
73 (defun uml-delete-timeline ()
74  "Delete the timeline at point."
75  (interactive)
76  (uml--redraw-sequence-diagram (list 'name :delete 'col (current-column))))
77 
78 (defun uml-insert-timeline ()
79  "Insert a timeline to the right of the point."
80  (interactive)
81  (uml--redraw-sequence-diagram (list 'name :insert 'col (current-column))))
82 
83 (defun uml-sequence-diagram ()
84  "Formats a sequence diagram."
85  (interactive)
86  (uml--redraw-sequence-diagram nil))
87 
88 (defun uml--write-text-centered-on (text target)
89  "Write TEXT centered on the TARGET column."
90  (let* ((halfname (floor (/ (length text) 2)))
91  (col (- target halfname))) ; target-pos-len/2
92  (move-to-column col t)
93  (insert (format "%s" text))))
94 
95 (defun uml--write-vertical-space (timelines prefix)
96  "Write a row of empty timeline bars for TIMELINES after writing PREFIX."
97  (if prefix
98  (insert prefix))
99  (dolist (elt timelines)
100  (let* ((col (plist-get elt 'center)))
101  (move-to-column col t)
102  (insert (format "|")))))
103 
104 (defun uml--find-nearest-timeline (timelines col)
105  "Return the index of the nearest of TIMELINES to the column COL."
106  (let ((ii 0)
107  olddelta
108  ret
109  delta)
110  (dolist (elt timelines)
111  (setq delta (abs (- col (plist-get elt 'origcenter))))
112  (when (or (not ret) (< delta olddelta))
113  (setq ret ii)
114  (setq olddelta delta))
115  (setq ii (1+ ii)))
116  ret))
117 
118 (defun uml--write-arrow (from to dashed)
119  "Write an arrow from FROM timeline to TO timeline, possibly with a DASHED line."
120  (let ((delta (abs (- to from)))
121  (ii 0)
122  on) ; bool to toggle between dash or space
123  (move-to-column (1+ (min to from)))
124  (if (> from to) ; <---
125  (insert ?<))
126  (while (< ii (- delta 2))
127  (insert (if (or (not dashed) on) ?- ? ))
128  (if on (setq on nil) (setq on t)) ; toggle dash
129  (setq ii (1+ ii)))
130  (if (< from to) ; --->
131  (insert ?>))
132  (delete-char (- delta 1))))
133 
134 (defun uml--write-label-and-arrow (timelines prefix fromcol tocol text dashed)
135  "Write TIMELINES with PREFIX then label and arrow for a message from column FROMCOL to column TOCOL with label TEXT which may be DASHED."
136  ;; write label
137  (if text
138  (let (center)
139  (dotimes (ii (length text))
140  (uml--write-vertical-space timelines prefix)
141  (newline)
142  (forward-line -1)
143  (setq center (floor (/ (+ fromcol tocol) 2)))
144  (uml--write-text-centered-on (nth ii text) center)
145  (delete-char (length (nth ii text)))
146  (forward-line))))
147 
148  ;; write arrow
149  (uml--write-vertical-space timelines prefix)
150  (newline)
151  (forward-line -1)
152  (uml--write-arrow fromcol tocol dashed)
153  (forward-line))
154 
155 (defun uml--write-self-arrow (timelines prefix col text)
156  "Write TIMELINES with PREFIX and an arrow from and to column COL, labeled with TEXT."
157  (let ((numrows (max 2 (length text)))
158  arrow part-index text-part)
159  (dotimes (ii numrows)
160  (setq arrow (cond
161  ((= (- numrows ii) 2) " --.")
162  ((= (- numrows ii) 1) "<--'")
163  (t " ")))
164  (if (not text)
165  (setq text-part "")
166  (setq part-index (+ (- ii numrows) (length text)))
167  (setq text-part (if (< part-index 0) "" (nth part-index text))))
168  (uml--write-vertical-space timelines prefix)
169  (newline)
170  (forward-line -1)
171  (move-to-column (1+ col))
172  (insert (format "%s %s" arrow text-part))
173  (delete-char (min (+ 5 (length text-part)) (- (line-end-position) (point))))
174  (forward-line))))
175 
176 (defun uml--fit-label-between (timelines left right width)
177  "Spread out TIMELINES so that LEFT and RIGHT have WIDTH space between them."
178  (let (leftcol
179  rightcol
180  needed)
181  (setq leftcol (plist-get (nth left timelines) 'center))
182  (setq rightcol (plist-get (nth right timelines) 'center))
183  (setq needed (- (+ leftcol width) rightcol))
184  (if (> needed 0)
185  (uml--shift-to-the-right timelines right needed))))
186 
187 (defun uml--shift-to-the-right (timelines right needed)
188  "Shift all TIMELINES greater than or equal to RIGHT to the right by NEEDED."
189  (let ((ii right)
190  elt)
191  (while (< ii (length timelines))
192  (setq elt (nth ii timelines))
193  (plist-put elt 'center (+ (plist-get elt 'center) needed))
194  (setq ii (1+ ii)))))
195 
196 (defun uml--swap-timelines (timelines messages col1 col2)
197  "Given all TIMELINES and MESSAGES, swap COL1 and COL2."
198  (let (tmp)
199  (setq tmp (nth col1 timelines))
200  (setcar (nthcdr col1 timelines) (nth col2 timelines))
201  (setcar (nthcdr col2 timelines) tmp))
202  (dolist (elt messages)
203  (if (= (plist-get elt 'from) col1) (plist-put elt 'from col2)
204  (if (= (plist-get elt 'from) col2) (plist-put elt 'from col1)))
205  (if (= (plist-get elt 'to) col1) (plist-put elt 'to col2)
206  (if (= (plist-get elt 'to) col2) (plist-put elt 'to col1)))))
207 
208 (defun uml--find-top-or-bottom (direction)
209  "Return the position at the top or bottom of the diagram depending on DIRECTION (:top or :bottom)."
210  (let ((end-of-buffer (if (eq direction :top) (point-min) (point-max)))
211  (step (if (eq direction :top) -1 1)))
212  (while (and
213  (not (= (point) end-of-buffer))
214  (not (looking-at "^[^[:word:]|]*$")))
215  (forward-line step))
216  (cond
217  ((eq direction :top)
218  (if (looking-at "^[^[:word:]|]*$")
219  (forward-line))
220  (point))
221  ((eq direction :bottom)
222  (if (not (= (point) (point-max)))
223  (forward-line -1))
224  (line-end-position)))))
225 
226 (defun uml--calc-middle (start end)
227  "This just computes the integer mean of START and END."
228  (floor (/ (+ start end) 2)))
229 
230 (defun uml--determine-prefix ()
231  "Determine the prefix (if there is one).
232 
233 The prefix is made up of any characters on the left margin that
234 aren't part of the diagram, such as comment characters. Prefixes
235 can be any length but must be made up of only special
236 characters. Prefixes can have leading spaces but cannot contain
237 spaces in the middle or at the end."
238  (if (looking-at "\\([[:blank:]]*[^[:word:][:blank:]]+\\) ")
239  (match-string 1)
240  nil))
241 
242 (defun uml--parse-timelines (prefix bottom)
243  "Parse the timeline names.
244 
245 Parse timeline names after the PREFIX of each line until we hit
246 BOTTOM or see a pipe indicating we're past the timeline names and
247 into the messages. For each timeline, determine the name and
248 center column. The return structure looks like:
249 
250  [ (name \"timeline1\" origcenter 5) ... ]
251 
252 Names can contain any characters except whitespace or pipes."
253  (let (timelines eob)
254  (while (and (looking-at (concat prefix "[^|]+$"))
255  (< (point) bottom))
256  (forward-char (length prefix))
257  ;; the first "[:blank:]" allows whitespace leading to the name,
258  ;; but doesn't let the while loop go to the next line.
259  (while (looking-at "[[:blank:]]*\\([^[:blank:]|\n]+\\)")
260  (let* ((name (match-string 1))
261  (beg (- (match-beginning 1) (line-beginning-position)))
262  (end (- (match-end 1) (line-beginning-position)))
263  (center (uml--calc-middle beg end))
264  (index (uml--find-nearest-timeline timelines center))
265  (halflen (and index (/ (uml--max-length-multipart-name (plist-get (nth index timelines) 'name) 2) 2))))
266  ;; if this is the first timeline or center is outside of the
267  ;; nearest existing timeline, then this is a new timeline
268  ;; and we should create a new timeline, else append to an
269  ;; existing one
270  (if (or (not timelines)
271  (or (> beg (+ (plist-get (nth index timelines) 'origcenter) halflen))
272  (< end (- (plist-get (nth index timelines) 'origcenter) halflen))))
273  (setq timelines (append timelines (list (list 'name (list name)
274  'origcenter center))))
275  (nconc (plist-get (nth index timelines) 'name) (list name))))
276  (goto-char (match-end 1)))
277  (setq eob (= 1 (forward-line 1))))
278  (if (not eob) ; if we didn't hit the end of the buffer,
279  (forward-line -1)) ; back up so message parsing can pick up from the last header line
280 
281  (sort timelines (lambda (a b) (< (plist-get a 'origcenter)
282  (plist-get b 'origcenter))))))
283 
284 (defun uml--parse-messages (timelines prefix bottom)
285  "Parse the messages from the diagram.
286 
287 Parse messages from the diagram given the TIMELINES and PREFIX
288 until we reach the BOTTOM. Messages is a mixed list of plists of
289 arrows and separators.
290 
291 Arrows look like:
292  (from 0 to 2 label (\"doIt()\") dashed nil)
293 
294 Labels must start with a number or letter and cannot contain
295 spaces, angle brackets or dashes.
296 
297 Separators look like:
298  (text \"title for next part\")"
299  (let (messages label dashed found)
300  (while (and (< (line-end-position) (- bottom (length prefix)))
301  (< (line-end-position) (buffer-end 1)))
302  (forward-line 1)
303  (forward-char (length prefix))
304 
305  ;; the label may be above the message or on the same line
306  (when (re-search-forward "[[:word:]][^\n|<>\-]*" (line-end-position) t)
307  (if (not label)
308  (setq label (list (string-trim-right (match-string 0)))) ; single part
309  (nconc label (list (string-trim-right (match-string 0))))) ; multi part
310  (beginning-of-line))
311 
312  ;; FOUND is (from . to) where FROM and TO are timeline indices
313  (setq found (uml--find-message-bounds-maybe timelines))
314 
315  (when found
316  (beginning-of-line)
317  (setq dashed (re-search-forward "\- \-" (line-end-position) t))
318  (setq messages (append messages (list (list 'label label
319  'from (car found)
320  'to (cdr found)
321  'dashed dashed))))
322  (setq label nil)))
323  messages))
324 
325 (defun uml--find-message-bounds-maybe (timelines)
326  "Find which timelines a message connects.
327 
328 Return the indices in TIMELINES between which the message passes
329 as (from . to), else nil if there is no message on the current
330 line"
331  (let (from to found)
332  (cond
333  ((re-search-forward "\-.*>" (line-end-position) t) ; ->
334  (setq from (uml--find-nearest-timeline timelines (- (match-beginning 0) (line-beginning-position))))
335  (setq to (uml--find-nearest-timeline timelines (- (match-end 0) (line-beginning-position))))
336  (setq found t))
337 
338  ((re-search-forward "<.*\-" (line-end-position) t) ; <-
339  (setq from (uml--find-nearest-timeline timelines (- (match-end 0) (line-beginning-position))))
340  (setq to (uml--find-nearest-timeline timelines (- (match-beginning 0) (line-beginning-position))))
341  (setq found t))
342 
343  ((re-search-forward "<" (line-end-position) t) ; <
344  (setq from (uml--find-nearest-timeline timelines (- (match-end 0) (line-beginning-position))))
345  (setq to (uml--find-nearest-timeline timelines (- (match-beginning 0) (line-beginning-position))))
346  (setq found t))
347 
348  ((re-search-forward "|\-" (line-end-position) t) ; |-
349  (setq from (uml--find-nearest-timeline timelines (- (match-beginning 0) (line-beginning-position))))
350  (setq to (1+ from))
351  (if (< to (length timelines))
352  (setq found t)
353  (message "Ignoring out of bounds message.")))
354 
355  ((re-search-forward "\-|" (line-end-position) t) ; -|
356  (setq from (uml--find-nearest-timeline timelines (- (match-beginning 0) (line-beginning-position))))
357  (setq to (- from 1))
358  (if (>= to 0)
359  (setq found t)
360  (message "Ignoring out of bounds message."))))
361  (if found (cons from to) nil)))
362 
363 (defun uml--apply-adjustments (adjust timelines messages)
364  "Apply ADJUST to TIMELINES and MESSAGES.
365 
366 Return (TIMELINES . MESSAGES) since we mucked with both of them."
367  (cond
368  ((eq :swapleft (plist-get adjust 'name))
369  (let (current swapwith)
370  (setq current (uml--find-nearest-timeline timelines (plist-get adjust 'col)))
371  (setq swapwith (- current 1))
372  (if (or (< swapwith 0) (>= swapwith (length timelines)))
373  (plist-put adjust 'movetocol current)
374  (plist-put adjust 'movetocol swapwith)
375  (uml--swap-timelines timelines messages current swapwith))))
376 
377  ((eq :swapright (plist-get adjust 'name))
378  (let (current swapwith)
379  (setq current (uml--find-nearest-timeline timelines (plist-get adjust 'col)))
380  (setq swapwith (1+ current))
381  (if (or (< swapwith 0) (>= swapwith (length timelines)))
382  (plist-put adjust 'movetocol current)
383  (plist-put adjust 'movetocol swapwith)
384  (uml--swap-timelines timelines messages current swapwith))))
385 
386  ((eq :delete (plist-get adjust 'name))
387  (let (current col)
388  (setq current (uml--find-nearest-timeline timelines (plist-get adjust 'col))
389  col current)
390  (plist-put adjust 'movetocol (max 0 (1- col)))
391  (when (>= col 0)
392  (setq timelines (delete (nth col timelines) timelines))
393  (dolist (elt messages)
394  (let ((from (plist-get elt 'from))
395  (to (plist-get elt 'to)))
396  (if (or (= from col) (= to col))
397  (setq messages (delete elt messages))
398  (if (> from col) (plist-put elt 'from (- from 1)))
399  (if (> to col) (plist-put elt 'to (- to 1)))))))))
400 
401  ((eq :insert (plist-get adjust 'name))
402  (let (current new rest)
403  (setq current (uml--find-nearest-timeline timelines (plist-get adjust 'col)))
404  (plist-put adjust 'movetocol current)
405  (setq current (1+ current))
406  (setq new (list (list 'name (list "new")
407  'origcenter nil)))
408  (setq rest (nthcdr current timelines))
409  (setcdr (nthcdr (- current 1) timelines) new)
410  (setcdr new rest)
411  (dolist (elt messages)
412  (let ((from (plist-get elt 'from))
413  (to (plist-get elt 'to)))
414  (if (>= from current) (plist-put elt 'from (1+ from)))
415  (if (>= to current) (plist-put elt 'to (1+ to))))))))
416  (cons timelines messages))
417 
418 (defun uml--max-length-multipart-name (multipart-name min)
419  "Convenience function to compute the longest string.
420 
421 Return the longest string in MULTIPART-NAME, which is a list of
422 strings, or MIN if it is longer."
423  (seq-reduce (lambda (namelength namepart) (max namelength (length namepart)))
424  multipart-name
425  min))
426 
427 (defun uml--space-out-timelines (timelines messages prefix)
428  "Space out TIMELINES to fit MESSAGES' labels and PREFIX."
429  (dotimes (ii (length timelines))
430  (plist-put (nth ii timelines) 'center (+ (* 12 ii) 6 (length prefix))))
431  (let (elt needed namelen)
432  (dotimes (ii (length timelines))
433  (setq elt (nth ii timelines))
434  (setq namelen (uml--max-length-multipart-name (plist-get elt 'name) 8))
435  (setq needed (floor (/ (- namelen 8) 2)))
436  (when (> needed 0)
437  (uml--shift-to-the-right timelines ii needed)
438  (uml--shift-to-the-right timelines (1+ ii) needed))))
439 
440  (dolist (elt messages)
441  (let* ((to (plist-get elt 'to))
442  (from (plist-get elt 'from))
443  (left (min to from))
444  (right (max to from)))
445  (if (= left right)
446  (if (< (1+ left) (length timelines))
447  (uml--fit-label-between timelines ; self arrow
448  left
449  (1+ left)
450  (+ (uml--max-length-multipart-name (plist-get elt 'label) 0) 8)))
451  (uml--fit-label-between timelines
452  left
453  right
454  (+ (uml--max-length-multipart-name (plist-get elt 'label) 0) 4))))))
455 
456 (defun uml--count-timeline-name-rows (timelines)
457  "Count the rows of the TIMELINES' names."
458  (seq-reduce (lambda (val elt) (max val (length (plist-get elt 'name))))
459  timelines 0))
460 
461 (defun uml--write-diagram (timelines messages prefix)
462  "Write the TIMELINES and MESSAGES using PREFIX to the buffer.
463 
464 This is done in two steps:
465 1. write timeline names
466 2. write messages"
467 
468  ;; 1. write timeline names
469  (let (numrows)
470  ;; determine the number of rows needed for the timeline names
471  (setq numrows (uml--count-timeline-name-rows timelines))
472  ;; then write them out to the buffer
473  (dotimes (ii numrows)
474  (if prefix
475  (insert prefix))
476  (dolist (elt timelines)
477  (let* ((parts (plist-get elt 'name))
478  (index (+ (- (length parts) numrows) ii))
479  (part (and (>= index 0) (nth index parts))))
480  (if part
481  (uml--write-text-centered-on part
482  (plist-get elt 'center)))))
483  (newline)))
484 
485  ;; 2. write messages
486  (dolist (elt messages)
487  (uml--write-vertical-space timelines prefix)
488  (newline)
489 
490  (let* ((text (plist-get elt 'label))
491  (from (plist-get elt 'from))
492  (to (plist-get elt 'to))
493  (fromcenter (plist-get (nth from timelines) 'center))
494  (tocenter (plist-get (nth to timelines) 'center))
495  (dashed (plist-get elt 'dashed))
496  selfmessage)
497  (setq selfmessage (= (plist-get elt 'from) (plist-get elt 'to)))
498 
499  (if selfmessage
500  (uml--write-self-arrow timelines prefix fromcenter text)
501  (uml--write-label-and-arrow timelines prefix fromcenter tocenter text dashed))))
502 
503  (uml--write-vertical-space timelines prefix))
504 
505 (defun uml--redraw-sequence-diagram (adjust)
506  "Redraws a sequence diagram after applying ADJUST. This is the main routine."
507  (let (top ; first line in buffer of diagram
508  bottom ; last line in buffer of diagram
509  prefix ; comment character or nil
510  timelines ; list of timeline data
511  messages) ; list of arrow data
512 
513  (beginning-of-line)
514 
515  ;; find the top and bottom of the diagram
516  (setq top (uml--find-top-or-bottom :top))
517  (setq bottom (uml--find-top-or-bottom :bottom))
518  ;; (message "top: %d bottom: %d" top bottom)
519 
520  (goto-char top)
521  (setq prefix (uml--determine-prefix))
522 
523  ;; parse timeline names from old diagram
524  (setq timelines (uml--parse-timelines prefix bottom))
525  ;; (message "timelines %s" timelines)
526 
527  ;; parse messages from old diagram
528  (setq messages (uml--parse-messages timelines prefix bottom))
529  ;; (message "messages %s" messages)
530 
531  ;; clear the old diagram content from the buffer
532  (goto-char top)
533  (delete-char (- bottom top))
534 
535  ;; apply adjustments such as shifts or swaps
536  (let (ret)
537  (setq ret (uml--apply-adjustments adjust timelines messages))
538  (setq timelines (car ret)
539  messages (cdr ret)))
540 
541  ;; calculate timeline center columns
542  (uml--space-out-timelines timelines messages prefix)
543 
544  ;; render the diagram into the buffer
545  (uml--write-diagram timelines messages prefix)
546 
547  ;; move the cursor back to the column where it was before we did anything
548  (goto-char top)
549  (when (plist-get adjust 'movetocol)
550  (forward-line (1- (uml--count-timeline-name-rows timelines)))
551  (move-to-column (plist-get (nth (plist-get adjust 'movetocol) timelines) 'center)))))
552 
553 ;;;###autoload
554 (define-minor-mode uml-mode
555  "Toggle uml mode.
556 Interactively with no argument, this command toggles the mode.
557 A positive prefix argument enables the mode, any other prefix
558 argument disables it. From Lisp, argument omitted or nil enables
559 the mode, `toggle' toggles the state.
560 
561 When uml mode is enabled, C-c while the point is in a
562 sequence diagram cleans up the formatting of the diagram.
563 See the command \\[uml-seqence-diagram]."
564  ;; The initial value.
565  :init-value nil
566  ;; The indicator for the mode line.
567  :lighter " uml"
568  ;; The minor mode bindings.
569  :keymap
570  `((,(kbd "C-c C-c") . uml-sequence-diagram)
571  (,(kbd "<M-left>") . uml-swap-left)
572  (,(kbd "<M-right>") . uml-swap-right)
573  (,(kbd "<M-S-left>") . uml-delete-timeline)
574  (,(kbd "<M-S-right>") . uml-insert-timeline)
575  (,(kbd "M-f") . uml-forward-timeline)
576  (,(kbd "M-b") . uml-back-timeline))
577  :group 'uml)
578 
579 (provide 'uml-mode)
580 
581 ;;; uml-mode.el ends here