mte/unikernel/duniverse/dune_/editor-integration/emacs/dune-watch.el

198 lines
7 KiB
EmacsLisp
Raw Normal View History

2025-11-11 02:07:51 +01:00
;;; dune-watch.el --- provides integration with dune watch -*- lexical-binding: t; -*-
;; Copyright (C) 2021 Kiran Gopinathan
;; Author: Kiran Gopinathan <kirang@comp.nus.edu.sg>
;; Keywords:
;; This file is not part of GNU Emacs.
;;; Commentary:
;; This package complements dune mode with integration with --watch
;; tasks.
;; Permission to use, copy, modify, and distribute this software for
;; any purpose with or without fee is hereby granted, provided that
;; the above copyright notice and this permission notice appear in
;; all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
;; LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;; Code:
(require 'subr-x)
;;;; Customisation
(defgroup dune-watch nil
"Customisation group for dune-watch-minor-mode."
:prefix "dune-watch-mode-"
:group 'applications)
;;;;; Command configuration
(defcustom dune-watch-default-command "build"
"Default command for dune-watch."
:type 'string
:group 'dune-watch)
(defcustom dune-watch-command-format
"opam exec -- dune %s --watch"
"Format of command to run to invoke dune watch mode."
:type 'string
:group 'dune-watch)
(defcustom dune-watch-delete-buffer-on-termination t
"Whether to delete the dune watch buffer when the dune watch process terminates."
:type 'bool
:group 'dune-watch)
(defcustom dune-watch-popup-function #'display-buffer-pop-up-window
"Function passed to `display-buffer' to present compilation output to user."
:type 'function
:group 'dune-watch)
(defcustom dune-watch-read-command t
"Whether the user should be prompted to select a build task."
:type 'bool
:group 'dune-watch)
;;;; Constants
(defconst dune-watch-buffer-name "*dune-watch*"
"Name of buffer used to track buffer outputs.")
(defconst dune-watch-process-name "*dune-watch-process*"
"Name of process used to run dune watch.")
(defconst dune-watch-header "********** NEW BUILD"
"Prefix of the header of dune watch output.")
(defconst dune-watch-read-prompt "dune task (default: %s): "
"Prompt displayed to the user to dune task.")
(defconst dune-watch-supported-commands '("build" "test")
"List of dune build tasks supported by dune-watch.")
;;;; Global variables
(defvar dune-watch-task-history nil
"Variable used to track history of dune build tasks.")
;;;; Local variables
(defvar-local dune-watch-buffer nil
"Buffer corresponding to dune watch for current program")
(defvar-local dune-watch-process nil
"Process corresponding to dune watch for current program")
(defvar-local dune-watch-header-start nil
"Start of the header of the most recent dune watch output.")
;;;; Implementation
(defun dune-watch-generate-new-buffer ()
"Return a new buffer to be used by dune watch."
(let ((buffer (generate-new-buffer dune-watch-buffer-name)))
(with-current-buffer buffer
(compilation-mode)
(setq dune-watch-buffer buffer)
(setq dune-watch-process nil)
(setq dune-watch-header-start nil))
buffer))
(defun dune-watch-sentinel-function (watch-buffer process event)
"Process sentinel used by dune-watch-minor-mode.
WATCH-BUFFER is the buffer used by dune watch.
PROCESS is the dune-watch process name
EVENT is the text output by the sentinel."
(when (and dune-watch-delete-buffer-on-termination (buffer-live-p watch-buffer))
(kill-buffer watch-buffer))
(message "Dune watch process %s terminated with message \"%s\"" process (string-trim event)))
(defun dune-watch-update-header-start ()
"Update the position of the header of the most recent dune watch output."
(save-excursion
(goto-char (point-max))
(setq dune-watch-header-start
(search-backward dune-watch-header nil t))))
(defun dune-watch-beautify-buffer ()
"Cleans dune watch buffer by removing all but the last build output."
(let ((inhibit-read-only t)
(buffer-start (point-min)))
(when (and dune-watch-header-start buffer-start)
(delete-region buffer-start dune-watch-header-start)
(setq dune-watch-header-start buffer-start))))
(defun dune-watch-contains-errors ()
"Determines whether the dune watch buffer output contain any errors."
(save-excursion
(when dune-watch-header-start
(goto-char dune-watch-header-start)
(search-forward "File" nil t))))
(defun dune-watch-popup-buffer ()
"Pops up compilation output to user."
(when dune-watch-buffer
(display-buffer dune-watch-buffer dune-watch-popup-function)))
(defun dune-watch-filter-function (watch-buffer process event)
"Process filter function used by dune watch.
WATCH-BUFFER is the buffer corresponding to the process.
PROCESS is the name of the process.
EVENT is the string returned by the dune watch."
(when (and watch-buffer (buffer-live-p watch-buffer))
(with-current-buffer watch-buffer
(let ((inhibit-read-only t)
(buffer-end (point-max)))
(goto-char buffer-end)
(insert event)
(dune-watch-update-header-start)
(dune-watch-beautify-buffer)
(when (dune-watch-contains-errors)
(dune-watch-popup-buffer))))))
(defun dune-watch-on-kill-buffer ()
"Kill dune-watch process when main buffer is killed."
(when (and dune-watch-minor-mode dune-watch-process (process-live-p dune-watch-process))
(kill-process dune-watch-process)))
(defun start-dune-watch (command)
"Start a subprocess to run the dune COMMAND using the --watch flag."
(message "starting process to watch %s task..." command)
(let* ((buffer (dune-watch-generate-new-buffer))
(filter-process (apply-partially #'dune-watch-filter-function buffer))
(sentinel-process (apply-partially #'dune-watch-sentinel-function buffer))
(command (format dune-watch-command-format command)))
(setq dune-watch-buffer buffer)
(setq dune-watch-process (make-process
:name dune-watch-process-name
:buffer buffer
:command (split-string command)
:filter filter-process
:sentinel sentinel-process))
(add-hook 'kill-buffer-hook #'dune-watch-on-kill-buffer)))
(define-minor-mode dune-watch-minor-mode "A minor mode to run dune commands"
:group 'dune-watch
:init-value nil
(let ((command dune-watch-default-command))
(when dune-watch-read-command
(setq command (or (completing-read
(format dune-watch-read-prompt dune-watch-default-command)
dune-watch-supported-commands
nil nil dune-watch-default-command 'dune-watch-task-history)
command)))
(start-dune-watch command)))
(provide 'dune-watch)
;;; dune-watch.el ends here