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))))