Skip to content

Commit 5f9ae29

Browse files
committed
Rewrite REPL input-completeness check on parse-partial-sexp
The hand-rolled character walker in `port-repl--input-complete-p' worked but reimplemented bracket counting, string parsing, and comment skipping that Emacs's standard `parse-partial-sexp' already handles. Switch to a `with-temp-buffer' + `parse-partial-sexp' approach keyed on a new `port-repl-input-syntax-table' defvar (Clojure defaults: all three bracket flavours, double-quoted strings with backslash escapes, semicolon line comments). Same observable behaviour on the existing test cases, plus: - explicit coverage for `[]' and `{}' inputs that the previous test set didn't exercise directly, - a customisation seam: rebind the syntax table for dialect variants that have different bracket or comment conventions. Borrowed pattern from Neat's `neat-repl-input-syntax-table'.
1 parent 5bac03a commit 5f9ae29

3 files changed

Lines changed: 67 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## 0.3.0-snapshot (unreleased)
44

5+
- REPL input-completeness check rewritten on top of
6+
`parse-partial-sexp' against a new `port-repl-input-syntax-table'
7+
defvar. Functionally identical to the previous hand-rolled
8+
character walker (same balance, string, and `;'-comment
9+
semantics), but ~20 lines shorter and override-friendly: drop in
10+
a different syntax table to use Port's REPL with dialect variants
11+
that have different bracket or comment conventions.
512
- Wire-level message log for debugging. Set `port-log-messages` to
613
`t` (or run `M-x port-toggle-message-log`) and every outgoing form
714
and incoming parsed message gets mirrored to `*port-messages*`,

lisp/port-repl.el

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -397,32 +397,41 @@ than the one we're about to erase."
397397
(t
398398
(port-repl-send-input input)))))))
399399

400+
(defvar port-repl-input-syntax-table
401+
(let ((tbl (make-syntax-table)))
402+
;; Matched bracket pairs — Clojure uses all three flavours.
403+
(modify-syntax-entry ?\( "()" tbl)
404+
(modify-syntax-entry ?\) ")(" tbl)
405+
(modify-syntax-entry ?\[ "(]" tbl)
406+
(modify-syntax-entry ?\] ")[" tbl)
407+
(modify-syntax-entry ?\{ "(}" tbl)
408+
(modify-syntax-entry ?\} "){" tbl)
409+
;; Double-quoted strings with backslash escapes.
410+
(modify-syntax-entry ?\" "\"" tbl)
411+
(modify-syntax-entry ?\\ "\\" tbl)
412+
;; Semicolon line comments terminated by newline.
413+
(modify-syntax-entry ?\; "<" tbl)
414+
(modify-syntax-entry ?\n ">" tbl)
415+
tbl)
416+
"Syntax table used to check REPL input completeness.
417+
Default models Clojure: round, square, and curly brackets all
418+
open/close balanced pairs; double-quoted strings with backslash
419+
escapes; semicolon line comments terminated by newline. Replace
420+
with a different syntax table to use Port's REPL with dialect
421+
variants that have different bracket or comment conventions.")
422+
400423
(defun port-repl--input-complete-p (s)
401-
"Heuristic: does S contain balanced parens/brackets/braces?"
402-
(let ((depth 0)
403-
(i 0)
404-
(len (length s))
405-
(in-string nil)
406-
(escape nil))
407-
(catch 'unbalanced
408-
(while (< i len)
409-
(let ((c (aref s i)))
410-
(cond
411-
(escape (setq escape nil))
412-
(in-string
413-
(cond
414-
((eq c ?\\) (setq escape t))
415-
((eq c ?\") (setq in-string nil))))
416-
((eq c ?\;) ;; line comment
417-
(while (and (< i len) (not (eq (aref s i) ?\n)))
418-
(cl-incf i))
419-
(cl-decf i))
420-
((eq c ?\") (setq in-string t))
421-
((memq c '(?\( ?\[ ?\{)) (cl-incf depth))
422-
((memq c '(?\) ?\] ?\})) (cl-decf depth)
423-
(when (< depth 0) (throw 'unbalanced nil)))))
424-
(cl-incf i))
425-
(and (= depth 0) (not in-string)))))
424+
"Return non-nil when S forms a complete top-level expression.
425+
Uses `parse-partial-sexp' against `port-repl-input-syntax-table',
426+
so bracket balance, string state, and line comments are handled
427+
by Emacs's standard syntactic parser rather than a hand-rolled
428+
character walker."
429+
(with-temp-buffer
430+
(set-syntax-table port-repl-input-syntax-table)
431+
(insert s)
432+
(let ((state (parse-partial-sexp (point-min) (point-max))))
433+
(and (zerop (nth 0 state)) ; bracket depth balanced
434+
(null (nth 3 state)))))) ; not inside a string
426435

427436
(defun port-repl-send-input (input)
428437
"Submit INPUT to the prepl and append it to history."

test/port-client-tests.el

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,40 @@
125125

126126
(it "accepts balanced forms"
127127
(expect (port-repl--input-complete-p "(+ 1 2)") :to-be-truthy)
128-
(expect (port-repl--input-complete-p "42") :to-be-truthy))
128+
(expect (port-repl--input-complete-p "42") :to-be-truthy)
129+
(expect (port-repl--input-complete-p "") :to-be-truthy))
129130

130-
(it "rejects unbalanced parens or strings"
131+
(it "handles all three Clojure bracket flavours"
132+
(expect (port-repl--input-complete-p "[1 2 3]") :to-be-truthy)
133+
(expect (port-repl--input-complete-p "{:a 1 :b 2}") :to-be-truthy)
134+
(expect (port-repl--input-complete-p "(defn f [x] {:x x})") :to-be-truthy))
135+
136+
(it "rejects unbalanced brackets or strings"
131137
(expect (port-repl--input-complete-p "(+ 1") :to-be nil)
138+
(expect (port-repl--input-complete-p "[1 2") :to-be nil)
139+
(expect (port-repl--input-complete-p "{:a 1") :to-be nil)
132140
(expect (port-repl--input-complete-p "\"unterminated") :to-be nil))
133141

134142
(it "ignores closers inside strings and line comments"
135143
(expect (port-repl--input-complete-p "\"a string with ) inside\"")
136144
:to-be-truthy)
137145
(expect (port-repl--input-complete-p "(do ;; comment with )\n 1)")
138-
:to-be-truthy)))
146+
:to-be-truthy))
147+
148+
(it "honours a custom `port-repl-input-syntax-table'"
149+
;; Override with an Emacs-Lisp-style table where `[' is punctuation:
150+
;; `[1 2 3]' no longer registers as a balanced bracket pair, so we
151+
;; should see it as incomplete (unclosed `]' becomes a syntax error
152+
;; on the closing side, but the opener is no longer a paren either,
153+
;; so depth stays at 0 — accept this as `complete' in elisp's view).
154+
(let ((port-repl-input-syntax-table
155+
(let ((tbl (make-syntax-table)))
156+
(modify-syntax-entry ?\( "()" tbl)
157+
(modify-syntax-entry ?\) ")(" tbl)
158+
tbl)))
159+
(expect (port-repl--input-complete-p "(+ 1 2)") :to-be-truthy)
160+
;; Unbalanced paren still detected.
161+
(expect (port-repl--input-complete-p "(+ 1") :to-be nil))))
139162

140163
(describe "port-client message log"
141164

0 commit comments

Comments
 (0)