From 59aed1e84d19a22771a1b40ab36fc671978684d4 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 6 Mar 2026 13:45:57 +0200 Subject: [PATCH 1/7] Replace hard tuareg dependency with lazy loading tuareg is now loaded on demand only when its functions are actually needed (i.e., when the user is in tuareg-mode). This removes the mandatory (require 'tuareg) and the tuareg entry from Package-Requires, allowing utop.el to work without tuareg installed. --- src/top/utop.el | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/top/utop.el b/src/top/utop.el index 59a913e2..26775442 100644 --- a/src/top/utop.el +++ b/src/top/utop.el @@ -5,7 +5,7 @@ ;; URL: https://github.com/ocaml-community/utop ;; Licence: BSD3 ;; Version: 1.11 -;; Package-Requires: ((emacs "26") (tuareg "2.2.0")) +;; Package-Requires: ((emacs "26")) ;; Keywords: ocaml languages ;; This file is a part of utop. @@ -16,8 +16,8 @@ ;; ;; utop.el has two components - a nice OCaml REPL with auto-completion and a ;; minor mode (`utop-minor-mode'), which extends OCaml major modes -;; (e.g. `caml-mode' and `tuareg-mode') with commands to evaluate forms directly -;; in the REPL. +;; (e.g. `tuareg-mode', `caml-mode', `neocaml-mode') with commands to evaluate +;; forms directly in the REPL. ;; ;; See the "Integration with Emacs" section of the README for more info. @@ -27,7 +27,8 @@ (require 'pcase) (require 'seq) (require 'tabulated-list) -(require 'tuareg) +(declare-function tuareg-discover-phrase "tuareg" (&optional pos)) +(declare-function tuareg-skip-blank-and-comments "tuareg" ()) ;; +-----------------------------------------------------------------+ ;; | License | @@ -267,6 +268,7 @@ modes you need to set these variables: (defun utop-tuareg-next-phrase () "Move to the next phrase after point." + (require 'tuareg) (let* ((pos (save-excursion (when (looking-at-p "[;[:blank:]]*$") (skip-chars-backward ";[:blank:]") @@ -288,9 +290,14 @@ modes you need to set these variables: caml-skip-to-end-of-phrase reason-next-phrase)))) +(defun utop-tuareg-discover-phrase () + "Discover the phrase at point using tuareg." + (require 'tuareg) + (tuareg-discover-phrase)) + (defun utop-compat-discover-phrase () (funcall - (utop-compat-resolve '(tuareg-discover-phrase + (utop-compat-resolve '(utop-tuareg-discover-phrase typerex-discover-phrase caml-find-phrase reason-discover-phrase)))) From e26d9277ce2224fbe080dfc37b718d68c76759b7 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 6 Mar 2026 13:46:23 +0200 Subject: [PATCH 2/7] Replace hard-coded mode dispatch with extensible alist Introduce `utop-mode-compat-alist` that maps major modes to their phrase navigation functions. This replaces the old positional-index dispatch and makes it easy for any OCaml major mode (e.g. neocaml) to register itself without modifying utop.el. --- src/top/utop.el | 59 +++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/top/utop.el b/src/top/utop.el index 26775442..55521450 100644 --- a/src/top/utop.el +++ b/src/top/utop.el @@ -248,23 +248,21 @@ backend") ;; | Compatibility with different ocaml major modes | ;; +-----------------------------------------------------------------+ -(defun utop-compat-resolve (choices) - "Resolve a symbol based on the current major mode. CHOICES is a -list of 3 function symbols: (tuareg-symbol typerex-symbol caml-symbol)." - (nth - (pcase major-mode - ('tuareg-mode 0) - ('typerex-mode 1) - ('caml-mode 2) - ('reason-mode 3) - (major-mode (error (format "utop doesn't support the major mode \"%s\". It -supports caml, tuareg, typerex and reason modes by default. For other -modes you need to set these variables: - -- `utop-next-phrase-beginning' -- `utop-discover-phrase' -" major-mode)))) - choices)) +(defvar utop-mode-compat-alist + '((tuareg-mode . (utop-tuareg-next-phrase . utop-tuareg-discover-phrase)) + (typerex-mode . (typerex-skip-to-end-of-phrase . typerex-discover-phrase)) + (caml-mode . (caml-skip-to-end-of-phrase . caml-find-phrase)) + (reason-mode . (reason-next-phrase . reason-discover-phrase))) + "Alist mapping major modes to their phrase navigation functions. +Each entry is (MODE . (NEXT-PHRASE-FN . DISCOVER-PHRASE-FN)). + +NEXT-PHRASE-FN should move point to the beginning of the next phrase. +DISCOVER-PHRASE-FN should return a triple (begin-pos end-pos end-pos-with-comments). + +To add support for a new OCaml major mode, add an entry to this alist: + + (add-to-list \\='utop-mode-compat-alist + \\='(my-ocaml-mode . (my-next-phrase . my-discover-phrase)))") (defun utop-tuareg-next-phrase () "Move to the next phrase after point." @@ -283,24 +281,27 @@ modes you need to set these variables: (goto-char (match-end 0))) (tuareg-skip-blank-and-comments)))) -(defun utop-compat-next-phrase-beginning () - (funcall - (utop-compat-resolve '(utop-tuareg-next-phrase - typerex-skip-to-end-of-phrase - caml-skip-to-end-of-phrase - reason-next-phrase)))) - (defun utop-tuareg-discover-phrase () "Discover the phrase at point using tuareg." (require 'tuareg) (tuareg-discover-phrase)) +(defun utop-compat-lookup (slot) + "Look up a phrase function for the current major mode. +SLOT should be `car' for next-phrase or `cdr' for discover-phrase." + (let ((entry (assq major-mode utop-mode-compat-alist))) + (if entry + (funcall slot (cdr entry)) + (error "utop doesn't support the major mode \"%s\". +To add support, either customize `utop-mode-compat-alist' or +set `utop-next-phrase-beginning' and `utop-discover-phrase' +as buffer-local variables" major-mode)))) + +(defun utop-compat-next-phrase-beginning () + (funcall (utop-compat-lookup #'car))) + (defun utop-compat-discover-phrase () - (funcall - (utop-compat-resolve '(utop-tuareg-discover-phrase - typerex-discover-phrase - caml-find-phrase - reason-discover-phrase)))) + (funcall (utop-compat-lookup #'cdr))) ;; +-----------------------------------------------------------------+ ;; | Utils | From d107b33b7f9b4b27c09b9594887b31f2678ae15b Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 6 Mar 2026 13:46:54 +0200 Subject: [PATCH 3/7] Support derived modes in compat lookup Walk the derived-mode-parent chain when looking up phrase functions, so modes that derive from a known mode (e.g. a mode derived from tuareg-mode) work automatically without explicit registration. --- src/top/utop.el | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/top/utop.el b/src/top/utop.el index 55521450..c136ff80 100644 --- a/src/top/utop.el +++ b/src/top/utop.el @@ -288,8 +288,13 @@ To add support for a new OCaml major mode, add an entry to this alist: (defun utop-compat-lookup (slot) "Look up a phrase function for the current major mode. -SLOT should be `car' for next-phrase or `cdr' for discover-phrase." - (let ((entry (assq major-mode utop-mode-compat-alist))) +SLOT should be `car' for next-phrase or `cdr' for discover-phrase. +Also checks parent modes via `derived-mode-parent'." + (let ((mode major-mode) + entry) + (while (and mode (not entry)) + (setq entry (assq mode utop-mode-compat-alist)) + (setq mode (get mode 'derived-mode-parent))) (if entry (funcall slot (cdr entry)) (error "utop doesn't support the major mode \"%s\". From c8f6a57db7ad939afc81b49adb20c7e3b5fe3804 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 6 Mar 2026 15:03:53 +0200 Subject: [PATCH 4/7] Update README to document mode registration and mention neocaml Document `utop-mode-compat-alist` as the way to add support for new major modes, mention derived mode support, and list neocaml alongside the other supported modes. --- README.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index eab8143e..2013fa88 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ OCaml. It can run in a terminal or in Emacs. It supports line editing, history, real-time and context sensitive completion, colors, and more. -It integrates with the Tuareg, caml, ReasonML and typerex modes in Emacs. +It integrates with the Tuareg, caml, neocaml, ReasonML and typerex modes in Emacs. ![Screenshot](screenshot.png) @@ -240,12 +240,20 @@ have it enabled by default with the following configuration: If you plan to use utop with another major-mode than tuareg, replace `tuareg-mode-hook` by the appropriate hook. The utop minor mode will work out of -the box with these modes: `tuareg-mode`, `caml-mode`, `reason-mode` and -`typerex-mode`. For other modes you will need to set the following three -variables: +the box with these modes: `tuareg-mode`, `caml-mode`, `neocaml-mode`, +`reason-mode` and `typerex-mode`. Modes derived from any of these will +also work automatically. -- `utop-skip-blank-and-comments` -- `utop-skip-to-end-of-phrase` +For other modes you can register support via `utop-mode-compat-alist`: + +```elisp +(add-to-list 'utop-mode-compat-alist + '(my-ocaml-mode . (my-next-phrase . my-discover-phrase))) +``` + +Alternatively, you can set the following buffer-local variables: + +- `utop-next-phrase-beginning` - `utop-discover-phrase` You can also complete text in a buffer using the environment of the From 484141fc8b67370ffc5686d552deb15cb17b6b72 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 6 Mar 2026 15:38:36 +0200 Subject: [PATCH 5/7] Use plists instead of cons pairs in utop-mode-compat-alist Plist entries like `:next-phrase` and `:discover-phrase` are more readable and easier to extend with new properties in the future than positional cons pairs. --- README.md | 3 ++- src/top/utop.el | 33 ++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2013fa88..65880466 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,8 @@ For other modes you can register support via `utop-mode-compat-alist`: ```elisp (add-to-list 'utop-mode-compat-alist - '(my-ocaml-mode . (my-next-phrase . my-discover-phrase))) + '(my-ocaml-mode :next-phrase my-next-phrase + :discover-phrase my-discover-phrase)) ``` Alternatively, you can set the following buffer-local variables: diff --git a/src/top/utop.el b/src/top/utop.el index c136ff80..bf979f46 100644 --- a/src/top/utop.el +++ b/src/top/utop.el @@ -249,20 +249,25 @@ backend") ;; +-----------------------------------------------------------------+ (defvar utop-mode-compat-alist - '((tuareg-mode . (utop-tuareg-next-phrase . utop-tuareg-discover-phrase)) - (typerex-mode . (typerex-skip-to-end-of-phrase . typerex-discover-phrase)) - (caml-mode . (caml-skip-to-end-of-phrase . caml-find-phrase)) - (reason-mode . (reason-next-phrase . reason-discover-phrase))) + '((tuareg-mode :next-phrase utop-tuareg-next-phrase + :discover-phrase utop-tuareg-discover-phrase) + (typerex-mode :next-phrase typerex-skip-to-end-of-phrase + :discover-phrase typerex-discover-phrase) + (caml-mode :next-phrase caml-skip-to-end-of-phrase + :discover-phrase caml-find-phrase) + (reason-mode :next-phrase reason-next-phrase + :discover-phrase reason-discover-phrase)) "Alist mapping major modes to their phrase navigation functions. -Each entry is (MODE . (NEXT-PHRASE-FN . DISCOVER-PHRASE-FN)). +Each entry is (MODE :next-phrase FN :discover-phrase FN). -NEXT-PHRASE-FN should move point to the beginning of the next phrase. -DISCOVER-PHRASE-FN should return a triple (begin-pos end-pos end-pos-with-comments). +:next-phrase FN should move point to the beginning of the next phrase. +:discover-phrase FN should return a triple (begin-pos end-pos end-pos-with-comments). To add support for a new OCaml major mode, add an entry to this alist: (add-to-list \\='utop-mode-compat-alist - \\='(my-ocaml-mode . (my-next-phrase . my-discover-phrase)))") + \\='(my-ocaml-mode :next-phrase my-next-phrase + :discover-phrase my-discover-phrase))") (defun utop-tuareg-next-phrase () "Move to the next phrase after point." @@ -286,9 +291,9 @@ To add support for a new OCaml major mode, add an entry to this alist: (require 'tuareg) (tuareg-discover-phrase)) -(defun utop-compat-lookup (slot) +(defun utop-compat-lookup (prop) "Look up a phrase function for the current major mode. -SLOT should be `car' for next-phrase or `cdr' for discover-phrase. +PROP should be a keyword like :next-phrase or :discover-phrase. Also checks parent modes via `derived-mode-parent'." (let ((mode major-mode) entry) @@ -296,17 +301,19 @@ Also checks parent modes via `derived-mode-parent'." (setq entry (assq mode utop-mode-compat-alist)) (setq mode (get mode 'derived-mode-parent))) (if entry - (funcall slot (cdr entry)) + (or (plist-get (cdr entry) prop) + (error "utop mode compat entry for \"%s\" is missing %s" + (car entry) prop)) (error "utop doesn't support the major mode \"%s\". To add support, either customize `utop-mode-compat-alist' or set `utop-next-phrase-beginning' and `utop-discover-phrase' as buffer-local variables" major-mode)))) (defun utop-compat-next-phrase-beginning () - (funcall (utop-compat-lookup #'car))) + (funcall (utop-compat-lookup :next-phrase))) (defun utop-compat-discover-phrase () - (funcall (utop-compat-lookup #'cdr))) + (funcall (utop-compat-lookup :discover-phrase))) ;; +-----------------------------------------------------------------+ ;; | Utils | From 468e45251acc8ca5d46c68772fb0484010539f61 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 6 Mar 2026 15:46:34 +0200 Subject: [PATCH 6/7] Clarify that neocaml integration lives on the neocaml side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't list neocaml alongside the built-in modes — instead mention it as an example of a third-party mode that integrates via the new utop-mode-compat-alist mechanism. --- README.md | 13 ++++++++----- src/top/utop.el | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 65880466..fe61343b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ OCaml. It can run in a terminal or in Emacs. It supports line editing, history, real-time and context sensitive completion, colors, and more. -It integrates with the Tuareg, caml, neocaml, ReasonML and typerex modes in Emacs. +It includes built-in Emacs integration and works with Tuareg, caml, +ReasonML and typerex modes. Other modes like +[neocaml](https://github.com/bbatsov/neocaml) can also integrate +via `utop-mode-compat-alist`. ![Screenshot](screenshot.png) @@ -240,11 +243,11 @@ have it enabled by default with the following configuration: If you plan to use utop with another major-mode than tuareg, replace `tuareg-mode-hook` by the appropriate hook. The utop minor mode will work out of -the box with these modes: `tuareg-mode`, `caml-mode`, `neocaml-mode`, -`reason-mode` and `typerex-mode`. Modes derived from any of these will -also work automatically. +the box with these modes: `tuareg-mode`, `caml-mode`, `reason-mode` and +`typerex-mode`. Modes derived from any of these will also work automatically. -For other modes you can register support via `utop-mode-compat-alist`: +Other modes (e.g. `neocaml-mode`) can register support via +`utop-mode-compat-alist`: ```elisp (add-to-list 'utop-mode-compat-alist diff --git a/src/top/utop.el b/src/top/utop.el index bf979f46..f22fc4da 100644 --- a/src/top/utop.el +++ b/src/top/utop.el @@ -16,8 +16,8 @@ ;; ;; utop.el has two components - a nice OCaml REPL with auto-completion and a ;; minor mode (`utop-minor-mode'), which extends OCaml major modes -;; (e.g. `tuareg-mode', `caml-mode', `neocaml-mode') with commands to evaluate -;; forms directly in the REPL. +;; (e.g. `tuareg-mode', `caml-mode') with commands to evaluate forms directly +;; in the REPL. Other modes can integrate via `utop-mode-compat-alist'. ;; ;; See the "Integration with Emacs" section of the README for more info. From 68573dd667e00b68396f15ab82b423104c426ae4 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 6 Mar 2026 16:26:30 +0200 Subject: [PATCH 7/7] Fix docstring line exceeding 80 characters --- src/top/utop.el | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/top/utop.el b/src/top/utop.el index f22fc4da..893f2b10 100644 --- a/src/top/utop.el +++ b/src/top/utop.el @@ -261,7 +261,8 @@ backend") Each entry is (MODE :next-phrase FN :discover-phrase FN). :next-phrase FN should move point to the beginning of the next phrase. -:discover-phrase FN should return a triple (begin-pos end-pos end-pos-with-comments). +:discover-phrase FN should return a triple +\(begin-pos end-pos end-pos-with-comments). To add support for a new OCaml major mode, add an entry to this alist: