Skip to content

Commit 746e8b2

Browse files
committed
Increase robustness of invalid syntax indentation.
Specific error recovery is now used to help maintain proper indentation when invalid syntax exists in the buffer. Anchoring to package, project, case, etc. occurs by looking for the corresponding keywords when syntax errors prevent the higher-level structures from being parsed correctly.
1 parent 876baef commit 746e8b2

6 files changed

+638
-24
lines changed

README.org

+13-9
Original file line numberDiff line numberDiff line change
@@ -244,15 +244,19 @@ the in-memory syntax tree may not accurately reflect the language
244244
specific node) and indentation rules may not be applied when they
245245
should, due to these errors.
246246

247-
To help combat this issue, it is suggested to use functionality that
248-
can help to reduce the number of syntax errors that might exist in the
249-
buffer at a particular point in time. Functionality such as enabling
250-
=electric-pair-mode= to insert matching parenthesis, quotation marks,
251-
etc. or using snippets (e.g., [[https://github.com/brownts/gpr-yasnippets][gpr-yasnippets]]) to automatically insert
252-
multi-line control constructs (e.g., project declarations, package
253-
declarations, case statements, etc.) are highly recommended. Not only
254-
can this help keep your buffer closer to a syntactically correct
255-
state, you also benefit from the productivity gains as well.
247+
To help combat this issue, specific indentation error recovery is used
248+
to help maintain indentation even when portions of the syntax are
249+
missing, which provides a best-effort approach to maintain accurate
250+
indentation. To further increase indentation accuracy, it is
251+
suggested to use functionality that can help to reduce the number of
252+
syntax errors that might exist in the buffer at a particular point in
253+
time. Functionality such as enabling =electric-pair-mode= to insert
254+
matching parenthesis, quotation marks, etc. or using snippets (e.g.,
255+
[[https://github.com/brownts/gpr-yasnippets][gpr-yasnippets]]) to automatically insert multi-line control constructs
256+
(e.g., project declarations, package declarations, case statements,
257+
etc.) are highly recommended. Not only can this help keep your buffer
258+
closer to a syntactically correct state, you also benefit from the
259+
productivity gains as well.
256260

257261
The indentation strategy can help recover from previously incorrect
258262
indentation that has occurred while the buffer was in a syntactically

doc/gpr-ts-mode.texi

+13-9
Original file line numberDiff line numberDiff line change
@@ -329,15 +329,19 @@ the in-memory syntax tree may not accurately reflect the language
329329
specific node) and indentation rules may not be applied when they
330330
should, due to these errors.
331331

332-
To help combat this issue, it is suggested to use functionality that
333-
can help to reduce the number of syntax errors that might exist in the
334-
buffer at a particular point in time. Functionality such as enabling
335-
@samp{electric-pair-mode} to insert matching parenthesis, quotation marks,
336-
etc. or using snippets (e.g., @uref{https://github.com/brownts/gpr-yasnippets, gpr-yasnippets}) to automatically insert
337-
multi-line control constructs (e.g., project declarations, package
338-
declarations, case statements, etc.) are highly recommended. Not only
339-
can this help keep your buffer closer to a syntactically correct
340-
state, you also benefit from the productivity gains as well.
332+
To help combat this issue, specific indentation error recovery is used
333+
to help maintain indentation even when portions of the syntax are
334+
missing, which provides a best-effort approach to maintain accurate
335+
indentation. To further increase indentation accuracy, it is
336+
suggested to use functionality that can help to reduce the number of
337+
syntax errors that might exist in the buffer at a particular point in
338+
time. Functionality such as enabling @samp{electric-pair-mode} to insert
339+
matching parenthesis, quotation marks, etc. or using snippets (e.g.,
340+
@uref{https://github.com/brownts/gpr-yasnippets, gpr-yasnippets}) to automatically insert multi-line control constructs
341+
(e.g., project declarations, package declarations, case statements,
342+
etc.) are highly recommended. Not only can this help keep your buffer
343+
closer to a syntactically correct state, you also benefit from the
344+
productivity gains as well.
341345

342346
The indentation strategy can help recover from previously incorrect
343347
indentation that has occurred while the buffer was in a syntactically

gpr-ts-mode.el

+216-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
;; Author: Troy Brown <[email protected]>
66
;; Created: February 2023
7-
;; Version: 0.6.2
7+
;; Version: 0.6.3
88
;; Keywords: gpr gnat ada languages tree-sitter
99
;; URL: https://github.com/brownts/gpr-ts-mode
1010
;; Package-Requires: ((emacs "29.1"))
@@ -289,6 +289,186 @@ SYMBOL, else the default value is updated instead."
289289
(funcall (gpr-ts-mode--next-sibling-not-matching type) node parent bol)))
290290
(car (treesit-simple-indent sibling-node parent (treesit-node-start sibling-node))))))
291291

292+
(defun gpr-ts-mode--prev-sibling (node parent bol &rest _)
293+
"Determine previous sibling in PARENT before this NODE or BOL."
294+
(if node
295+
(treesit-node-prev-sibling node)
296+
(car
297+
(reverse
298+
(treesit-filter-child
299+
parent
300+
(lambda (n)
301+
(< (treesit-node-start n) bol)))))))
302+
303+
(defun gpr-ts-mode--prev-sibling-matches-p (type)
304+
"Check if previous sibling matches TYPE."
305+
(lambda (node parent bol &rest _)
306+
(if-let ((prev (gpr-ts-mode--prev-sibling node parent bol)))
307+
(string-equal (treesit-node-type prev) type))))
308+
309+
(defun gpr-ts-mode--prev-nonextra-sibling-matches-p (type)
310+
"Check if previous non-extra sibling matches TYPE."
311+
(lambda (node parent bol &rest _)
312+
(let ((prev (gpr-ts-mode--prev-sibling node parent bol)))
313+
(while (and prev (treesit-node-check prev 'extra))
314+
(setq prev (treesit-node-prev-sibling prev)))
315+
(when prev
316+
(string-equal (treesit-node-type prev) type)))))
317+
318+
(defun gpr-ts-mode--indent-error-recovery (&optional op)
319+
"Look for nearest indent error recovery point.
320+
If OP is nil or \\='anchor\\=', determine recovery anchor. If OP is
321+
\\='offset\\=', determine recovery offset."
322+
(lambda (node parent bol &rest _)
323+
(let ((compound-alist
324+
`(("when" . ( :compound-type "case_item"))
325+
("case" . ( :compound-type "case_construction"
326+
:offset gpr-ts-mode-indent-when-offset))
327+
("package" . ( :compound-type "package_declaration"
328+
:offset
329+
(lambda (_anchor)
330+
(if (and ,node (string-equal (treesit-node-type ,node) "end"))
331+
0
332+
gpr-ts-mode-indent-offset))))
333+
("project" . ( :compound-type "project_declaration"
334+
:predicate
335+
(lambda (n)
336+
(let* ((next (treesit-node-next-sibling n))
337+
(next-type (treesit-node-type next)))
338+
(or (null next-type)
339+
(not (string-equal next-type "'")))))
340+
;; Anchor at beginning of line to account
341+
;; for possible qualifier prefixes (e.g.,
342+
;; "abstract")
343+
:offset
344+
(lambda (_anchor)
345+
(if (and ,node (string-equal (treesit-node-type ,node) "end"))
346+
0
347+
gpr-ts-mode-indent-offset))
348+
:anchor-bol t))
349+
("(" . ( :compound-type ("attribute_reference"
350+
"expression_list"
351+
"typed_string_declaration"
352+
"attribute_declaration")
353+
:predicate
354+
(lambda (_n)
355+
(or (null ,node)
356+
;; Recovery point.
357+
(not (or
358+
(gpr-ts-mode--declaration-p ,node)
359+
(member (treesit-node-type ,node)
360+
'("ERROR" "when" "case" "package" "end"))))))
361+
:matching-pair ")"
362+
:offset
363+
(lambda (anchor)
364+
(if (and ,node (string-equal (treesit-node-type ,node) ")"))
365+
0
366+
(let ((anchor-column
367+
(save-excursion
368+
(goto-char (treesit-node-start anchor))
369+
(current-column)))
370+
(next anchor))
371+
(while (and next
372+
(or (treesit-node-eq next anchor)
373+
;; skip comments
374+
(treesit-node-check next 'extra)))
375+
(save-excursion
376+
(goto-char (treesit-node-end next))
377+
(skip-chars-forward " \t\n" ,bol)
378+
(if (>= (point) ,bol)
379+
(setq next nil)
380+
(setq next (treesit-node-at (point))))))
381+
(if next
382+
(let ((anchor-column
383+
(save-excursion
384+
(goto-char (treesit-node-start anchor))
385+
(current-column))))
386+
(save-excursion
387+
(goto-char (treesit-node-start next))
388+
(- (current-column) anchor-column)))
389+
1))))))))
390+
(matches nil))
391+
(while (and parent (not matches))
392+
(setq matches
393+
(treesit-induce-sparse-tree
394+
parent
395+
(lambda (candidate)
396+
(when (< (treesit-node-start candidate) bol)
397+
(if-let* ((type (treesit-node-type candidate))
398+
(entry (alist-get type compound-alist nil nil #'equal))
399+
((let ((predicate (plist-get entry :predicate)))
400+
(or (null predicate)
401+
(funcall predicate candidate))))
402+
(parent (treesit-node-parent candidate))
403+
(parent-type (treesit-node-type parent))
404+
(compound-type (ensure-list (plist-get entry :compound-type))))
405+
(let ((matching-pair (plist-get entry :matching-pair)))
406+
(cond
407+
;; intact compound, no matching pair
408+
((and (member parent-type compound-type)
409+
(not matching-pair))
410+
(treesit-node-enclosed-p (cons bol bol) parent t))
411+
;; broken compound, no matching pair
412+
((and (not (member parent-type compound-type))
413+
(not matching-pair))
414+
t)
415+
;; matching pair, but not before BOL
416+
((null
417+
(treesit-filter-child
418+
parent
419+
(lambda (n)
420+
(and (string-equal (treesit-node-type n)
421+
matching-pair)
422+
(< (treesit-node-start candidate)
423+
(treesit-node-start n)
424+
bol)))))))))))
425+
(pcase op
426+
('offset
427+
(lambda (candidate)
428+
(let* ((type (treesit-node-type candidate))
429+
(entry (alist-get type compound-alist nil nil #'equal)))
430+
(let ((offset (plist-get entry :offset)))
431+
(pcase offset
432+
((pred null) gpr-ts-mode-indent-offset)
433+
((pred functionp) (funcall offset candidate))
434+
((pred integerp) offset)
435+
((pred symbolp) (symbol-value offset))
436+
(_ (error "Unknown offset: %s" offset)))))))
437+
((or 'anchor 'test (pred null))
438+
(lambda (candidate)
439+
(let* ((type (treesit-node-type candidate))
440+
(entry (alist-get type compound-alist nil nil #'equal))
441+
(anchor-bol (plist-get entry :anchor-bol)))
442+
(if anchor-bol
443+
(save-excursion
444+
(goto-char (treesit-node-start candidate))
445+
(forward-line 0)
446+
(treesit-node-at (point))
447+
(if (eq op 'test)
448+
(treesit-node-at (point))
449+
(treesit-node-start (treesit-node-at (point)))))
450+
(if (eq op 'test)
451+
candidate
452+
(treesit-node-start candidate))))))
453+
(_ (error "Unknown operation: %s" op)))
454+
nil))
455+
(setq parent (treesit-node-parent parent)))
456+
;; Pick the match which is closest to point
457+
(if (eq op 'test)
458+
matches
459+
(caar (reverse matches))))))
460+
461+
(defalias 'gpr-ts-mode--indent-error-recovery-exists-p
462+
'gpr-ts-mode--indent-error-recovery)
463+
464+
(defun gpr-ts-mode--anchor-of-indent-error-recovery ()
465+
"Determine indentation anchor of error recovery point."
466+
(gpr-ts-mode--indent-error-recovery 'anchor))
467+
468+
(defun gpr-ts-mode--offset-of-indent-error-recovery ()
469+
"Determine indentation offset of error recovery point."
470+
(gpr-ts-mode--indent-error-recovery 'offset))
471+
292472
(defun gpr-ts-mode--offset-of-next-sibling-not-matching (type)
293473
"Determine indentation offset of next sibling not matching TYPE."
294474
(lambda (node parent bol &rest _)
@@ -392,11 +572,41 @@ Return nil if no child of that type is found."
392572

393573
(defvar gpr-ts-mode--indent-rules
394574
`((gpr
575+
576+
;; Non-Parent-driven indentation
577+
578+
;; Indent empty lines immediately following a case_item as part
579+
;; of the case_item. This allows additional lines to keep being
580+
;; added to the case_item without causing indentation to jump
581+
;; after each newline.
582+
((and no-node
583+
(gpr-ts-mode--prev-nonextra-sibling-matches-p "case_item"))
584+
(gpr-ts-mode--anchor-first-sibling-matching "case_item")
585+
gpr-ts-mode-indent-offset)
586+
587+
;; Parent ERROR recovery rules.
588+
589+
((and (or (parent-is "ERROR")
590+
(gpr-ts-mode--prev-sibling-matches-p "ERROR"))
591+
(gpr-ts-mode--indent-error-recovery-exists-p))
592+
(gpr-ts-mode--anchor-of-indent-error-recovery)
593+
(gpr-ts-mode--offset-of-indent-error-recovery))
594+
595+
;; When previous parent error recovery fails, likely a top-level
596+
;; construct so anchor to the first column without an offset.
597+
;; This is a catch-all for any remaining parent ERROR nodes as
598+
;; many rules that follow assume a valid parent node and don't
599+
;; explicitly check.
600+
((parent-is "ERROR") column-0 0)
601+
602+
;; Normal indentation rules.
603+
395604
;; top-level
396605
((parent-is ,(rx bos "project" eos)) column-0 0)
397606
;; with_declaration
398607
((and (parent-is "with_declaration")
399608
(or (node-is "string_literal")
609+
no-node
400610
(node-is ","))
401611
(gpr-ts-mode--after-first-sibling-p "string_literal"))
402612
(gpr-ts-mode--anchor-first-sibling-matching "string_literal")
@@ -408,6 +618,7 @@ Return nil if no child of that type is found."
408618
;; expression / expression_list
409619
((and (parent-is "expression_list")
410620
(or (node-is ,(rx bos "expression" eos))
621+
no-node
411622
(node-is ","))
412623
(gpr-ts-mode--after-first-sibling-p "expression"))
413624
(gpr-ts-mode--anchor-first-sibling-matching "expression")
@@ -419,6 +630,10 @@ Return nil if no child of that type is found."
419630
((parent-is ,(rx bos "expression" eos))
420631
parent
421632
gpr-ts-mode-indent-exp-item-offset)
633+
((or (parent-is ,(rx bos "project_reference" eos))
634+
(parent-is ,(rx bos "variable_reference" eos)))
635+
parent
636+
0)
422637
((node-is "expression_list")
423638
parent
424639
gpr-ts-mode-indent-broken-offset)

test/resources/indent-package_declaration-nl.erts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ project Test is
1010
end Test;
1111
=-=
1212
project Test is
13-
package Package_A is
14-
|
13+
package Package_A is
14+
|
1515
end Test;
1616
=-=-=

0 commit comments

Comments
 (0)