Subject: [PATCH] Add rust-ts-mode

From: Randy Taylor <[email-masked]>
Date: Sat, 17 Dec 2022 02:17:07 +0000

X-Debbugs-CC: [email-masked]

Tried to match rust-mode's syntax highlighting where possible.

From 94c94d7c415b49f97c61506809f60f27be9d7c8b Mon Sep 17 00:00:00 2001
From: Randy Taylor <[email-masked]>
Date: Fri, 16 Dec 2022 16:05:29 -0500
Subject: [PATCH] Add rust-ts-mode

* etc/NEWS: Mention it.
* lisp/progmodes/eglot.el (eglot-server-programs): Add it.
* lisp/progmodes/rust-ts-mode.el: New major mode with
tree-sitter support.
---
 etc/NEWS                       |   4 +
 lisp/progmodes/eglot.el        |   2 +-
 lisp/progmodes/rust-ts-mode.el | 371 +++++++++++++++++++++++++++++++++
 3 files changed, 376 insertions(+), 1 deletion(-)
 create mode 100644 lisp/progmodes/rust-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 157fe98c98..50dea06875 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -3094,6 +3094,10 @@ the Go language.  It is auto-enabled for files with the ".go" extension.
 A major mode based on the tree-sitter library for editing "go.mod"
 files.  It is auto-enabled for files which are named "go.mod".
 
+*** New major mode 'rust-ts-mode'.
+A major mode based on the tree-sitter library for editing programs in
+the Rust language.  It is auto-enabled for files with the ".rs" extension.
+
 
 * Incompatible Lisp Changes in Emacs 29.1
 
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index 20534cfa1c..0c6faec00d 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -182,7 +182,7 @@ eglot-alternatives
                       when probe return (cons probe args)
                       finally (funcall err)))))))
 
-(defvar eglot-server-programs `((rust-mode . ,(eglot-alternatives '("rust-analyzer" "rls")))
+(defvar eglot-server-programs `(((rust-ts-mode rust-mode) . ,(eglot-alternatives '("rust-analyzer" "rls")))
                                 ((cmake-mode cmake-ts-mode) . ("cmake-language-server"))
                                 (vimrc-mode . ("vim-language-server" "--stdio"))
                                 ((python-mode python-ts-mode)
diff --git a/lisp/progmodes/rust-ts-mode.el b/lisp/progmodes/rust-ts-mode.el
new file mode 100644
index 0000000000..8b2ed19101
--- /dev/null
+++ b/lisp/progmodes/rust-ts-mode.el
@@ -0,0 +1,371 @@
+;;; rust-ts-mode.el --- tree-sitter support for Rust  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; Author     : Randy Taylor <[email-masked]>
+;; Maintainer : Randy Taylor <[email-masked]>
+;; Created    : December 2022
+;; Keywords   : rust languages tree-sitter
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(require 'treesit)
+(eval-when-compile (require 'rx))
+
+(declare-function treesit-parser-create "treesit.c")
+(declare-function treesit-induce-sparse-tree "treesit.c")
+(declare-function treesit-node-child "treesit.c")
+(declare-function treesit-node-child-by-field-name "treesit.c")
+(declare-function treesit-node-start "treesit.c")
+(declare-function treesit-node-type "treesit.c")
+
+(defcustom rust-ts-mode-indent-offset 4
+  "Number of spaces for each indentation step in `rust-ts-mode'."
+  :version "29.1"
+  :type 'integer
+  :safe 'integerp
+  :group 'rust)
+
+(defvar rust-ts-mode--syntax-table
+  (let ((table (make-syntax-table)))
+    (modify-syntax-entry ?+   "."      table)
+    (modify-syntax-entry ?-   "."      table)
+    (modify-syntax-entry ?=   "."      table)
+    (modify-syntax-entry ?%   "."      table)
+    (modify-syntax-entry ?&   "."      table)
+    (modify-syntax-entry ?|   "."      table)
+    (modify-syntax-entry ?^   "."      table)
+    (modify-syntax-entry ?!   "."      table)
+    (modify-syntax-entry ?@   "."      table)
+    (modify-syntax-entry ?~   "."      table)
+    (modify-syntax-entry ?<   "."      table)
+    (modify-syntax-entry ?>   "."      table)
+    (modify-syntax-entry ?/   ". 124b" table)
+    (modify-syntax-entry ?*   ". 23"   table)
+    (modify-syntax-entry ?\n  "> b"    table)
+    (modify-syntax-entry ?\^m "> b"    table)
+    table)
+  "Syntax table for `rust-ts-mode'.")
+
+(defvar rust-ts-mode--indent-rules
+  `((rust
+     ((node-is ")") parent-bol 0)
+     ((node-is "]") parent-bol 0)
+     ((node-is "}") (and parent parent-bol) 0)
+     ((parent-is "arguments") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "await_expression") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "array_expression") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "binary_expression") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "block") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "declaration_list") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "enum_variant_list") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "field_declaration_list") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "field_expression") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "field_initializer_list") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "let_declaration") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "macro_definition") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "parameters") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "token_tree") parent-bol rust-ts-mode-indent-offset)
+     ((parent-is "use_list") parent-bol rust-ts-mode-indent-offset)))
+  "Tree-sitter indent rules for `rust-ts-mode'.")
+
+(defvar rust-ts-mode--builtin-macros
+  '("concat_bytes" "concat_idents" "const_format_args"
+    "format_args_nl" "log_syntax" "trace_macros" "assert" "assert_eq"
+    "assert_ne" "cfg" "column" "compile_error" "concat" "dbg"
+    "debug_assert" "debug_assert_eq" "debug_assert_ne" "env" "eprint"
+    "eprintln" "file" "format" "format_args" "include" "include_bytes"
+    "include_str" "is_x86_feature_detected" "line" "matches"
+    "module_path" "option_env" "panic" "print" "println" "stringify"
+    "thread_local" "todo" "try" "unimplemented" "unreachable" "vec"
+    "write" "writeln")
+  "Rust built-in macros for tree-sitter font-locking.")
+
+(defvar rust-ts-mode--keywords
+  '("as" "async" "await" "break" "const" "continue" "dyn" "else"
+    "enum" "extern" "fn" "for" "if" "impl" "in" "let" "loop" "match"
+    "mod" "move" "pub" "ref" "return" "static" "struct" "trait" "type"
+    "union" "unsafe" "use" "where" "while" (crate) (self) (super)
+    (mutable_specifier))
+  "Rust keywords for tree-sitter font-locking.")
+
+(defvar rust-ts-mode--operators
+  '("!"  "!=" "%" "%=" "&" "&=" "&&" "*" "*=" "+" "+=" "," "-" "-="
+    "->" "."  ".."  "..=" "..."  "/" "/=" ":" ";" "<<" "<<=" "<" "<="
+    "=" "==" "=>" ">" ">=" ">>" ">>=" "@" "^" "^=" "|" "|=" "||" "?")
+  "Rust operators for tree-sitter font-locking.")
+
+(defvar rust-ts-mode--font-lock-settings
+  (treesit-font-lock-rules
+   :language 'rust
+   :feature 'attribute
+   '((attribute_item) @font-lock-constant-face
+     (inner_attribute_item) @font-lock-constant-face)
+
+   :language 'rust
+   :feature 'bracket
+   '((["(" ")" "[" "]" "{" "}"]) @font-lock-bracket-face)
+
+   :language 'rust
+   :feature 'builtin
+   `((macro_invocation
+      macro: ((identifier) @font-lock-builtin-face
+              (:match ,(rx-to-string
+                        `(seq bol
+                              (or ,@rust-ts-mode--builtin-macros)
+                              eol))
+                      @font-lock-builtin-face)))
+     ((identifier) @font-lock-type-face
+      (:match "^\\(:?Err\\|Ok\\|None\\|Some\\)$" @font-lock-type-face)))
+
+   :language 'rust
+   :feature 'comment
+   '(([(block_comment) (line_comment)]) @font-lock-comment-face)
+
+   :language 'rust
+   :feature 'constant
+   `((boolean_literal) @font-lock-constant-face
+     ((identifier) @font-lock-constant-face
+      (:match "^[A-Z][A-Z\\d_]*$" @font-lock-constant-face)))
+
+   :language 'rust
+   :feature 'delimiter
+   '((["," "." ";" ":" "::"]) @font-lock-delimiter-face)
+
+   :language 'rust
+   :feature 'function
+   '((call_expression
+      function:
+      [(identifier) @font-lock-function-name-face
+       (field_expression
+        field: (field_identifier) @font-lock-function-name-face)
+       (scoped_identifier
+        name: (identifier) @font-lock-function-name-face)])
+     (function_item (identifier) @font-lock-function-name-face)
+     (generic_function
+      function: [(identifier) @font-lock-function-name-face
+                 (field_expression
+                  field: (field_identifier) @font-lock-function-name-face)
+                 (scoped_identifier
+                  name: (identifier) @font-lock-function-name-face)])
+     (macro_definition "macro_rules!" @font-lock-constant-face)
+     (macro_definition (identifier) @font-lock-preprocessor-face)
+     (macro_invocation macro: (identifier) @font-lock-preprocessor-face))
+
+   :language 'rust
+   :feature 'keyword
+   `([,@rust-ts-mode--keywords] @font-lock-keyword-face)
+
+   :language 'rust
+   :feature 'number
+   '([(float_literal) (integer_literal)] @font-lock-number-face)
+
+   :language 'rust
+   :feature 'operator
+   `([,@rust-ts-mode--operators] @font-lock-operator-face)
+
+   :language 'rust
+   :feature 'string
+   '([(char_literal)
+      (raw_string_literal)
+      (string_literal)] @font-lock-string-face)
+
+   :language 'rust
+   :feature 'type
+   `((call_expression
+      function: (scoped_identifier
+                 path: (identifier) @font-lock-type-face))
+     (enum_variant name: (identifier) @font-lock-type-face)
+     (match_arm
+      pattern: (match_pattern (_ type: (identifier) @font-lock-type-face)))
+     (match_arm
+      pattern: (match_pattern
+                (_ type: (scoped_identifier
+                          path: (identifier) @font-lock-type-face))))
+     (mod_item name: (identifier) @font-lock-constant-face)
+     (primitive_type) @font-lock-type-face
+     (type_identifier) @font-lock-type-face
+     (scoped_identifier name: (identifier) @font-lock-type-face)
+     (scoped_identifier path: (identifier) @font-lock-constant-face)
+     (scoped_identifier
+      (scoped_identifier
+       path: (identifier) @font-lock-constant-face))
+     ((scoped_identifier
+       path: [(identifier) @font-lock-type-face
+              (scoped_identifier
+               name: (identifier) @font-lock-type-face)])
+      (:match "^[A-Z]" @font-lock-type-face))
+     (scoped_type_identifier path: (identifier) @font-lock-constant-face)
+     (scoped_use_list
+      path: [(identifier) @font-lock-constant-face
+             (scoped_identifier (identifier) @font-lock-constant-face)])
+     (type_identifier) @font-lock-type-face
+     (use_as_clause alias: (identifier) @font-lock-type-face)
+     (use_list (identifier) @font-lock-type-face))
+
+   :language 'rust
+   :feature 'variable
+   '((identifier) @font-lock-variable-name-face
+     ;; Everything in a token_tree is an identifier.
+     (token_tree (identifier) @default))
+
+   :language 'rust
+   :feature 'escape-sequence
+   :override t
+   '((escape_sequence) @font-lock-escape-face)
+
+   :language 'rust
+   :feature 'property
+   :override t
+   '((field_identifier) @font-lock-property-face
+     (shorthand_field_initializer (identifier) @font-lock-property-face))
+
+   :language 'rust
+   :feature 'error
+   :override t
+   '((ERROR) @font-lock-warning-face))
+  "Tree-sitter font-lock settings for `rust-ts-mode'.")
+
+(defun rust-ts-mode--imenu ()
+  "Return Imenu alist for the current buffer."
+  (let* ((node (treesit-buffer-root-node))
+         (enum-tree (treesit-induce-sparse-tree
+                     node "enum_item" nil))
+         (enum-index (rust-ts-mode--imenu-1 enum-tree))
+         (func-tree (treesit-induce-sparse-tree
+                     node "function_item" nil))
+         (func-index (rust-ts-mode--imenu-1 func-tree))
+         (impl-tree (treesit-induce-sparse-tree
+                     node "impl_item" nil))
+         (impl-index (rust-ts-mode--imenu-1 impl-tree))
+         (mod-tree (treesit-induce-sparse-tree
+                    node "mod_item" nil))
+         (mod-index (rust-ts-mode--imenu-1 mod-tree))
+         (struct-tree (treesit-induce-sparse-tree
+                       node "struct_item" nil))
+         (struct-index (rust-ts-mode--imenu-1 struct-tree))
+         (type-tree (treesit-induce-sparse-tree
+                     node "type_item" nil))
+         (type-index (rust-ts-mode--imenu-1 type-tree)))
+    (append
+     (when mod-index `(("Module" . ,mod-index)))
+     (when enum-index `(("Enum" . ,enum-index)))
+     (when impl-index `(("Impl" . ,impl-index)))
+     (when type-index `(("Type" . ,type-index)))
+     (when struct-index `(("Struct" . ,struct-index)))
+     (when func-index `(("Fn" . ,func-index))))))
+
+(defun rust-ts-mode--imenu-1 (node)
+  "Helper for `rust-ts-mode--imenu'.
+Find string representation for NODE and set marker, then recurse
+the subtrees."
+  (let* ((ts-node (car node))
+         (children (cdr node))
+         (subtrees (mapcan #'rust-ts-mode--imenu-1
+                           children))
+         (name (when ts-node
+                 (pcase (treesit-node-type ts-node)
+                   ("enum_item"
+                    (treesit-node-text
+                     (treesit-node-child-by-field-name ts-node "name") t))
+                   ("function_item"
+                    (treesit-node-text
+                     (treesit-node-child-by-field-name ts-node "name") t))
+                   ("impl_item"
+                    (let ((trait-node (treesit-node-child-by-field-name ts-node "trait")))
+                      (concat
+                       (treesit-node-text
+                        trait-node t)
+                       (when trait-node
+                         " for ")
+                       (treesit-node-text
+                        (treesit-node-child-by-field-name ts-node "type") t))))
+                   ("mod_item"
+                    (treesit-node-text
+                     (treesit-node-child-by-field-name ts-node "name") t))
+                   ("struct_item"
+                    (treesit-node-text
+                     (treesit-node-child-by-field-name ts-node "name") t))
+                   ("type_item"
+                    (treesit-node-text
+                     (treesit-node-child-by-field-name ts-node "name") t)))))
+         (marker (when ts-node
+                   (set-marker (make-marker)
+                               (treesit-node-start ts-node)))))
+    (cond
+     ((or (null ts-node) (null name)) subtrees)
+     (subtrees
+      `((,name ,(cons name marker) ,@subtrees)))
+     (t
+      `((,name . ,marker))))))
+
+;;;###autoload
+(add-to-list 'auto-mode-alist '("\\.rs\\'" . rust-ts-mode))
+
+;;;###autoload
+(define-derived-mode rust-ts-mode prog-mode "Rust"
+  "Major mode for editing Rust, powered by tree-sitter."
+  :group 'rust
+  :syntax-table rust-ts-mode--syntax-table
+
+  (when (treesit-ready-p 'rust)
+    (treesit-parser-create 'rust)
+
+    ;; Comments.
+    (setq-local comment-start "// ")
+    (setq-local comment-end "")
+    (setq-local comment-start-skip (rx (or (seq "/" (+ "/"))
+                                           (seq "/" (+ "*")))
+                                       (* (syntax whitespace))))
+    (setq-local comment-end-skip
+                (rx (* (syntax whitespace))
+                    (group (or (syntax comment-end)
+                               (seq (+ "*") "/")))))
+
+    ;; Font-lock.
+    (setq-local treesit-font-lock-settings rust-ts-mode--font-lock-settings)
+    (setq-local treesit-font-lock-feature-list
+                '(( comment)
+                  ( keyword string)
+                  ( attribute builtin constant escape-sequence
+                    function number property type variable)
+                  ( bracket delimiter error operator)))
+
+    ;; Imenu.
+    (setq-local imenu-create-index-function #'rust-ts-mode--imenu)
+    (setq-local which-func-functions nil)
+
+    ;; Indent.
+    (setq-local indent-tabs-mode nil
+                treesit-simple-indent-rules rust-ts-mode--indent-rules)
+
+    ;; Navigation.
+    (setq-local treesit-defun-type-regexp
+                (regexp-opt '("enum_item"
+                              "function_item"
+                              "impl_item"
+                              "struct_item")))
+
+    (treesit-major-mode-setup)))
+
+(provide 'rust-ts-mode)
+
+;;; rust-ts-mode.el ends here
-- 
2.39.0