2025-08-11 Using Eldoc to show entities with given uuids in the echo area

This is the culmination of my series of posts on customization variables, timestamps, ElDoc, PostgreSQL and interacting with potentially unreliable external processes via stdin and stdout. All the time, my goal was to be able to show the database row corresponding to a uuid when the point is on that uuid. Since most of the hard work was already done in the preceding six posts, this one will be rather short (not including the code itself).

We already know how to write an ElDoc-compatible function which can check if the point is on some kind of entity and do something about this entity if yes. Last time it was easier, since the entity in question was a simple integer, and we could process it synchronously. This time the processing will be asynchronous – we’ll need to send the uuid to the program we’ve written earlier, and it is going to take some time to get the information we need from the database. One thing which makes this less nice than it could be is how ElDoc works with asynchronous code. The eldoc-documentation-functions hook must contain functions accepting a callback and, as the manual says, it should “arrange for CALLBACK to be called at a later time, using asynchronous processes or other asynchronous mechanisms”. The problem is, the code which will need to call that callback is the process filter, and the only way I know for the process filter to receive the callback is to pass it in some global variable. It’s very far from elegant and probably can give rise to some race conditions, when the user moves the point quickly between uuids, but I really don’t see any sensible alternative. Here is my solution.

(defvar show-row-by-uuid-callback nil
  "Callback to show the entity by uuid.")

(defun show-row-by-uuid-eldoc-function (callback)
  "An ElDoc-compatible wrapper around TODO."
  (when-let* ((uuid-at-point (thing-at-point 'uuid)))
    (setq show-row-by-uuid-callback callback)
    (show-row-by-uuid-send-input uuid-at-point)))

(define-minor-mode show-row-by-uuid-mode
  "Toggle ElDoc Timestamp Conversion mode.
When enabled, this mode causes numbers at point to be displayed as
timestamps in the echo area using ElDoc (which must be enabled, too).
Numbers greater than `maximum-unix-timestamp-for-conversion' are treated
as JavaScript `Date's and the rest as Unix timestamps (seconds since
1970-01-01)."
  :init-value nil
  :global t
  (if show-row-by-uuid-mode
      (progn
        (add-hook 'eldoc-documentation-functions
                  #'show-row-by-uuid-eldoc-function
                  nil nil)
        (show-row-by-uuid-start-process))
    (remove-hook 'eldoc-documentation-functions
                 #'show-row-by-uuid-eldoc-function
                 nil)
    (show-row-by-uuid-stop-process)))

(defvar show-row-by-uuid-process--process nil
  "The show-row-by-uuid process.")

(defvar show-row-by-uuid-process--auto-restart-p t
  "Non-nil means restart the show-row-by-uuid process after it dies.")

(defun show-row-by-uuid-process--sentinel (process event)
  "Restart the show-row-by-uuid process if it is dead."
  (when (and show-row-by-uuid-process--auto-restart-p
             (not (process-live-p show-row-by-uuid-process--process)))
    (message "Show-Row-By-Uuid process died, restarting.")
    (show-row-by-uuid-start-process)))

(defun show-row-by-uuid-start-process ()
  "Start `show-row-by-uuid.js' in the background unless already started."
  (interactive)
  (unless (process-live-p show-row-by-uuid-process--process)
    (setq show-row-by-uuid-process--auto-restart-p t)
    (setq show-row-by-uuid-process--process
          (start-process "show-row-by-uuid"
                         (get-buffer-create "*show-row-by-uuid*")
                         "show-row-by-uuid.js"))
    (set-marker-insertion-type
     (process-mark show-row-by-uuid-process--process) t)
    (setq show-row-by-uuid-process--output-start
          (copy-marker
           (process-mark show-row-by-uuid-process--process)))
    (set-process-filter show-row-by-uuid-process--process
                        #'show-row-by-uuid-process-filter)
    (set-process-sentinel show-row-by-uuid-process--process
                          #'show-row-by-uuid-process--sentinel)))

(defun show-row-by-uuid-stop-process ()
  "Stop the show-row-by-uuid process and do not restart it."
  (interactive)
  (setq show-row-by-uuid-process--auto-restart-p nil)
  (kill-process show-row-by-uuid-process--process))

(defun show-row-by-uuid-process--start-countdown ()
  "Count down to restart the show-row-by-uuid process when it hangs."
  (show-row-by-uuid-process--stop-countdown)
  (setq show-row-by-uuid-process--timeout-timer
        (run-with-timer show-row-by-uuid-process-timeout
                        nil
                        #'show-row-by-uuid-process--restart-process)))

(defun show-row-by-uuid-process--stop-countdown ()
  "Stop the countdown, see `show-row-by-uuid-process--start-countdown'."
  (when (timerp show-row-by-uuid-process--timeout-timer)
    (cancel-timer show-row-by-uuid-process--timeout-timer)))

(defun show-row-by-uuid-send-input (show-row-by-uuid-input)
  "Send INPUT to the show-row-by-uuid process."
  (interactive "sinput: \n")
  (if (process-live-p show-row-by-uuid-process--process)
      (with-current-buffer
          (process-buffer show-row-by-uuid-process--process)
        (goto-char (process-mark show-row-by-uuid-process--process))
        (insert show-row-by-uuid-input "\n")
        (setq show-row-by-uuid-process--output-start (point-marker))
        (process-send-string show-row-by-uuid-process--process
                             (concat show-row-by-uuid-input "\n"))
        (show-row-by-uuid-process--start-countdown))
    (user-error "show-row-by-uuid process is not alive")))

(defvar show-row-by-uuid-process--output-start nil
  "The place the current output should start.")

(defcustom show-row-by-uuid-process-timeout 2
  "The time (in seconds) to wait for output from `show-row-by-uuid.js'.")

(defvar show-row-by-uuid-process--timeout-timer nil
  "The timer to restart the show-row-by-uuid process when it times out.")

(defun show-row-by-uuid-process--restart-process ()
  "Restart the show-row-by-uuid process.
It does so by just killing the process; the actual restarting is handled
by the sentinel."
  (when (processp show-row-by-uuid-process--process)
    (kill-process show-row-by-uuid-process--process)))

(defun show-row-by-uuid-show-entity (entity-data)
  "Show ENTITY-DATA in the echo area.
Use `show-row-by-uuid-callback' if non-nil or `message' otherwise."
  (if show-row-by-uuid-callback
      (let ((eldoc-echo-area-use-multiline-p t)
            (print-length nil))
        (funcall show-row-by-uuid-callback
                 (format "%s" (plist-get entity-data :entity))
                 :thing (plist-get entity-data :table)
                 :face 'font-lock-keyword-face)
        (setq show-row-by-uuid-callback nil))
    (message "%s" entity-data)))

(defun show-row-by-uuid-process-filter (process output)
  "Insert OUTPUT to the buffer of PROCESS.
Also, message the user."
  (let ((buffer (process-buffer process)))
    (when (buffer-live-p buffer)
      (with-current-buffer buffer
        (save-excursion
          (goto-char (process-mark process))
          (insert output))
        (save-excursion
          (goto-char show-row-by-uuid-process--output-start)
          (if-let* ((output-end
                     (and (re-search-forward "^uuid:$" nil t)
                          (match-beginning 0))))
              (progn (goto-char show-row-by-uuid-process--output-start)
                     (condition-case error
                         (unless (bobp)
                           (show-row-by-uuid-show-entity
                            (json-parse-buffer :object-type 'plist)))
                       (json-parse-error
                        (message "invalid JSON received: %s"
                                 (buffer-substring-no-properties
                                  show-row-by-uuid-process--output-start
                                  (1- output-end)))))
                     (setq show-row-by-uuid-process--output-start
                           (1+ (match-end 0)))
                     (setq mark show-row-by-uuid-process--output-start)
                     (show-row-by-uuid-process--stop-countdown))
            (show-row-by-uuid-process--start-countdown)))))))

That code is still prototype-ish a bit, and I’m not even sure how to make it much better. For example, my mode does not automatically turn ElDoc mode on in JSON files opened before it was started. (The relation between ElDoc mode and Global ElDoc mode is still a bit mysterious to me, by the way.) The JSON provided by my script is formatted as a plist, which is probably not ideal, but works good enough for me.

Anyway, that’s it for now. I have some ideas how to make this work even better, but that will need to wait for another time.

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryPostgreSQL