From 45b8204e09b7d399b792bb26c799efe48835c4a7 Mon Sep 17 00:00:00 2001 From: Theodor Thornhill Date: Tue, 11 Oct 2022 10:27:55 +0200 Subject: [PATCH] Add TypeScript support with tree-sitter * lisp/progmodes/ts-mode.el (ts-mode): New major mode for TypeScript with support for tree-sitter. It uses the TSX parser, so that we get support for TSX as well as TypeScript. If we cannot find tree-sitter, we default to using js-mode. --- etc/NEWS | 7 + lisp/progmodes/ts-mode.el | 364 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 lisp/progmodes/ts-mode.el diff --git a/etc/NEWS b/etc/NEWS index 88b1431d6a6..c5a142b500f 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2774,6 +2774,13 @@ Emacs buffers, like indentation and the like. The new ert function This is a lightweight variant of 'js-mode' that is used by default when visiting JSON files. + +** New mode ts-mode'. +Support is added for TypeScript, based on the new integration with +Tree-Sitter. There's support for font-locking, indentation and +navigation. Tree-Sitter is required for this mode to function, but if +it is not available, we will default to use 'js-mode'. + * Incompatible Lisp Changes in Emacs 29.1 diff --git a/lisp/progmodes/ts-mode.el b/lisp/progmodes/ts-mode.el new file mode 100644 index 00000000000..99ffe0c0f63 --- /dev/null +++ b/lisp/progmodes/ts-mode.el @@ -0,0 +1,364 @@ +;;; ts-mode.el --- tree sitter support for TypeScript -*- lexical-binding: t; -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; Author : Theodor Thornhill +;; Maintainer : Theodor Thornhill +;; Created : October 2022 +;; Keywords : typescript tsx languages tree-sitter + +;; This file is part of GNU Emacs. + +;; This program 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. + +;; This program 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 this program. If not, see . + +(require 'treesit) +(require 'rx) +(require 'js) + +(defcustom ts-mode-indent-offset 2 + "Number of spaces for each indentation step in `ts-mode'." + :type 'integer + :safe 'integerp + :group 'typescript) + +(defvar ts-mode--syntax-table + (let ((table (make-syntax-table))) + ;; Taken from the cc-langs version + (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 ?\240 "." table) + table) + "Syntax table for `ts-mode'.") + +(defvar ts-mode--indent-rules + `((tsx + ((node-is "}") parent-bol 0) + ((node-is ")") parent-bol 0) + ((node-is "]") parent-bol 0) + ((node-is ">") parent-bol 0) + ((node-is ".") + parent-bol ,ts-mode-indent-offset) + ((parent-is "ternary_expression") + parent-bol ,ts-mode-indent-offset) + ((parent-is "named_imports") + parent-bol ,ts-mode-indent-offset) + ((parent-is "statement_block") + parent-bol ,ts-mode-indent-offset) + ((parent-is "type_arguments") + parent-bol ,ts-mode-indent-offset) + ((parent-is "variable_declarator") + parent-bol ,ts-mode-indent-offset) + ((parent-is "arguments") + parent-bol ,ts-mode-indent-offset) + ((parent-is "array") + parent-bol ,ts-mode-indent-offset) + ((parent-is "formal_parameters") + parent-bol ,ts-mode-indent-offset) + ((parent-is "template_substitution") + parent-bol ,ts-mode-indent-offset) + ((parent-is "object_pattern") + parent-bol ,ts-mode-indent-offset) + ((parent-is "object") + parent-bol ,ts-mode-indent-offset) + ((parent-is "object_type") + parent-bol ,ts-mode-indent-offset) + ((parent-is "enum_body") + parent-bol ,ts-mode-indent-offset) + ((parent-is "arrow_function") + parent-bol ,ts-mode-indent-offset) + ((parent-is "parenthesized_expression") + parent-bol ,ts-mode-indent-offset) + + ;; TSX + ((parent-is "jsx_opening_element") + parent ,ts-mode-indent-offset) + ((node-is "jsx_closing_element") parent 0) + ((parent-is "jsx_element") + parent ,ts-mode-indent-offset) + ((node-is "/") parent 0) + ((parent-is "jsx_self_closing_element") + parent ,ts-mode-indent-offset) + (no-node parent-bol 0)))) + +(defvar ts-mode--settings + (treesit-font-lock-rules + :language 'tsx + :override t + '( + (template_string) @font-lock-string-face + + ((identifier) @font-lock-constant-face + (:match "^[A-Z_][A-Z_\\d]*$" @font-lock-constant-face)) + + (nested_type_identifier + module: (identifier) @font-lock-type-face) + (type_identifier) @font-lock-type-face + (predefined_type) @font-lock-type-face + + (new_expression + constructor: (identifier) @font-lock-type-face) + + (function + name: (identifier) @font-lock-function-name-face) + + (function_declaration + name: (identifier) @font-lock-function-name-face) + + (method_definition + name: (property_identifier) @font-lock-function-name-face) + + (variable_declarator + name: (identifier) @font-lock-function-name-face + value: [(function) (arrow_function)]) + + (variable_declarator + name: (array_pattern + (identifier) + (identifier) @font-lock-function-name-face) + value: (array (number) (function))) + + (assignment_expression + left: [(identifier) @font-lock-function-name-face + (member_expression + property: (property_identifier) @font-lock-function-name-face)] + right: [(function) (arrow_function)]) + + (call_expression + function: + [(identifier) @font-lock-function-name-face + (member_expression + property: (property_identifier) @font-lock-function-name-face)]) + + (variable_declarator + name: (identifier) @font-lock-variable-name-face) + + (enum_declaration (identifier) @font-lock-type-face) + + (enum_body (property_identifier) @font-lock-type-face) + + (enum_assignment name: (property_identifier) @font-lock-type-face) + + (assignment_expression + left: [(identifier) @font-lock-variable-name-face + (member_expression + property: (property_identifier) @font-lock-variable-name-face)]) + + (for_in_statement + left: (identifier) @font-lock-variable-name-face) + + (arrow_function + parameter: (identifier) @font-lock-variable-name-face) + + (arrow_function + parameters: + [(_ (identifier) @font-lock-variable-name-face) + (_ (_ (identifier) @font-lock-variable-name-face)) + (_ (_ (_ (identifier) @font-lock-variable-name-face)))]) + + + (pair key: (property_identifier) @font-lock-variable-name-face) + + (pair value: (identifier) @font-lock-variable-name-face) + + (pair + key: (property_identifier) @font-lock-function-name-face + value: [(function) (arrow_function)]) + + (property_signature + name: (property_identifier) @font-lock-variable-name-face) + + ((shorthand_property_identifier) @font-lock-variable-name-face) + + (pair_pattern + key: (property_identifier) @font-lock-variable-name-face) + + ((shorthand_property_identifier_pattern) + @font-lock-variable-name-face) + + (array_pattern (identifier) @font-lock-variable-name-face) + + (jsx_opening_element + [(nested_identifier (identifier)) (identifier)] + @font-lock-function-name-face) + + (jsx_closing_element + [(nested_identifier (identifier)) (identifier)] + @font-lock-function-name-face) + + (jsx_self_closing_element + [(nested_identifier (identifier)) (identifier)] + @font-lock-function-name-face) + + (jsx_attribute (property_identifier) @font-lock-constant-face) + + [(this) (super)] @font-lock-keyword-face + + [(true) (false) (null)] @font-lock-constant-face + (regex pattern: (regex_pattern)) @font-lock-string-face + (number) @font-lock-constant-face + + (string) @font-lock-string-face + (template_string) @font-lock-string-face + + (template_substitution + ["${" "}"] @font-lock-constant-face) + + ["!" + "abstract" + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "declare" + "default" + "delete" + "do" + "else" + "enum" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "implements" + "import" + "in" + "instanceof" + "interface" + "keyof" + "let" + "namespace" + "new" + "of" + "private" + "protected" + "public" + "readonly" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "type" + "typeof" + "var" + "void" + "while" + "with" + "yield" + ] @font-lock-keyword-face + + (comment) @font-lock-comment-face + ))) + +(defvar ts-mode--defun-type-regexp + (rx (or "class_declaration" + "method_definition" + "function_declaration" + "lexical_declaration")) + "Regular expression that matches type of defun nodes. +Used in `ts-mode--beginning-of-defun' and friends.") + +(defun ts-mode--beginning-of-defun (&optional arg) + "Tree-sitter `beginning-of-defun' function. +ARG is the same as in `beginning-of-defun." + (let ((arg (or arg 1))) + (if (> arg 0) + ;; Go backward. + (while (and (> arg 0) + (treesit-search-forward-goto + ts-mode--defun-type-regexp 'start nil t)) + (setq arg (1- arg))) + ;; Go forward. + (while (and (< arg 0) + (treesit-search-forward-goto + ts-mode--defun-type-regexp 'start)) + (setq arg (1+ arg)))))) + +(defun ts-mode--end-of-defun (&optional arg) + "Tree-sitter `end-of-defun' function. +ARG is the same as in `end-of-defun." + (let ((arg (or arg 1))) + (if (< arg 0) + ;; Go backward. + (while (and (< arg 0) + (treesit-search-forward-goto + ts-mode--defun-type-regexp 'end nil t)) + (setq arg (1+ arg))) + ;; Go forward. + (while (and (> arg 0) + (treesit-search-forward-goto + ts-mode--defun-type-regexp 'end)) + (setq arg (1- arg)))))) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.ts\\'" . ts-mode)) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.tsx\\'" . ts-mode)) + +;;;###autoload +(define-derived-mode ts-mode prog-mode "TypeScript" + "Major mode for editing TypeScript." + :group 'typescript + :syntax-table ts-mode--syntax-table + + (cond + ((and (treesit-can-enable-p) + (treesit-language-available-p 'tsx)) + ;; Comments + (setq-local comment-start "// ") + (setq-local comment-start-skip "\\(?://+\\|/\\*+\\)\\s *") + (setq-local comment-end "") + + (setq-local treesit-simple-indent-rules ts-mode--indent-rules) + (setq-local indent-line-function #'treesit-indent) + + (setq-local beginning-of-defun-function #'ts-mode--beginning-of-defun) + (setq-local end-of-defun-function #'ts-mode--end-of-defun) + + (unless font-lock-defaults + (setq font-lock-defaults '(nil t))) + + (setq-local treesit-font-lock-settings ts-mode--settings) + + (treesit-font-lock-enable)) + (t + (message "Tree sitter for TypeScript isn't available, defaulting to js-mode") + (js-mode)))) + +(provide 'ts-mode) + +;;; ts-mode.el ends here -- 2.39.2