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 -*- 3 ;; Copyright (C) 2015-2020 Ian Martins 5 ;; Author: Ian Martins <ianxm@jhu.edu> 6 ;; URL: http://github.com/ianxm/emacs-uml 9 ;; Package-Requires: ((emacs "24.4") seq) 11 ;; This file is not part of GNU Emacs. 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. 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. 23 ;; For a full copy of the GNU General Public License 24 ;; see <http://www.gnu.org/licenses/>. 28 ;; provides functions that help in writing ascii uml sequence diagrams. 35 (defun uml-forward-timeline () 36 "Move the point to the next timeline bar." 45 (not (eq ?| (char-after))) 49 (defun uml-back-timeline () 50 "Move the point to the previous timeline bar." 59 (not (eq ?| (char-after))) 63 (defun uml-swap-left () 64 "Swap the timeline at the point with the timeline to its left." 66 (uml--redraw-sequence-diagram (list 'name :swapleft 'col (current-column)))) 68 (defun uml-swap-right () 69 "Swap the timeline at the point with the timeline to its right." 71 (uml--redraw-sequence-diagram (list 'name :swapright 'col (current-column)))) 73 (defun uml-delete-timeline () 74 "Delete the timeline at point." 76 (uml--redraw-sequence-diagram (list 'name :delete 'col (current-column)))) 78 (defun uml-insert-timeline () 79 "Insert a timeline to the right of the point." 81 (uml--redraw-sequence-diagram (list 'name :insert 'col (current-column)))) 83 (defun uml-sequence-diagram () 84 "Formats a sequence diagram." 86 (uml--redraw-sequence-diagram nil)) 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)))) 95 (defun uml--write-vertical-space (timelines prefix) 96 "Write a row of empty timeline bars for TIMELINES after writing PREFIX." 99 (dolist (elt timelines) 100 (let* ((col (plist-get elt 'center))) 101 (move-to-column col t) 102 (insert (format "|"))))) 104 (defun uml--find-nearest-timeline (timelines col) 105 "Return the index of the nearest of TIMELINES to the column COL." 110 (dolist (elt timelines) 111 (setq delta (abs (- col (plist-get elt 'origcenter)))) 112 (when (or (not ret) (< delta olddelta)) 114 (setq olddelta delta)) 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))) 122 on) ; bool to toggle between dash or space 123 (move-to-column (1+ (min to from))) 124 (if (> from to) ; <--- 126 (while (< ii (- delta 2)) 127 (insert (if (or (not dashed) on) ?- ? )) 128 (if on (setq on nil) (setq on t)) ; toggle dash 130 (if (< from to) ; ---> 132 (delete-char (- delta 1)))) 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." 139 (dotimes (ii (length text)) 140 (uml--write-vertical-space timelines prefix) 143 (setq center (floor (/ (+ fromcol tocol) 2))) 144 (uml--write-text-centered-on (nth ii text) center) 145 (delete-char (length (nth ii text))) 149 (uml--write-vertical-space timelines prefix) 152 (uml--write-arrow fromcol tocol dashed) 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) 161 ((= (- numrows ii) 2) " --.") 162 ((= (- numrows ii) 1) "<--'") 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) 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)))) 176 (defun uml--fit-label-between (timelines left right width) 177 "Spread out TIMELINES so that LEFT and RIGHT have WIDTH space between them." 181 (setq leftcol (plist-get (nth left timelines) 'center)) 182 (setq rightcol (plist-get (nth right timelines) 'center)) 183 (setq needed (- (+ leftcol width) rightcol)) 185 (uml--shift-to-the-right timelines right needed)))) 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." 191 (while (< ii (length timelines)) 192 (setq elt (nth ii timelines)) 193 (plist-put elt 'center (+ (plist-get elt 'center) needed)) 196 (defun uml--swap-timelines (timelines messages col1 col2) 197 "Given all TIMELINES and MESSAGES, swap COL1 and COL2." 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))))) 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))) 213 (not (= (point) end-of-buffer)) 214 (not (looking-at "^[^[:word:]|]*$"))) 218 (if (looking-at "^[^[:word:]|]*$") 221 ((eq direction :bottom) 222 (if (not (= (point) (point-max))) 224 (line-end-position))))) 226 (defun uml--calc-middle (start end) 227 "This just computes the integer mean of START and END." 228 (floor (/ (+ start end) 2))) 230 (defun uml--determine-prefix () 231 "Determine the prefix (if there is one). 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:]]+\\) ") 242 (defun uml--parse-timelines (prefix bottom) 243 "Parse the timeline names. 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: 250 [ (name \"timeline1\" origcenter 5) ... ] 252 Names can contain any characters except whitespace or pipes." 254 (while (and (looking-at (concat prefix "[^|]+$")) 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 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 281 (sort timelines (lambda (a b) (< (plist-get a 'origcenter) 282 (plist-get b 'origcenter)))))) 284 (defun uml--parse-messages (timelines prefix bottom) 285 "Parse the messages from the diagram. 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. 292 (from 0 to 2 label (\"doIt()\") dashed nil) 294 Labels must start with a number or letter and cannot contain 295 spaces, angle brackets or dashes. 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))) 303 (forward-char (length prefix)) 305 ;; the label may be above the message or on the same line 306 (when (re-search-forward "[[:word:]][^\n|<>\-]*" (line-end-position) t) 308 (setq label (list (string-trim-right (match-string 0)))) ; single part 309 (nconc label (list (string-trim-right (match-string 0))))) ; multi part 312 ;; FOUND is (from . to) where FROM and TO are timeline indices 313 (setq found (uml--find-message-bounds-maybe timelines)) 317 (setq dashed (re-search-forward "\- \-" (line-end-position) t)) 318 (setq messages (append messages (list (list 'label label 325 (defun uml--find-message-bounds-maybe (timelines) 326 "Find which timelines a message connects. 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 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)))) 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)))) 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)))) 348 ((re-search-forward "|\-" (line-end-position) t) ; |- 349 (setq from (uml--find-nearest-timeline timelines (- (match-beginning 0) (line-beginning-position)))) 351 (if (< to (length timelines)) 353 (message "Ignoring out of bounds message."))) 355 ((re-search-forward "\-|" (line-end-position) t) ; -| 356 (setq from (uml--find-nearest-timeline timelines (- (match-beginning 0) (line-beginning-position)))) 360 (message "Ignoring out of bounds message.")))) 361 (if found (cons from to) nil))) 363 (defun uml--apply-adjustments (adjust timelines messages) 364 "Apply ADJUST to TIMELINES and MESSAGES. 366 Return (TIMELINES . MESSAGES) since we mucked with both of them." 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)))) 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)))) 386 ((eq :delete (plist-get adjust 'name)) 388 (setq current (uml--find-nearest-timeline timelines (plist-get adjust 'col)) 390 (plist-put adjust 'movetocol (max 0 (1- col))) 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))))))))) 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") 408 (setq rest (nthcdr current timelines)) 409 (setcdr (nthcdr (- current 1) timelines) new) 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)) 418 (defun uml--max-length-multipart-name (multipart-name min) 419 "Convenience function to compute the longest string. 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))) 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))) 437 (uml--shift-to-the-right timelines ii needed) 438 (uml--shift-to-the-right timelines (1+ ii) needed)))) 440 (dolist (elt messages) 441 (let* ((to (plist-get elt 'to)) 442 (from (plist-get elt 'from)) 444 (right (max to from))) 446 (if (< (1+ left) (length timelines)) 447 (uml--fit-label-between timelines ; self arrow 450 (+ (uml--max-length-multipart-name (plist-get elt 'label) 0) 8))) 451 (uml--fit-label-between timelines 454 (+ (uml--max-length-multipart-name (plist-get elt 'label) 0) 4)))))) 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)))) 461 (defun uml--write-diagram (timelines messages prefix) 462 "Write the TIMELINES and MESSAGES using PREFIX to the buffer. 464 This is done in two steps: 465 1. write timeline names 468 ;; 1. write timeline names 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) 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)))) 481 (uml--write-text-centered-on part 482 (plist-get elt 'center))))) 486 (dolist (elt messages) 487 (uml--write-vertical-space timelines prefix) 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)) 497 (setq selfmessage (= (plist-get elt 'from) (plist-get elt 'to))) 500 (uml--write-self-arrow timelines prefix fromcenter text) 501 (uml--write-label-and-arrow timelines prefix fromcenter tocenter text dashed)))) 503 (uml--write-vertical-space timelines prefix)) 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 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) 521 (setq prefix (uml--determine-prefix)) 523 ;; parse timeline names from old diagram 524 (setq timelines (uml--parse-timelines prefix bottom)) 525 ;; (message "timelines %s" timelines) 527 ;; parse messages from old diagram 528 (setq messages (uml--parse-messages timelines prefix bottom)) 529 ;; (message "messages %s" messages) 531 ;; clear the old diagram content from the buffer 533 (delete-char (- bottom top)) 535 ;; apply adjustments such as shifts or swaps 537 (setq ret (uml--apply-adjustments adjust timelines messages)) 538 (setq timelines (car ret) 541 ;; calculate timeline center columns 542 (uml--space-out-timelines timelines messages prefix) 544 ;; render the diagram into the buffer 545 (uml--write-diagram timelines messages prefix) 547 ;; move the cursor back to the column where it was before we did anything 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))))) 554 (define-minor-mode 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. 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. 566 ;; The indicator for the mode line. 568 ;; The minor mode bindings. 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)) 581 ;;; uml-mode.el ends here