-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathneocaml-utop.el
More file actions
839 lines (740 loc) · 35 KB
/
Copy pathneocaml-utop.el
File metadata and controls
839 lines (740 loc) · 35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
;;; neocaml-utop.el --- utop toplevel integration via its Emacs protocol -*- lexical-binding: t; -*-
;; Copyright © 2025-2026 Bozhidar Batsov
;;
;; Author: Bozhidar Batsov <bozhidar@batsov.dev>
;; Maintainer: Bozhidar Batsov <bozhidar@batsov.dev>
;; URL: http://github.com/bbatsov/neocaml
;; Keywords: languages ocaml
;; This file is not part of GNU Emacs.
;;; Commentary:
;; An alternative OCaml toplevel integration that drives `utop' through
;; its native editor protocol (`utop -emacs') instead of treating it as a
;; plain comint stream like `neocaml-repl' does.
;;
;; In `-emacs' mode utop does no rendering of its own: it speaks a small
;; line-based protocol of `command:argument' messages over stdin/stdout.
;; This lets neocaml present a proper transcript while getting things a
;; raw stream cannot provide:
;;
;; - stdout, stderr, results, errors and warnings arrive already
;; separated, so no regexp sniffing is needed to tell them apart;
;; - parse and type errors come with character ranges into the submitted
;; phrase, which we map back onto the *source* buffer and underline
;; precisely (see `neocaml-utop-error-face');
;; - completion is answered by utop's own engine, wired here into
;; `completion-at-point'.
;;
;; The transcript buffer is a `comint' derivative, so input editing,
;; history (with optional persistence) and read-only prompts come for
;; free; only the transport is custom (a preoutput filter that parses the
;; protocol and an input sender that emits it).
;;
;; This module is independent of `neocaml-repl': use whichever you
;; prefer. `neocaml-repl' remains the right choice for the plain `ocaml'
;; toplevel, which has no protocol of its own.
;;; License:
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License
;; as published by the Free Software Foundation; either version 3
;; of the License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.
;;; Code:
(require 'comint)
(require 'pulse)
(require 'subr-x)
(require 'neocaml)
(require 'neocaml-common)
;; Reused for the transport-agnostic source-side helpers (project naming
;; and `;;'-delimited phrase bounds); `neocaml-repl' itself is untouched.
(require 'neocaml-repl)
(defgroup neocaml-utop nil
"OCaml toplevel integration via utop's Emacs protocol."
:prefix "neocaml-utop-"
:group 'neocaml)
(defcustom neocaml-utop-program "utop"
"Program name used to invoke utop in protocol mode.
The `-emacs' flag is always appended by neocaml."
:type 'string
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-program-args nil
"Extra command-line arguments passed to utop before `-emacs'.
For the `dune-utop' flavor these are passed to the utop that `dune utop'
launches (i.e. after the `--' separator)."
:type '(repeat string)
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-flavor 'utop
"Which utop toplevel `neocaml-utop' launches.
- `utop': run `neocaml-utop-program' directly.
- `dune-utop': run `dune utop' for the current project, so the project's
libraries are available in the toplevel.
Set this globally or per project via a `.dir-locals.el' file."
:type '(choice (const :tag "utop" utop)
(const :tag "dune utop" dune-utop))
:safe (lambda (v) (memq v '(utop dune-utop)))
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-buffer-name "*utop*"
"Base name of the utop transcript buffer.
Per-project sessions derive their name from this (e.g. \"*utop: proj*\")."
:type 'string
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-history-file
(expand-file-name "neocaml-utop-history" user-emacs-directory)
"File to persist utop input history across sessions.
Set to nil to disable history persistence."
:type '(choice (file :tag "History file")
(const :tag "Disable" nil))
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-history-size 1000
"Maximum number of input history entries to persist."
:type 'integer
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-completion-at-point t
"When non-nil, offer utop-backed completion in the transcript buffer.
Completion candidates come from utop's own completion engine via the
protocol's `complete-company' request."
:type 'boolean
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-echo-eval-result t
"When non-nil, echo the result of a source evaluation in the minibuffer.
This mirrors a SLIME/CIDER-style flow: sending a phrase or definition
from a source buffer shows its value (or error) in the echo area, while
the full output still goes to the transcript buffer."
:type 'boolean
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-inline-eval-result t
"When non-nil, show a source evaluation's value inline in the source buffer.
The value is shown as a CIDER-style `=> ...' overlay at the end of the
evaluated region; it is removed on the next evaluation or when the
underlying text is edited."
:type 'boolean
:package-version '(neocaml . "0.10.0"))
(defcustom neocaml-utop-fontify-input t
"When non-nil, fontify transcript input using tree-sitter via `neocaml-mode'.
This uses `comint-fontify-input-mode' (Emacs 29.1+) to give the OCaml
code you type in the transcript full syntax highlighting, while the
toplevel's output keeps its own highlighting.
Set to nil to use only the basic transcript font-lock keywords for input."
:type 'boolean
:package-version '(neocaml . "0.10.0"))
(defface neocaml-utop-error-face
'((((supports :underline (:style wave)))
:underline (:style wave :color "Red1"))
(t :inherit error))
"Face used to underline utop error spans in the source buffer."
:group 'neocaml-utop)
(defface neocaml-utop-result-face
'((t :inherit shadow))
"Face for the inline `=>' result overlay in the source buffer."
:group 'neocaml-utop)
(defconst neocaml-utop--prompt-regexp "^utop\\[[0-9]+\\]> "
"Regexp matching the prompt neocaml-utop renders for utop.")
(defvar neocaml-utop-font-lock-keywords
'(("\\(val\\) \\([^:]*\\)\\( *:\\)"
(1 font-lock-keyword-face) (2 font-lock-variable-name-face))
("\\(type\\) \\([^ =]*\\)"
(1 font-lock-keyword-face) (2 font-lock-type-face)))
"Font-lock keywords for utop result lines in the transcript buffer.")
;;;; Buffer-local state
;;
;; All of these live in the transcript buffer; the preoutput filter runs
;; there, so it reads and writes them directly.
(defvar-local neocaml-utop--partial ""
"Incomplete trailing protocol line buffered between filter calls.")
(defvar-local neocaml-utop--count -1
"Number of prompts shown so far, used to number the prompt.")
(defvar-local neocaml-utop--error-target nil
"Where to map error offsets for the phrase currently being evaluated.
A list (BUFFER START END): error character offsets reported by utop are
relative to buffer position START in BUFFER, and overlays are clamped to
END (the end of the submitted phrase) so the `;;' we append can't push a
span past the source. Nil when the last input was not source-derived
\(e.g. `#require'/`#use'), so no overlay is drawn.")
(defvar-local neocaml-utop--overlays nil
"Error overlays currently shown for the last evaluated phrase.")
(defvar-local neocaml-utop--error-pending nil
"Error overlays awaiting their message text from the following stderr.")
(defvar-local neocaml-utop--error-message ""
"Accumulated error text for the overlays in `neocaml-utop--error-pending'.")
(defvar-local neocaml-utop--result-overlays nil
"Inline result overlays (the CIDER-style `=>') shown in source buffers.")
(defvar-local neocaml-utop--echo nil
"When non-nil, capture this evaluation's output for minibuffer echo.
Set when a source evaluation is sent; cleared when its terminating
`prompt' arrives and the result has been echoed.")
(defvar-local neocaml-utop--echo-lines nil
"Accumulated (KIND . TEXT) output lines for the current echo capture.
KIND is `out' (stdout) or `err' (stderr); the list is reversed.")
(defvar-local neocaml-utop--echo-timer nil
"Debounce timer that flushes the echo capture once output settles.")
(defvar-local neocaml-utop--completions nil
"Completion candidates accumulated from the current `complete' request.")
(defvar-local neocaml-utop--completion-state nil
"State of an in-flight completion request: `collecting', `done', or nil.")
(defvar-local neocaml-utop--source-buffer nil
"Source buffer that last switched to this transcript.")
(defvar-local neocaml-utop--command-line nil
"The (PROGRAM ARG...) this transcript was launched with.
Recorded so `neocaml-utop-restart' relaunches the same toplevel even
though it runs from the transcript buffer, which lacks the source's
directory-local `neocaml-utop-flavor'.")
;;;; Session naming and command
(defun neocaml-utop--buffer-name ()
"Return the transcript buffer name for the current context.
In a transcript buffer that is the buffer itself; in a source buffer it
is the per-project session derived from `neocaml-utop-buffer-name'."
(cond
((derived-mode-p 'neocaml-utop-mode) (buffer-name))
((neocaml-repl--project-id)
(let ((base (if (string-suffix-p "*" neocaml-utop-buffer-name)
(substring neocaml-utop-buffer-name 0 -1)
neocaml-utop-buffer-name)))
(format "%s: %s*" base (neocaml-repl--project-id))))
(t neocaml-utop-buffer-name)))
(defun neocaml-utop--command ()
"Return (PROGRAM ARG...) for launching utop per `neocaml-utop-flavor'."
(pcase neocaml-utop-flavor
('dune-utop
(let ((root (or (neocaml-common-dune-project-root) default-directory)))
(append (list "dune" "utop" root "--")
neocaml-utop-program-args (list "-emacs"))))
(_ (append (list neocaml-utop-program)
neocaml-utop-program-args (list "-emacs")))))
;;;; Error overlays
(defun neocaml-utop--overlay-evaporate (ov after &rest _)
"Delete OV when the text under it is edited.
Used as an overlay modification hook; AFTER is non-nil on the call made
after the change, which is when we remove the overlay."
(when after (delete-overlay ov)))
(defun neocaml-utop--clear-overlays ()
"Remove the error overlays tracked in this transcript buffer."
(mapc #'delete-overlay neocaml-utop--overlays)
(setq neocaml-utop--overlays nil
neocaml-utop--error-pending nil
neocaml-utop--error-message ""))
(defun neocaml-utop--handle-accept (arg)
"Handle an `accept:ARG' message: ARG is empty on success.
Otherwise ARG is a comma-separated list of (a,b) character-offset pairs
into the submitted phrase, which we map onto the source via
`neocaml-utop--error-target' and underline. Offsets are clamped to the
phrase bounds so the appended `;;' (or an end-of-input error) cannot
push a span past the source buffer. The overlays clear themselves when
the underlying text is edited, and get the error message as a tooltip
once the following stderr arrives."
(neocaml-utop--clear-overlays)
(when (and (not (string-empty-p arg)) neocaml-utop--error-target)
(let* ((buf (nth 0 neocaml-utop--error-target))
(start (nth 1 neocaml-utop--error-target))
(end (nth 2 neocaml-utop--error-target))
(nums (mapcar #'string-to-number (split-string arg "," t)))
(overlays nil))
(when (buffer-live-p buf)
(with-current-buffer buf
(let ((limit (min end (point-max))))
(while (>= (length nums) 2)
(let* ((a (pop nums))
(b (pop nums))
;; utop reports character offsets, so map by simple
;; addition; clamp to the phrase to stay in bounds.
(beg (max start (min (+ start a) limit)))
(ovend (max start (min (+ start b) limit))))
(when (< beg ovend)
(let ((ov (make-overlay beg ovend)))
(overlay-put ov 'face 'neocaml-utop-error-face)
(overlay-put ov 'help-echo "neocaml-utop: error here")
(overlay-put ov 'modification-hooks
(list #'neocaml-utop--overlay-evaporate))
(push ov overlays))))))))
;; Store back in the transcript buffer (we're running in its filter);
;; the message text follows on stderr (see `neocaml-utop--handle-line').
(setq neocaml-utop--overlays overlays
neocaml-utop--error-pending overlays
neocaml-utop--error-message ""))))
(defun neocaml-utop--note-error-text (text)
"Append TEXT to the pending error message and refresh overlay tooltips."
(when neocaml-utop--error-pending
(setq neocaml-utop--error-message
(if (string-empty-p neocaml-utop--error-message)
text
(concat neocaml-utop--error-message "\n" text)))
(dolist (ov neocaml-utop--error-pending)
(when (overlay-buffer ov)
(overlay-put ov 'help-echo neocaml-utop--error-message)))))
;;;; Result echo and inline display
;;
;; utop's evaluated value is written to its redirected stdout, which
;; reaches us on a different channel than the command messages and can
;; land just *after* the terminating `prompt' (errors, sent inline, land
;; before it). So rather than bound the capture on the prompt, we
;; debounce: each output line refreshes a short timer, and the result is
;; shown once output settles.
(defconst neocaml-utop--echo-settle-delay 0.2
"Seconds of output quiet before a captured result is shown.")
(defun neocaml-utop--echo-result (lines)
"Echo captured LINES (each (KIND . TEXT)) in the minibuffer.
Error and warning text is highlighted; empty lines are dropped."
(let* ((kept (seq-remove (lambda (l) (string-empty-p (cdr l))) lines))
(text (mapconcat
(lambda (l)
(if (eq (car l) 'err)
(propertize (cdr l) 'face 'font-lock-warning-face)
(cdr l)))
kept "\n")))
(unless (string-empty-p text)
(message "%s" text))))
(defun neocaml-utop--clear-result-overlays ()
"Remove the inline result overlays tracked in this transcript buffer."
(mapc #'delete-overlay neocaml-utop--result-overlays)
(setq neocaml-utop--result-overlays nil))
(defun neocaml-utop--inline-result (lines target transcript)
"Show the value from LINES inline at the end of TARGET's region.
TARGET is the (BUFFER START END) of the evaluated source; nothing is
shown for interactive input (TARGET in TRANSCRIPT), for errors, or when
there is no value. The overlay is tracked in TRANSCRIPT and clears
itself when the underlying text is edited."
(let ((value (mapconcat #'cdr
(seq-filter (lambda (l) (and (eq (car l) 'out)
(not (string-empty-p (cdr l)))))
lines)
" ")))
(when (and (consp target) (not (string-empty-p value)))
(let ((buf (nth 0 target))
(end (nth 2 target)))
(when (and (buffer-live-p buf) (not (eq buf transcript)))
(let ((ov (with-current-buffer buf
(save-excursion
(goto-char (min end (point-max)))
(let ((o (make-overlay (line-end-position) (line-end-position))))
(overlay-put o 'after-string
(propertize (concat " => " value)
'face 'neocaml-utop-result-face))
(overlay-put o 'modification-hooks
(list #'neocaml-utop--overlay-evaporate))
o)))))
(with-current-buffer transcript
(push ov neocaml-utop--result-overlays))))))))
(defun neocaml-utop--echo-flush (buffer)
"Show and clear BUFFER's pending result capture (echo and/or inline)."
(when (buffer-live-p buffer)
(with-current-buffer buffer
(let ((lines (nreverse neocaml-utop--echo-lines))
(target neocaml-utop--error-target))
(setq neocaml-utop--echo nil
neocaml-utop--echo-lines nil
neocaml-utop--echo-timer nil)
(when neocaml-utop-echo-eval-result
(neocaml-utop--echo-result lines))
(when neocaml-utop-inline-eval-result
(neocaml-utop--inline-result lines target buffer))))))
(defun neocaml-utop--echo-accumulate (kind text)
"Record a TEXT line of KIND for the result capture and re-arm the timer."
(when neocaml-utop--echo
(push (cons kind text) neocaml-utop--echo-lines)
(when (timerp neocaml-utop--echo-timer)
(cancel-timer neocaml-utop--echo-timer))
(setq neocaml-utop--echo-timer
(run-at-time neocaml-utop--echo-settle-delay nil
#'neocaml-utop--echo-flush (current-buffer)))))
;;;; Protocol parsing
(defun neocaml-utop--prompt-string ()
"Return the prompt string for the next phrase, advancing the counter."
(setq neocaml-utop--count (1+ neocaml-utop--count))
(format "utop[%d]> " neocaml-utop--count))
(defun neocaml-utop--handle-line (line)
"Interpret one protocol LINE and return the text to insert (maybe \"\")."
(let* ((idx (string-search ":" line))
(cmd (if idx (substring line 0 idx) line))
(arg (if idx (substring line (1+ idx)) "")))
(pcase cmd
("stdout"
(neocaml-utop--echo-accumulate 'out arg)
(concat arg "\n"))
("stderr"
(neocaml-utop--echo-accumulate 'err arg)
(neocaml-utop--note-error-text arg)
(concat (propertize arg 'face 'font-lock-warning-face) "\n"))
("prompt"
(setq neocaml-utop--error-pending nil)
(neocaml-utop--prompt-string))
("accept" (neocaml-utop--handle-accept arg) "")
("completion-start"
(setq neocaml-utop--completions nil
neocaml-utop--completion-state 'collecting)
"")
("completion" (push arg neocaml-utop--completions) "")
("completion-stop" (setq neocaml-utop--completion-state 'done) "")
("no-such-package"
(concat (propertize (format "No such package: %s" arg)
'face 'font-lock-warning-face)
"\n"))
;; Control/metadata messages with nothing to display.
((or "protocol-version" "phrase-terminator" "continue"
"history-data" "history-end" "history-bound")
"")
(_ ""))))
(defun neocaml-utop--preoutput (string)
"Parse protocol STRING from utop and return the human-visible text.
Installed in `comint-preoutput-filter-functions'."
(setq neocaml-utop--partial (concat neocaml-utop--partial string))
(let ((lines (split-string neocaml-utop--partial "\n"))
(out ""))
;; The last element is an incomplete line (or "" if STRING ended in \n).
(setq neocaml-utop--partial (car (last lines)))
(dolist (line (butlast lines))
(setq out (concat out (neocaml-utop--handle-line line))))
out))
;;;; Sending phrases
(defun neocaml-utop--send-data (proc text)
"Send TEXT to PROC as a protocol data block (`data:' lines then `end:')."
(dolist (line (split-string text "\n"))
(process-send-string proc (concat "data:" line "\n")))
(process-send-string proc "end:\n"))
(defun neocaml-utop--send-eval (proc text)
"Send TEXT to PROC for evaluation, appending `;;' when missing.
Uses the protocol's `input-multi' so a region or buffer containing
several `;;'-separated phrases evaluates all of them, not just the
first. The terminator is required: without it utop reports a syntax
error at end of input rather than waiting for more."
(let ((payload (if (string-suffix-p ";;" (string-trim-right text))
text
(concat text ";;"))))
(process-send-string proc "input-multi:\n")
(neocaml-utop--send-data proc payload)))
(defun neocaml-utop--input-sender (proc input)
"Comint input sender: send INPUT typed in the transcript to PROC.
Records the input's buffer span so error offsets land on it."
(with-current-buffer (process-buffer proc)
(let ((start (marker-position comint-last-input-start)))
(setq neocaml-utop--error-target
(list (current-buffer) start (+ start (length input))))))
(neocaml-utop--send-eval proc input))
(defun neocaml-utop--phrase-complete-p (input)
"Return non-nil when INPUT is ready to submit to utop.
True for empty input, a phrase terminated by `;;', or a toplevel
directive (beginning with `#')."
(let ((trimmed (string-trim input)))
(or (string-empty-p trimmed)
(string-suffix-p ";;" (string-trim-right input))
(string-prefix-p "#" trimmed))))
(defun neocaml-utop-return ()
"Submit the current input, or insert a newline if the phrase is incomplete.
Bound to \\`RET' in the transcript: input is treated as complete (and
submitted) when it ends with `;;' or is a toplevel directive (begins
with `#'), letting you type multi-line phrases naturally. Pressing
\\`RET' on a blank trailing line submits regardless."
(interactive)
(let* ((proc (get-buffer-process (current-buffer)))
(input (and proc (buffer-substring-no-properties
(process-mark proc) (point-max)))))
(if (or (null proc)
(neocaml-utop--phrase-complete-p input)
(save-excursion (beginning-of-line) (looking-at-p "[ \t]*$")))
(comint-send-input)
(newline))))
;;;; Completion
(defun neocaml-utop--complete (input)
"Ask utop to complete INPUT and return the candidate list.
Blocks (briefly) waiting for the protocol round-trip."
(let ((proc (get-buffer-process (current-buffer))))
(when (process-live-p proc)
(setq neocaml-utop--completions nil
neocaml-utop--completion-state 'collecting)
(process-send-string proc "complete-company:\n")
(neocaml-utop--send-data proc input)
(with-timeout (2 nil)
(while (eq neocaml-utop--completion-state 'collecting)
(accept-process-output proc 0.05)))
(nreverse neocaml-utop--completions))))
(defun neocaml-utop-completion-at-point ()
"Completion-at-point function backed by utop's completion engine.
Active only in the transcript buffer, on the current input line."
(when (and neocaml-utop-completion-at-point
(derived-mode-p 'neocaml-utop-mode))
(let ((proc (get-buffer-process (current-buffer))))
(when (and (process-live-p proc)
(>= (point) (comint-line-beginning-position)))
(let ((input (buffer-substring-no-properties
(comint-line-beginning-position) (point))))
(when (> (length (string-trim input)) 0)
(let ((start (save-excursion
(skip-chars-backward "A-Za-z0-9_'")
(point)))
(candidates (neocaml-utop--complete input)))
(when candidates
(neocaml-common-capf start (point) candidates "utop" 'text)))))))))
;;;; Mode
(defvar neocaml-utop-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map comint-mode-map)
(define-key map (kbd "RET") #'neocaml-utop-return)
(define-key map (kbd "C-c C-z") #'neocaml-utop-switch-to-source)
(easy-menu-define neocaml-utop-mode-menu map "utop Menu"
'("utop"
["Switch to Source" neocaml-utop-switch-to-source
:help "Switch back to the source buffer that last invoked utop"]
"--"
["Interrupt" neocaml-utop-interrupt
:enable (comint-check-proc (current-buffer))
:help "Interrupt the utop process"]
["Restart" neocaml-utop-restart
:help "Kill and restart the utop toplevel"]
["Clear Buffer" neocaml-utop-clear-buffer
:help "Erase the transcript buffer contents"]
"--"
["Customize utop..." (customize-group 'neocaml-utop)
:help "Customize the neocaml-utop settings"]))
map)
"Keymap for `neocaml-utop-mode'.")
(define-derived-mode neocaml-utop-mode comint-mode "utop"
"Major mode for the utop toplevel driven via its Emacs protocol.
\\{neocaml-utop-mode-map}"
(setq-local comint-prompt-regexp neocaml-utop--prompt-regexp)
(setq-local comint-prompt-read-only t)
(setq-local comint-input-sender #'neocaml-utop--input-sender)
(setq-local comint-process-echoes nil)
(setq-local neocaml-utop--partial "")
(setq-local neocaml-utop--count -1)
(setq-local neocaml-utop--overlays nil)
(setq-local neocaml-utop--result-overlays nil)
(setq-local comment-start "(* ")
(setq-local comment-end " *)")
(setq-local comment-start-skip "(\\*+[ \t]*")
(setq-local font-lock-defaults '(neocaml-utop-font-lock-keywords t))
(add-hook 'comint-preoutput-filter-functions #'neocaml-utop--preoutput nil t)
(add-hook 'completion-at-point-functions
#'neocaml-utop-completion-at-point nil t)
(setq-local prettify-symbols-alist (neocaml--prettify-symbols-alist))
;; Error and inline-result overlays live in source buffers but are
;; tracked here; drop them when this transcript is killed so they don't
;; linger on the source.
(add-hook 'kill-buffer-hook #'neocaml-utop--clear-overlays nil t)
(add-hook 'kill-buffer-hook #'neocaml-utop--clear-result-overlays nil t)
(when neocaml-utop-history-file
(setq-local comint-input-ring-file-name neocaml-utop-history-file)
(setq-local comint-input-ring-size neocaml-utop-history-size)
(setq-local comint-input-ignoredups t)
(comint-read-input-ring t)
(add-hook 'kill-buffer-hook #'comint-write-input-ring nil t))
;; Tree-sitter fontification for transcript input, via an indirect
;; buffer in `neocaml-mode' (mirrors `neocaml-repl').
(when neocaml-utop-fontify-input
(setq-local comint-indirect-setup-function #'neocaml-mode)
(comint-fontify-input-mode)))
;;;; Lifecycle
(defun neocaml-utop--get-or-create (&optional command)
"Return the transcript buffer for the current context, starting it if needed.
COMMAND, when given, is the (PROGRAM ARG...) to launch (used by
`neocaml-utop-restart'). Otherwise it is computed here, while the
current buffer is still the source one, so that directory-local settings
like `neocaml-utop-flavor' are honored."
(let ((bufname (neocaml-utop--buffer-name))
(command (or command (neocaml-utop--command))))
(unless (comint-check-proc bufname)
(with-current-buffer (get-buffer-create bufname)
(unless (derived-mode-p 'neocaml-utop-mode)
(neocaml-utop-mode))
(setq neocaml-utop--partial "")
(setq neocaml-utop--count -1)
(setq neocaml-utop--command-line command)
(apply #'make-comint-in-buffer "neocaml-utop" (current-buffer)
(car command) nil (cdr command))))
(get-buffer bufname)))
(defun neocaml-utop--ensure-process ()
"Return a live utop process for the current context, starting one if needed."
(get-buffer-process (neocaml-utop--get-or-create)))
(defun neocaml-utop--process ()
"Return the running utop process for the current context, or nil."
(get-buffer-process (neocaml-utop--buffer-name)))
(defun neocaml-utop--set-error-target (target)
"Record TARGET in the transcript buffer for the current context.
TARGET is a (BUFFER START END) list, or nil when the next input is not
source-derived (so no error overlay should be drawn)."
(when-let* ((buf (get-buffer (neocaml-utop--buffer-name))))
(with-current-buffer buf
(setq neocaml-utop--error-target target))))
(defun neocaml-utop--arm-echo ()
"Begin capturing the next evaluation's output for result display.
Capture runs when either `neocaml-utop-echo-eval-result' or
`neocaml-utop-inline-eval-result' is enabled; the flush decides which
display(s) to use. Any inline overlay from a previous evaluation is
cleared."
(when-let* ((buf (get-buffer (neocaml-utop--buffer-name))))
(with-current-buffer buf
(when (timerp neocaml-utop--echo-timer)
(cancel-timer neocaml-utop--echo-timer))
(neocaml-utop--clear-result-overlays)
(setq neocaml-utop--echo (or neocaml-utop-echo-eval-result
neocaml-utop-inline-eval-result)
neocaml-utop--echo-lines nil
neocaml-utop--echo-timer nil))))
;;;###autoload
(defun neocaml-utop-run ()
"Start a utop session for the current buffer's project, or switch to it."
(interactive)
(pop-to-buffer (neocaml-utop--get-or-create)))
;;;###autoload
(defun neocaml-utop-switch-to-utop ()
"Switch to the utop transcript, saving the current buffer as the source.
Use \\[neocaml-utop-switch-to-source] in the transcript to return."
(interactive)
(let ((source (current-buffer))
(buffer (neocaml-utop--get-or-create)))
(with-current-buffer buffer
(setq neocaml-utop--source-buffer source))
(pop-to-buffer buffer)))
(defun neocaml-utop-switch-to-source ()
"Switch from the transcript back to the source buffer that invoked it."
(interactive)
(if (and neocaml-utop--source-buffer
(buffer-live-p neocaml-utop--source-buffer))
(pop-to-buffer neocaml-utop--source-buffer)
(message "No source buffer to return to")))
(defun neocaml-utop-interrupt ()
"Interrupt the utop process for the current context."
(interactive)
(when-let* ((proc (neocaml-utop--process)))
(interrupt-process proc)))
;;;###autoload
(defun neocaml-utop-restart ()
"Kill and restart the utop toplevel for the current buffer's project.
The same toplevel command is reused, so a `dune-utop' session comes back
as `dune-utop' even though restart runs from the transcript buffer."
(interactive)
(let* ((bufname (neocaml-utop--buffer-name))
(buf (get-buffer bufname))
(command (and buf (buffer-local-value 'neocaml-utop--command-line buf))))
(when (comint-check-proc bufname)
(let ((proc (get-buffer-process bufname)))
(set-process-query-on-exit-flag proc nil)
(delete-process proc)))
(pop-to-buffer (neocaml-utop--get-or-create command))))
(defun neocaml-utop-clear-buffer ()
"Erase the utop transcript buffer for the current context.
Works whether or not the toplevel is still running."
(interactive)
(when-let* ((buf (get-buffer (neocaml-utop--buffer-name))))
(with-current-buffer buf
(if (comint-check-proc buf)
(comint-clear-buffer)
(let ((inhibit-read-only t))
(erase-buffer))))))
(defun neocaml-utop-quit ()
"Ask the utop session to exit."
(interactive)
(when-let* ((proc (neocaml-utop--process)))
(process-send-string proc "exit:0\n")))
;;;; Sending source to utop
;;;###autoload
(defun neocaml-utop-send-region (start end)
"Send the region between START and END to utop for evaluation."
(interactive "r")
(let ((proc (neocaml-utop--ensure-process))
(source (current-buffer)))
(neocaml-utop--set-error-target (list source start end))
(neocaml-utop--arm-echo)
(neocaml-utop--send-eval proc (buffer-substring-no-properties start end))
(pulse-momentary-highlight-region start end)))
;;;###autoload
(defun neocaml-utop-send-buffer ()
"Send the entire buffer to utop."
(interactive)
(neocaml-utop-send-region (point-min) (point-max)))
;;;###autoload
(defun neocaml-utop-send-definition ()
"Send the definition at point to utop."
(interactive)
(if-let* ((node (treesit-defun-at-point))
(start (treesit-node-start node))
(end (treesit-node-end node)))
(neocaml-utop-send-region start end)
(user-error "No definition at point")))
;;;###autoload
(defun neocaml-utop-send-phrase ()
"Send the phrase at point (code between `;;') to utop."
(interactive)
(let ((bounds (neocaml-repl--phrase-bounds)))
(neocaml-utop-send-region (car bounds) (cdr bounds))))
;;;###autoload
(defun neocaml-utop-send-phrase-and-step ()
"Send the phrase at point to utop, then move to the next phrase."
(interactive)
(let ((bounds (neocaml-repl--phrase-bounds)))
(neocaml-utop-send-region (car bounds) (cdr bounds))
(goto-char (cdr bounds))
(skip-chars-forward " \t\n")))
;;;###autoload
(defun neocaml-utop-load-file (file)
"Load FILE into utop via the `#use' directive."
(interactive (list (buffer-file-name)))
(unless file
(user-error "Buffer is not visiting a file"))
(let ((proc (neocaml-utop--ensure-process)))
;; Not a source region: clear any stale target so a #use error can't
;; underline unrelated code in a previously evaluated buffer.
(neocaml-utop--set-error-target nil)
(neocaml-utop--send-eval proc (format "#use %S" file))))
;;;###autoload
(defun neocaml-utop-require (package)
"Load the findlib PACKAGE into utop via the `#require' directive."
(interactive (list (read-string "Require package: ")))
(let ((proc (neocaml-utop--ensure-process)))
(neocaml-utop--set-error-target nil)
(neocaml-utop--send-eval proc (format "#require %S" package))))
;;;; Source-buffer minor mode
(defvar neocaml-utop-minor-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-z") #'neocaml-utop-switch-to-utop)
(define-key map (kbd "C-c C-c") #'neocaml-utop-send-definition)
(define-key map (kbd "C-c C-r") #'neocaml-utop-send-region)
(define-key map (kbd "C-c C-b") #'neocaml-utop-send-buffer)
(define-key map (kbd "C-c C-p") #'neocaml-utop-send-phrase)
(define-key map (kbd "C-c C-n") #'neocaml-utop-send-phrase-and-step)
(define-key map (kbd "C-c C-l") #'neocaml-utop-load-file)
(define-key map (kbd "C-c C-i") #'neocaml-utop-interrupt)
(define-key map (kbd "C-c C-k") #'neocaml-utop-clear-buffer)
(easy-menu-define neocaml-utop-minor-mode-menu map "utop Menu"
'("utop"
["Start/Switch to utop" neocaml-utop-switch-to-utop
:help "Start utop or switch to a running session"]
"--"
["Send Definition" neocaml-utop-send-definition
:help "Send the definition at point to utop"]
["Send Region" neocaml-utop-send-region
:enable (use-region-p)
:help "Send the active region to utop"]
["Send Buffer" neocaml-utop-send-buffer
:help "Send the whole buffer to utop"]
["Send Phrase" neocaml-utop-send-phrase
:help "Send the phrase at point to utop"]
["Send Phrase and Step" neocaml-utop-send-phrase-and-step
:help "Send the phrase at point, then move to the next phrase"]
["Load File" neocaml-utop-load-file
:help "Load a file into utop with #use"]
["Require Package..." neocaml-utop-require
:help "Load a findlib package into utop with #require"]
"--"
["Interrupt utop" neocaml-utop-interrupt
:enable (comint-check-proc (neocaml-utop--buffer-name))
:help "Interrupt the utop process"]
["Clear utop Buffer" neocaml-utop-clear-buffer
:help "Erase the transcript buffer contents"]))
map)
"Keymap for `neocaml-utop-minor-mode'.")
;;;###autoload
(define-minor-mode neocaml-utop-minor-mode
"Minor mode for interacting with utop via its Emacs protocol.
\\{neocaml-utop-minor-mode-map}"
:init-value nil
:lighter " utop"
:keymap neocaml-utop-minor-mode-map
:group 'neocaml-utop)
(provide 'neocaml-utop)
;;; neocaml-utop.el ends here