Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

d2fmt: D2 Format Support w/ Autosave #37

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ Note: All compile commands will open the output in a buffer to view the resultin

## Customization

### `d2fmt` on Save

Add this to your `init.el` file to auto fmt on save

```elisp
(add-hook 'before-save-hook #d2fmt-before-save)
```

### `d2` binary location

You can specify the location of `d2` with the variable `d2-location`, the default assumes you have the binary in your `PATH` (and for that you probably want/need to install [`d2`](https://github.com/andorsk/d2-mode)).
Expand Down
106 changes: 106 additions & 0 deletions d2-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,23 @@ STR is the declaration."
(cons (- l (line-number-at-pos)) (current-indentation))
(cons -1 -1)))))

(defcustom d2format-args nil
"Additional arguments to pass to d2format."
:type '(repeat string)
:group 'd2)

(defcustom d2format-show-errors 'buffer
"Where to display d2format error output.
It can either be displayed in its own buffer, in the echo area, or not at all.
Please note that Emacs outputs to the echo area when writing
files and will overwrite d2format's echo output if used from inside
a `before-save-hook'."
:type '(choice
(const :tag "Own buffer" buffer)
(const :tag "Echo area" echo)
(const :tag "None" nil))
:group 'd2)

(defun d2-compile ()
"Compile the current d2 file using d2."
(interactive)
Expand Down Expand Up @@ -199,6 +216,85 @@ Optional argument BROWSE whether to open the browser."
(interactive)
(browse-url "https://github.com/terrastruct/d2"))


(defun d2format()
"Format the current buffer according to the formatting tool.
Code was heavily inspired by gofmt.
https://github.com/dominikh/go-mode.el/\
blob/166dfb1e090233c4609a50c2ec9f57f113c1da72/go-mode.el
The tool used can be set via ‘d2-location’ (default: d2format) and additional
arguments can be set as a list via ‘d2format-args’."

(interactive)
(let ((tmpfile (make-nearby-temp-file "d2format" nil ".d2"))
(patchbuf (get-buffer-create "*d2format patch*"))
(errbuf (if d2format-show-errors (get-buffer-create "*d2format Errors*")))
(coding-system-for-read 'utf-8)
(our-d2format-args "")
(coding-system-for-write 'utf-8))

(unwind-protect
(save-restriction
(widen)
(if errbuf
(with-current-buffer errbuf
(setq buffer-read-only nil)
(erase-buffer)))
(with-current-buffer patchbuf
(erase-buffer))

(write-region nil nil tmpfile)

(message "Calling d2format: %s %s" d2-location "fmt")

(if (zerop (apply #'process-file d2-location nil errbuf nil ""))
(message "processing file")
(progn
(if (zerop (let ((local-copy (file-local-copy tmpfile)))
(message (concat "running local copy " local-copy))
(unwind-protect
(call-process-region
(point-min) (point-max) "diff" nil patchbuf
nil "-n" "-" (or local-copy tmpfile))
(when local-copy (delete-file local-copy)))))
(message "Buffer is already d2format")
(message "Applied d2format"))
(if errbuf (d2format--kill-error-buffer errbuf)))
(message "Could not apply d2format")
(if errbuf (d2format--process-errors (buffer-file-name) tmpfile errbuf))))
(kill-buffer patchbuf)
(delete-file tmpfile))))

(defun d2format--kill-error-buffer (errbuf)
"Utility function that kill the error buffer.
Argument ERRBUF the buffer which contains the error message."
(let ((win (get-buffer-window errbuf)))
(if win
(quit-window t win)
(kill-buffer errbuf))))

(defun d2format--process-errors (filename tmpfile errbuf)
"Utility function that provides error handling.
Argument FILENAME the input file.
Argument TMPFILE the path to the temporary file.
Argument ERRBUF error buffer."
(with-current-buffer errbuf
(if (eq d2format-show-errors 'echo)
(progn
(message "%s" (buffer-string))
(d2format--kill-error-buffer errbuf))
;; Convert the d2format stderr to something understood by the compilation mode.
(goto-char (point-min))
(insert "d2format errors:\n")
(let ((truefile tmpfile))
(while (search-forward-regexp
(concat "^\\(" (regexp-quote (file-local-name truefile))
"\\):")
nil t)
(replace-match (file-name-nondirectory filename) t t nil 1)))
(compilation-mode)
(display-buffer errbuf))))

(defvar d2-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") 'd2-compile)
Expand All @@ -221,6 +317,16 @@ Optional argument BROWSE whether to open the browser."
(setq-local comment-end "")
(setq-local comment-start-skip "%%+ *"))

(defun d2format-before-save ()
"Add this to .emacs to run d2format on the current buffer when saving:
\=(add-hook 'before-save-hook 'd2format-before-save).
Note that this will cause ‘d2-mode’ to get loaded the first time
you save any file, kind of defeating the point of autoloading."

(interactive)
(when (eq major-mode 'd2-mode) (d2format)))


(provide 'd2-mode)

;;; d2-mode.el ends here