Skip to content

Commit 7888359

Browse files
committed
[Fix #28] Fix delete-pair and list navigation for delimiters
Add a `list' thing to `treesit-thing-settings' covering all OCaml container/delimited node types. On Emacs 31+ this enables native support for `forward-list', `up-list', `down-list', and correct `forward-sexp' behavior on delimiters via `treesit-forward-sexp-list'. For Emacs 29-30, introduce `neocaml--forward-sexp-hybrid' which falls back to syntax-table-based `scan-sexps' when point is on a delimiter character, so that commands like `delete-pair' find the correct matching paren instead of jumping to an unrelated tree-sitter node.
1 parent 356bfce commit 7888359

3 files changed

Lines changed: 159 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [#27](https://github.com/bbatsov/neocaml/issues/27): `neocaml-install-grammars` now accepts a prefix argument (`C-u`) to force reinstallation of grammars, even if they are already installed.
88
- Avoid the superfluous spaces after the prompt of the REPL when sending code to
99
the REPL via the commands `neocaml-repl-send-*`.
10+
- [#28](https://github.com/bbatsov/neocaml/issues/28): Fix `delete-pair` deleting the wrong closing delimiter. Add a `list` thing to `treesit-thing-settings` and a hybrid `forward-sexp` that falls back to syntax-table matching on delimiter characters.
1011

1112
### New features
1213

neocaml.el

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -742,13 +742,87 @@ to be used as `forward-sexp-function'."
742742
(treesit-beginning-of-thing neocaml--block-regex (- count))
743743
(treesit-end-of-thing neocaml--block-regex count)))
744744

745+
(defun neocaml--delimiter-p ()
746+
"Return non-nil if point is on a delimiter character."
747+
(let ((syntax (syntax-after (point))))
748+
(and syntax
749+
(memq (syntax-class syntax) '(4 5))))) ; 4=open, 5=close
750+
751+
(defun neocaml--forward-sexp-hybrid (arg)
752+
"Hybrid `forward-sexp-function' combining tree-sitter and syntax table.
753+
When point is on a delimiter character (paren, bracket, brace),
754+
fall back to syntax-table-based matching so that commands like
755+
`delete-pair' find the correct matching delimiter. Otherwise,
756+
use tree-sitter sexp navigation.
757+
758+
This function is used on Emacs 29 and 30. Emacs 31+ handles
759+
this natively via the `list' thing in `treesit-thing-settings'.
760+
761+
ARG is as in `forward-sexp-function'."
762+
(let ((arg (or arg 1)))
763+
(if (or (neocaml--delimiter-p)
764+
;; Moving backward: check if the character before point
765+
;; (skipping whitespace) is a closing delimiter.
766+
(and (< arg 0)
767+
(save-excursion
768+
(skip-chars-backward " \t")
769+
(not (bobp))
770+
(let ((syntax (syntax-after (1- (point)))))
771+
(and syntax (eq (syntax-class syntax) 5))))))
772+
;; On a delimiter: use syntax-table paren matching.
773+
;; `forward-sexp-default-function' wraps `scan-sexps' but is
774+
;; only available from Emacs 30; fall back to raw `scan-sexps'
775+
;; on Emacs 29.
776+
(if (fboundp 'forward-sexp-default-function)
777+
(forward-sexp-default-function arg)
778+
(goto-char (or (scan-sexps (point) arg) (buffer-end arg))))
779+
;; Not on a delimiter: use tree-sitter node navigation.
780+
;; `treesit-forward-sexp' is available from Emacs 30; on Emacs 29
781+
;; fall back to the simpler `neocaml-forward-sexp'.
782+
(if (fboundp 'treesit-forward-sexp)
783+
(treesit-forward-sexp arg)
784+
(neocaml-forward-sexp arg)))))
785+
745786
(defun neocaml--thing-settings (language)
746787
"Return `treesit-thing-settings' definitions for LANGUAGE.
747-
Configures sexp, sentence, text, and comment navigation."
788+
Configures sexp, list, sentence, text, and comment navigation.
789+
790+
The `list' thing covers delimited container nodes (parentheses,
791+
brackets, braces, arrays). On Emacs 31+, defining it causes
792+
`treesit-major-mode-setup' to use the hybrid
793+
`treesit-forward-sexp-list' for `forward-sexp-function', which
794+
falls back to syntax-table matching for delimiter characters.
795+
This makes commands like `delete-pair' work correctly."
748796
`((,language
749797
(sexp (not ,(rx (or "{" "}" "(" ")" "[" "]" "[|" "|]"
750798
"," "." ";" ";;" ":" "::" ":>" "->"
751799
"<-" "=" "|" ".."))))
800+
(list ,(regexp-opt '("parenthesized_expression"
801+
"parenthesized_operator"
802+
"parenthesized_pattern"
803+
"parenthesized_type"
804+
"parenthesized_class_expression"
805+
"parenthesized_module_expression"
806+
"parenthesized_module_type"
807+
"list_expression"
808+
"list_pattern"
809+
"list_binding_pattern"
810+
"array_expression"
811+
"array_pattern"
812+
"array_binding_pattern"
813+
"record_expression"
814+
"record_pattern"
815+
"record_binding_pattern"
816+
"record_declaration"
817+
"object_expression"
818+
"object_type"
819+
"object_copy_expression"
820+
"polymorphic_variant_type"
821+
"package_type"
822+
"signature"
823+
"structure"
824+
"class_body_type")
825+
'symbols))
752826
(sentence ,(regexp-opt '("value_definition"
753827
"type_definition"
754828
"exception_definition"
@@ -1118,6 +1192,14 @@ the language-specific parts of the mode."
11181192

11191193
(treesit-major-mode-setup)
11201194

1195+
;; On Emacs 30, treesit-major-mode-setup sets forward-sexp-function
1196+
;; to treesit-forward-sexp, which doesn't fall back to scan-sexps
1197+
;; for delimiter characters. This breaks commands like delete-pair.
1198+
;; Use a hybrid function that delegates to scan-sexps on delimiters.
1199+
;; Emacs 31+ handles this natively via the `list' thing.
1200+
(unless (fboundp 'treesit-forward-sexp-list)
1201+
(setq-local forward-sexp-function #'neocaml--forward-sexp-hybrid))
1202+
11211203
;; Workaround for treesit-transpose-sexps being broken on Emacs 30
11221204
;; (bug#60655). Emacs 31 rewrites the function to work correctly.
11231205
(when (and (fboundp 'transpose-sexps-default-function)
@@ -1155,9 +1237,10 @@ for .ml files and `neocaml-interface-mode' for .mli files."
11551237

11561238
(setq-local indent-line-function #'treesit-indent)
11571239

1158-
;; Fallback for Emacs 29 (no treesit-thing-settings)
1240+
;; Emacs 29 has no treesit-thing-settings, so treesit-major-mode-setup
1241+
;; won't configure forward-sexp. Set the hybrid function directly.
11591242
(unless (boundp 'treesit-thing-settings)
1160-
(setq-local forward-sexp-function #'neocaml-forward-sexp))
1243+
(setq-local forward-sexp-function #'neocaml--forward-sexp-hybrid))
11611244
(setq-local treesit-defun-type-regexp
11621245
(cons neocaml--defun-type-regexp
11631246
#'neocaml--defun-valid-p))

test/neocaml-navigation-test.el

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,76 @@
222222
(point)))
223223
:to-be-truthy))))
224224

225+
;;;; list navigation (Emacs 30+ only)
226+
227+
(describe "navigation: list (Emacs 30+)"
228+
(before-all
229+
(unless (treesit-language-available-p 'ocaml)
230+
(signal 'buttercup-pending "tree-sitter OCaml grammar not available"))
231+
(unless (boundp 'treesit-thing-settings)
232+
(signal 'buttercup-pending "treesit-thing-settings not available (requires Emacs 30+)")))
233+
234+
(it "forward-list moves over a parenthesized expression"
235+
(with-neocaml-buffer "let x = (1 + 2) + 3\n"
236+
(search-forward "= ")
237+
(forward-list)
238+
(expect (char-before) :to-equal ?\))))
239+
240+
(it "forward-list moves over a list expression"
241+
(with-neocaml-buffer "let x = [1; 2; 3]\n"
242+
(search-forward "= ")
243+
(forward-list)
244+
(expect (char-before) :to-equal ?\])))
245+
246+
(it "forward-list moves over a record expression"
247+
(with-neocaml-buffer "let x = {a = 1; b = 2}\n"
248+
(search-forward "= ")
249+
(forward-list)
250+
(expect (char-before) :to-equal ?})))
251+
252+
(it "forward-list moves over an array expression"
253+
(with-neocaml-buffer "let x = [|1; 2; 3|]\n"
254+
(search-forward "= ")
255+
(forward-list)
256+
(expect (looking-back "\\|\\]" (- (point) 2)) :to-be-truthy)))
257+
258+
(it "up-list moves out of a parenthesized expression"
259+
(with-neocaml-buffer "let x = (1 + 2)\n"
260+
(search-forward "1 ")
261+
(up-list)
262+
(expect (char-before) :to-equal ?\))))
263+
264+
(it "down-list moves into a parenthesized expression"
265+
(with-neocaml-buffer "let x = (1 + 2)\n"
266+
(search-forward "= ")
267+
(down-list)
268+
(expect (char-before) :to-equal ?\()))
269+
270+
(it "forward-list moves over a polymorphic variant type"
271+
(with-neocaml-buffer "type t = [ `Foo | `Bar ]\n"
272+
(search-forward "= ")
273+
(forward-list)
274+
(expect (char-before) :to-equal ?\])))
275+
276+
(it "forward-list moves over a package type"
277+
(with-neocaml-buffer "type t = (module S)\n"
278+
(search-forward "= ")
279+
(forward-list)
280+
(expect (char-before) :to-equal ?\))))
281+
282+
(it "delete-pair removes matching parentheses"
283+
(with-neocaml-buffer "let x = (1 + 2)\n"
284+
(search-forward "= ")
285+
(delete-pair)
286+
(expect (buffer-string) :to-equal "let x = 1 + 2\n")))
287+
288+
(it "delete-pair removes correct parens in nested code"
289+
(with-neocaml-buffer "let _ =\n (let world = \"world\" in\n Printf.printf \"Hello %s\\n\" world);\n ()\n"
290+
(search-forward " (let")
291+
(backward-char 4) ;; point on the opening paren before "let"
292+
(delete-pair)
293+
;; The semicolon paren should still be intact, and () at the end too
294+
(expect (buffer-string) :to-match "let world")
295+
(expect (buffer-string) :to-match "()"))))
296+
225297
;;; neocaml-navigation-test.el ends here

0 commit comments

Comments
 (0)