branch: elpa/racket-mode
commit cf9227740a240c46da9d32da2da4669655fd148d
Author: Greg Hendershott <g...@greghendershott.com>
Commit: Greg Hendershott <g...@greghendershott.com>

    Improve UX for back end startup failures; closes #744
    
    When the back end cannot start, display information in a new,
    dedicated Emacs buffer -- a better experience than something flashing
    by in the echo area and then only being viewable if user opens
    the *Messages* buffer.
    
    Furthermore, main.rkt now dynamic-require's command-server.rkt under an
    exn handler that looks for exn:fail:syntax:missing-module. In that
    case the dedicated buffer includes a browse-url button to go directly
    to our documentation about using Minimal Racket.
---
 racket-cmd.el    | 47 +++++++++++++++++++++++++++++++++++++++++-
 racket/image.rkt |  5 +++--
 racket/main.rkt  | 62 ++++++++++++++++++++++++++++++++++++++++++++++----------
 3 files changed, 100 insertions(+), 14 deletions(-)

diff --git a/racket-cmd.el b/racket-cmd.el
index 6723da554f..00f215dc0c 100644
--- a/racket-cmd.el
+++ b/racket-cmd.el
@@ -1,6 +1,6 @@
 ;;; racket-cmd.el -*- lexical-binding: t; -*-
 
-;; Copyright (c) 2013-2022 by Greg Hendershott.
+;; Copyright (c) 2013-2025 by Greg Hendershott.
 ;; Portions Copyright (C) 1985-1986, 1999-2013 Free Software Foundation, Inc.
 
 ;; Author: Greg Hendershott
@@ -75,6 +75,7 @@ Before doing anything runs the hook 
`racket-stop-back-end-hook'."
   ;; Avoid excess processes/buffers like "racket-process<1>".
   (when (racket--cmd-open-p)
     (racket--cmd-close))
+  (racket--kill-startup-error-buffer)
   ;; Give the process buffer the current values of some vars; see
   ;; <https://github.com/purcell/envrc/issues/22>.
   (cl-letf* (((default-value 'process-environment) process-environment)
@@ -214,6 +215,8 @@ Although mostly these are 1:1 responses to command 
requests, some
 like \"logger\", \"debug-break\", and \"hash-lang\" are
 notifications."
   (pcase response
+    (`(startup-error ,kind ,data)
+     (run-at-time 0.001 nil #'racket--on-startup-error kind data))
     (`(logger ,str)
      (run-at-time 0.001 nil #'racket--logger-on-notify back-end str))
     (`(debug-break . ,response)
@@ -362,6 +365,48 @@ in a specific namespace."
                        (error "Unknown response to command %S from %S to 
%S:\n%S"
                               command-sexpr buf name v)))))))
 
+;;; Back end startup error buffer
+
+(defconst racket--startup-error-buffer-name
+  "*Racket Mode back end startup failure*")
+
+(defun racket--kill-startup-error-buffer ()
+  (let ((buf (get-buffer racket--startup-error-buffer-name)))
+    (when (buffer-live-p buf)
+      (kill-buffer buf))))
+
+(defun racket--on-startup-error (kind data)
+  (let ((buf (get-buffer-create racket--startup-error-buffer-name)))
+    (with-current-buffer buf
+      (unless (eq major-mode 'special-mode)
+        (special-mode))
+      (visual-line-mode 1)
+      (let ((buffer-read-only nil))
+        (erase-buffer)
+        (pop-to-buffer buf)
+        (pcase kind
+          ('missing-module
+           (let ((url "https://racket-mode.com/#Minimal-Racket-1";))
+             (insert "The Racket Mode back end could not start because it was 
unable to load the module "
+                     ?' data ?' "."
+                     "\n\n"
+                     "This could be because you did not install the full 
\"main distribution\" of Racket, but instead installed only \"Minimal Racket\" 
(the default when using homebrew)."
+                     "\n\n"
+                     "In that case, you will need either to install the full 
main distribution, or, manually install certain additional Racket packages."
+                     "\n\n"
+                     "Please see ")
+             (save-excursion ;leave point at start of link, for handy RET
+               (insert-button url
+                              'url url
+                              'face 'link
+                              'follow-link t
+                              'action (lambda (button)
+                                        (when-let (url (button-get button 
'url))
+                                          (browse-url url))))
+               (insert "."))))
+          (_
+           (insert data)))))))
+
 (provide 'racket-cmd)
 
 ;; racket-cmd.el ends here
diff --git a/racket/image.rkt b/racket/image.rkt
index 86dde9fb0b..50ba68e5cc 100644
--- a/racket/image.rkt
+++ b/racket/image.rkt
@@ -1,10 +1,11 @@
-;; Copyright (c) 2013-2022 by Greg Hendershott.
+;; Copyright (c) 2013-2025 by Greg Hendershott.
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 
 #lang racket/base
 
-;;; Portions Copyright (C) 2012 Jose Antonio Ortega Ruiz.
+;; Portions Copyright (C) 2012 Jose Antonio Ortega Ruiz.
 
+;; Limit imports to those supplied by Minimal Racket!
 (require file/convertible
          racket/file
          racket/format
diff --git a/racket/main.rkt b/racket/main.rkt
index 62841cbf93..fdd4e34456 100644
--- a/racket/main.rkt
+++ b/racket/main.rkt
@@ -1,23 +1,53 @@
-;; Copyright (c) 2013-2022 by Greg Hendershott.
+;; Copyright (c) 2013-2025 by Greg Hendershott.
 ;; SPDX-License-Identifier: GPL-3.0-or-later.
 
 #lang racket/base
 
+;; This module acts as a "shim" or "launcher" for command-server.rkt.
+;;
+;; We dynamic-require command-server.rkt within an exn handler for
+;; missing modules, to provide a better error UX when people are using
+;; Minimal Racket; see issue #744. Any such error is written to stdout
+;; as a "notification" for the Emacs front end, which can display it
+;; in a dedicated buffer. Not only is this better than error text
+;; flashing by in the echo bar and hiding in the *Messages* buffer,
+;; our dedicated can supply a browse-url button to our docs section
+;; about Minimal Racket.
+;;
+;; Note that the exn handler is active only during the dynamic extent
+;; of the dynamic-require to extract the command-server-loop function.
+;; Subsequently we call that function without any such handler in
+;; effect.
+;;
+;; Use the same notification mechanism for other back end startup
+;; failures, such as when they need a newer version of Racket.
+
+;; Limit imports to those supplied by Minimal Racket!
 (require racket/match
-         racket/port
+         (only-in racket/port open-output-nowhere)
+         racket/runtime-path
          (only-in racket/string string-trim)
          (only-in racket/system system/exit-code)
          version/utils
-         "command-server.rkt"
          (only-in "image.rkt" set-use-svg?!))
 
+;; Write a "notification" for the Emacs front end and exit.
+(define (notify/exit kind data)
+  (writeln `(startup-error ,kind ,data))
+  (flush-output)
+  (exit 13))
+
 (define (assert-racket-version minimum-version)
   (define actual-version (version))
   (unless (version<=? minimum-version actual-version)
-    (error '|Racket Mode back end| "Need Racket ~a or newer but ~a is ~a"
-           minimum-version
-           (find-executable-path (find-system-path 'exec-file))
-           actual-version)))
+    (notify/exit
+     'other
+     (format "Racket Mode needs Racket ~a or newer but ~a is ~a."
+             minimum-version
+             (find-executable-path (find-system-path 'exec-file))
+             actual-version))
+    (flush-output)
+    (exit 14)))
 
 (define (macos-sequoia-or-newer?)
   (and (eq? 'macosx (system-type 'os))
@@ -40,14 +70,24 @@
     [(vector "--use-svg" )       (set-use-svg?! #t)]
     [(vector "--do-not-use-svg") (set-use-svg?! #f)]
     [v
-     (error '|Racket Mode back end|
-            "Bad command-line arguments:\n~s\n" v)])
+     (notify/exit
+      'other
+      (format "Bad command-line arguments:\n~s\n" v))])
+
+  (define-runtime-path command-server.rkt "command-server.rkt")
+  (define command-server-loop
+    (with-handlers ([exn:fail:syntax:missing-module?
+                     (λ (e)
+                       (notify/exit
+                        'missing-module
+                        (format "~a" (exn:fail:syntax:missing-module-path 
e))))])
+      (dynamic-require command-server.rkt 'command-server-loop)))
 
   ;; Save original current-{input output}-port to give to
-  ;; command-server-loop for command I/O.
+  ;; command-server-loop for command I/O ...
   (let ([stdin  (current-input-port)]
         [stdout (current-output-port)])
-    ;; Set no-ops so e.g. rando print can't bork the command I/O.
+    ;; ... and set no-ops so rando print can't bork the command I/O.
     (parameterize ([current-input-port  (open-input-bytes #"")]
                    [current-output-port (open-output-nowhere)])
       (command-server-loop stdin stdout))))

Reply via email to