diff options
| author | Yuan Fu | 2024-11-29 20:43:09 -0800 |
|---|---|---|
| committer | Yuan Fu | 2024-12-01 17:53:22 -0800 |
| commit | e37cd4fa597beaec3b491edb1b15ea0c19e72be4 (patch) | |
| tree | c9e7f33364e95c1a4597760833f3862212509cec | |
| parent | 4afdb7e80febd56f4024bad0aff4356198f6ce53 (diff) | |
| download | emacs-e37cd4fa597beaec3b491edb1b15ea0c19e72be4.tar.gz emacs-e37cd4fa597beaec3b491edb1b15ea0c19e72be4.zip | |
Add baseline tree-sitter indent rule for C-like languages
I found a really good baseline indent rule that handles a wide
range of situations very well. Now major modes can just start
with this rule and add exceptions on top.
This is worth mentioning in the manual, but that'll be a large
change, and doesn't have to be included in this commit.
* lisp/progmodes/c-ts-common.el:
(c-ts-common-list-indent-style): New variable.
(c-ts-common--standalone-parent):
(c-ts-common--prev-standalone-sibling):
(c-ts-common-parent-ignore-preproc):
(c-ts-common-baseline-indent-rule): New function.
* lisp/treesit.el (treesit--indent-prev-line-node): New function.
(treesit-simple-indent-presets): Add new preset.
| -rw-r--r-- | lisp/progmodes/c-ts-common.el | 163 | ||||
| -rw-r--r-- | lisp/treesit.el | 14 |
2 files changed, 177 insertions, 0 deletions
diff --git a/lisp/progmodes/c-ts-common.el b/lisp/progmodes/c-ts-common.el index d05b5248cc2..ab5c01bad2d 100644 --- a/lisp/progmodes/c-ts-common.el +++ b/lisp/progmodes/c-ts-common.el | |||
| @@ -532,6 +532,169 @@ characters on the current line." | |||
| 532 | (setq node (treesit-node-parent node))) | 532 | (setq node (treesit-node-parent node))) |
| 533 | (* level (symbol-value c-ts-common-indent-offset)))) | 533 | (* level (symbol-value c-ts-common-indent-offset)))) |
| 534 | 534 | ||
| 535 | ;;; Baseline indent rule | ||
| 536 | |||
| 537 | (defvar c-ts-common-list-indent-style 'align | ||
| 538 | "Intructs `c-ts-common-baseline-indent-rule' how to indent lists. | ||
| 539 | |||
| 540 | If the value is `align', indent lists like this: | ||
| 541 | |||
| 542 | const a = [ | ||
| 543 | 1, 2, 3 | ||
| 544 | 4, 5, 6, | ||
| 545 | ]; | ||
| 546 | |||
| 547 | If the value is `simple', indent lists like this: | ||
| 548 | |||
| 549 | const a = [ | ||
| 550 | 1, 2, 3, | ||
| 551 | 4, 5, 6, | ||
| 552 | ];") | ||
| 553 | |||
| 554 | (defun c-ts-common--standalone-parent (parent) | ||
| 555 | "Find the first parent that starts on a new line. | ||
| 556 | Start searching from PARENT, so if PARENT satisfies the condition, it'll | ||
| 557 | be returned. Return the starting position of the parent, return nil if | ||
| 558 | no parent satisfies the condition." | ||
| 559 | (save-excursion | ||
| 560 | (catch 'term | ||
| 561 | (while parent | ||
| 562 | (goto-char (treesit-node-start parent)) | ||
| 563 | (when (looking-back (rx bol (* whitespace)) | ||
| 564 | (line-beginning-position)) | ||
| 565 | (throw 'term (point))) | ||
| 566 | (setq parent (treesit-node-parent parent)))))) | ||
| 567 | |||
| 568 | (defun c-ts-common--prev-standalone-sibling (node) | ||
| 569 | "Return the previous sibling of NODE that starts on a new line. | ||
| 570 | Return nil if no sibling satisfies the condition." | ||
| 571 | (save-excursion | ||
| 572 | (setq node (treesit-node-prev-sibling node 'named)) | ||
| 573 | (goto-char (treesit-node-start node)) | ||
| 574 | (while (and node | ||
| 575 | (goto-char (treesit-node-start node)) | ||
| 576 | (not (looking-back (rx bol (* whitespace)) | ||
| 577 | (pos-bol)))) | ||
| 578 | (setq node (treesit-node-prev-sibling node 'named))) | ||
| 579 | node)) | ||
| 580 | |||
| 581 | (defun c-ts-common-parent-ignore-preproc (node) | ||
| 582 | "Return the parent of NODE, skipping preproc nodes." | ||
| 583 | (let ((parent (treesit-node-parent node)) | ||
| 584 | (pred (if (treesit-thing-defined-p | ||
| 585 | 'preproc (or (and node (treesit-node-language node)) | ||
| 586 | (treesit-parser-language | ||
| 587 | treesit-primary-parser))) | ||
| 588 | 'preproc | ||
| 589 | "preproc"))) | ||
| 590 | (while (and parent (treesit-node-match-p parent pred)) | ||
| 591 | (setq parent (treesit-node-parent parent))) | ||
| 592 | parent)) | ||
| 593 | |||
| 594 | (defun c-ts-common-baseline-indent-rule (node parent bol &rest _) | ||
| 595 | "Baseline indent rule for C-like languages. | ||
| 596 | |||
| 597 | NODE PARENT, BOL are like in other simple indent rules. | ||
| 598 | |||
| 599 | This rule works as follows: | ||
| 600 | |||
| 601 | Let PREV-NODE be the largest node that starts on previous line, | ||
| 602 | basically the NODE we get if we were indenting previous line. | ||
| 603 | |||
| 604 | 0. Closing brace aligns with first parent that starts on a new line. | ||
| 605 | |||
| 606 | 1. If PREV-NODE and NODE are siblings, align this line to previous | ||
| 607 | line (PREV-NODE as the anchor, and offset is 0). | ||
| 608 | |||
| 609 | 2. If PARENT is a list, ie, (...) [...], align with NODE's first | ||
| 610 | sibling. For the first sibling and the closing paren or bracket, indent | ||
| 611 | according to `c-ts-common-list-indent-style'. This rule also handles | ||
| 612 | initializer lists like {...}, but initializer lists doesn't respect | ||
| 613 | `c-ts-common-list-indent-style'--they always indent in the `simple' | ||
| 614 | style. | ||
| 615 | |||
| 616 | 3. Otherwise, go up the parse tree from NODE and look for a parent that | ||
| 617 | starts on a new line. Use that parent as the anchor and indent one | ||
| 618 | level. But if the node is a top-level construct (ignoring preprocessor | ||
| 619 | nodes), don't indent it. | ||
| 620 | |||
| 621 | This rule works for a wide range of scenarios including complex | ||
| 622 | situations. Major modes should use this as the fallback rule, and add | ||
| 623 | exception rules before it to cover the cases it doesn't apply. | ||
| 624 | |||
| 625 | This rule tries to be smart and ignore proprocessor node in some | ||
| 626 | situations. By default, any node that has \"proproc\" in its type are | ||
| 627 | considered a preprocessor node. If that heuristic is inaccurate, define | ||
| 628 | a `preproc' thing in `treesit-thing-settings', and this rule will use | ||
| 629 | the thing definition instead." | ||
| 630 | (let ((prev-line-node (treesit--indent-prev-line-node bol)) | ||
| 631 | (offset (symbol-value c-ts-common-indent-offset))) | ||
| 632 | (cond | ||
| 633 | ;; Condition 0. | ||
| 634 | ((and (treesit-node-match-p node "}") | ||
| 635 | (treesit-node-match-p (treesit-node-child parent 0) "{")) | ||
| 636 | (cons (c-ts-common--standalone-parent parent) | ||
| 637 | 0)) | ||
| 638 | ;; Condition 1. | ||
| 639 | ((and (treesit-node-eq (treesit-node-parent prev-line-node) | ||
| 640 | parent) | ||
| 641 | (not (treesit-node-match-p node (rx (or ")" "]"))))) | ||
| 642 | (cons (treesit-node-start prev-line-node) | ||
| 643 | 0)) | ||
| 644 | ;; Condition 2. | ||
| 645 | ((treesit-node-match-p (treesit-node-child parent 0) | ||
| 646 | (rx (or "(" "["))) | ||
| 647 | (let ((first-sibling (treesit-node-child parent 0 'named))) | ||
| 648 | (cond | ||
| 649 | ;; Closing delimeters. | ||
| 650 | ((treesit-node-match-p node (rx (or ")" "]"))) | ||
| 651 | (if (eq c-ts-common-list-indent-style 'align) | ||
| 652 | (cons (treesit-node-start (treesit-node-child parent 0)) | ||
| 653 | 0) | ||
| 654 | (cons (c-ts-common--standalone-parent parent) | ||
| 655 | 0))) | ||
| 656 | ;; First sibling. | ||
| 657 | ((treesit-node-eq node first-sibling) | ||
| 658 | (if (eq c-ts-common-list-indent-style 'align) | ||
| 659 | (cons (treesit-node-start (treesit-node-child parent 0)) | ||
| 660 | 1) | ||
| 661 | (cons (c-ts-common--standalone-parent parent) | ||
| 662 | offset))) | ||
| 663 | ;; Not first sibling | ||
| 664 | (t (cons (treesit-node-start | ||
| 665 | (or (c-ts-common--prev-standalone-sibling node) | ||
| 666 | first-sibling)) | ||
| 667 | 0))))) | ||
| 668 | ;; Condition 2 for initializer list, only apply to | ||
| 669 | ;; second line. Eg, | ||
| 670 | ;; | ||
| 671 | ;; return { 1, 2, 3, | ||
| 672 | ;; 4, 5, 6, --> Handled by this condition. | ||
| 673 | ;; 7, 8, 9 }; --> Handled by condition 1. | ||
| 674 | ((and (treesit-node-match-p (treesit-node-child parent 0) "{") | ||
| 675 | (treesit-node-prev-sibling node 'named)) | ||
| 676 | ;; If first sibling is a comment, indent like code; otherwise | ||
| 677 | ;; align to first sibling. | ||
| 678 | (if (treesit-node-match-p | ||
| 679 | (treesit-node-child parent 0 'named) | ||
| 680 | c-ts-common--comment-regexp 'ignore-missing) | ||
| 681 | (cons (c-ts-common--standalone-parent parent) | ||
| 682 | offset) | ||
| 683 | (cons (treesit-node-start | ||
| 684 | (treesit-node-child parent 0 'named)) | ||
| 685 | 0))) | ||
| 686 | ;; Before we fallback to condition 3, make sure we don't indent | ||
| 687 | ;; top-level stuff. | ||
| 688 | ((treesit-node-eq (treesit-parser-root-node | ||
| 689 | (treesit-node-parser parent)) | ||
| 690 | (c-ts-common-parent-ignore-preproc node)) | ||
| 691 | (cons (pos-bol) 0)) | ||
| 692 | ;; Condition 3. | ||
| 693 | (t (cons (c-ts-common--standalone-parent parent) | ||
| 694 | offset))))) | ||
| 695 | |||
| 696 | |||
| 697 | |||
| 535 | (provide 'c-ts-common) | 698 | (provide 'c-ts-common) |
| 536 | 699 | ||
| 537 | ;;; c-ts-common.el ends here | 700 | ;;; c-ts-common.el ends here |
diff --git a/lisp/treesit.el b/lisp/treesit.el index 3539942f19a..2acb46ab105 100644 --- a/lisp/treesit.el +++ b/lisp/treesit.el | |||
| @@ -1589,6 +1589,14 @@ should take the same argument as MATCHER or ANCHOR. If it matches, | |||
| 1589 | return a cons (ANCHOR-POS . OFFSET), where ANCHOR-POS is a position and | 1589 | return a cons (ANCHOR-POS . OFFSET), where ANCHOR-POS is a position and |
| 1590 | OFFSET is the indent offset; if it doesn't match, return nil.") | 1590 | OFFSET is the indent offset; if it doesn't match, return nil.") |
| 1591 | 1591 | ||
| 1592 | (defun treesit--indent-prev-line-node (pos) | ||
| 1593 | "Return the largest node on the previous line of POS." | ||
| 1594 | (save-excursion | ||
| 1595 | (goto-char pos) | ||
| 1596 | (when (eq (forward-line -1) 0) | ||
| 1597 | (back-to-indentation) | ||
| 1598 | (treesit--indent-largest-node-at (point))))) | ||
| 1599 | |||
| 1592 | (defvar treesit-simple-indent-presets | 1600 | (defvar treesit-simple-indent-presets |
| 1593 | (list (cons 'match | 1601 | (list (cons 'match |
| 1594 | (lambda | 1602 | (lambda |
| @@ -1639,6 +1647,12 @@ OFFSET is the indent offset; if it doesn't match, return nil.") | |||
| 1639 | (lambda (node &rest _) | 1647 | (lambda (node &rest _) |
| 1640 | (string-match-p | 1648 | (string-match-p |
| 1641 | type (or (treesit-node-type node) ""))))) | 1649 | type (or (treesit-node-type node) ""))))) |
| 1650 | ;; FIXME: Add to manual. | ||
| 1651 | (cons 'prev-line-is (lambda (type) | ||
| 1652 | (lambda (_n _p bol &rest _) | ||
| 1653 | (treesit-node-match-p | ||
| 1654 | (treesit--indent-prev-line-node bol) | ||
| 1655 | type)))) | ||
| 1642 | (cons 'field-is (lambda (name) | 1656 | (cons 'field-is (lambda (name) |
| 1643 | (lambda (node &rest _) | 1657 | (lambda (node &rest _) |
| 1644 | (string-match-p | 1658 | (string-match-p |