diff --git a/Eask b/Eask index 7f6aef4..cbf6865 100644 --- a/Eask +++ b/Eask @@ -4,7 +4,17 @@ (package-file "typescript-mode.el") -(files "*.el") +(files + "typescript-mode.el" + "typescript-mode-general-tests.el" + "typescript-mode-jsdoc-tests.el" + "typescript-mode-lexical-binding-tests.el" + "typescript-mode-tests.el" + "typescript-mode-test-utilities.el" + ;; "typescript-tree-sitter.el" + ;; "typescript-ts.el" + ) + (source "gnu") (source "melpa") diff --git a/README.md b/README.md index f77dda3..d681fac 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,32 @@ packages: Initializing these with `typescript.el` will then become a matter of creating your own `typescript-mode-hook` in your `init.el` file. + + +# Tree sitter integration +For now we have integration with both tree-sitter implementations. The oldest +one, the rust variant by @ubolonton is defined in `typescript-tree-sitter.el`, +and the newer, emacs feature branch variant is defined in +`typescript-ts.el`. The former requires the tree-sitter packages from MELPA, and +the latter requires an emacs built from the feature/tree-sitter upstream branch. + +Steps to get things working for now: + +1. Get the tree sitter lib dependency for emacs: +``` +git clone https://github.com/tree-sitter/tree-sitter.git +cd tree-sitter +make +(sudo) make install +``` +2. Get emacs from the feature branch: +- `git clone -b feature/tree-sitter git://git.sv.gnu.org/emacs.git` +- `cd emacs && make bootstrap && (sudo) make install` + +3. From the typescript repo +- run `install-tsx.sh `. If this script doesn't work for you try to find some + guide on the interwebs and report back to me. +- move the compiled file from `dist/` to `~/emacs/tree-sitter` + +4. enable `typescript-ts-mode` in some `.tsx` or `.ts` file and off you go + diff --git a/install-tsx.sh b/install-tsx.sh new file mode 100755 index 0000000..1d929d4 --- /dev/null +++ b/install-tsx.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +lang=$1 + +if [ $(uname) == "Darwin" ] +then + soext="dylib" +else + soext="so" +fi + +# Retrieve sources. +git clone "https://github.com/tree-sitter/tree-sitter-typescript.git" \ + --depth 1 +cd "tree-sitter-typescript/tsx/src" + +# Build. +cc -c -I. parser.c +# Compile scanner.c. +cc -fPIC -c -I. scanner.c +# Link. +cc -fPIC -shared *.o -o "libtree-sitter-tsx.${soext}" + +mkdir -p ../../../dist +cp "libtree-sitter-tsx.${soext}" ../../../dist +cd ../../../ +rm -rf "tree-sitter-tsx" diff --git a/typescript-tree-sitter.el b/typescript-tree-sitter.el new file mode 100644 index 0000000..78cff36 --- /dev/null +++ b/typescript-tree-sitter.el @@ -0,0 +1,197 @@ +;;; typescript-tree-sitter.el --- tree sitter support for Typescript -*- lexical-binding: t; -*- + +;; Copyright (C) Theodor Thornhill + +;; Author : Theodor Thornhill +;; Maintainer : Jostein Kjønigsen +;; Theodor Thornhill +;; Created : April 2022 +;; Modified : 2022 +;; Version : 0.4 +;; Keywords : typescript languages +;; X-URL : https://github.com/emacs-typescript/typescript.el +;; Package-Requires: ((emacs "26.1") (tree-sitter "0.12.1") (tree-sitter-indent "0.1") (tree-sitter-langs "0.9.1")) + +;; 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 . + +;; Note about indentation: +;; The indentation mechanics are adapted from Felipe Lemas tree-sitter-indent +;; package. We don't need a generic solution for now, because we are waiting +;; for Emacs proper. Let's just make it work for us, then move over to emacs +;; when that is ready. No need for a dependency. + +;;; Code: +(require 'cl-lib) +(require 'seq) +(require 'subr-x) + +(when t + ;; In order for the package to be usable and installable (and hence + ;; compilable) without tree-sitter, wrap the `require's within a dummy `when' + ;; so they're only executed when loading this file but not when compiling it. + + (require 'tree-sitter) + (require 'tree-sitter-hl) + (require 'tree-sitter-langs)) +;; Vars and functions defined by the above packages: +(defvar tree-sitter-major-mode-language-alist) +(declare-function tree-sitter-hl-mode "ext:tree-sitter-hl") +(declare-function tsc-node-end-position "ext:tree-sitter") +(declare-function tsc-node-start-position "ext:tree-sitter") + +(defvar typescript-tree-sitter-syntax-table) +(defvar typescript-tree-sitter-map) + +(defvar typescript-tree-sitter-scopes + '((indent + ;; if parent node is one of these and node is not first → indent + . (try_statement + if_statement + object + template_substitution + ;; function_declaration + interface_declaration + lexical_declaration + expression_statement + return_statement + named_imports + arguments + jsx_self_closing_element + jsx_element + jsx_opening_element)) + (outdent + ;; these nodes always outdent (1 shift in opposite direction) + . (")" + "}" + "]" + "/"))) + "Current scopes in use for tree-sitter-indent.") + +(defun typescript-tree-sitter--indent (node) + (let-alist typescript-tree-sitter-scopes + (member (tsc-node-type node) .indent))) + +(defun typescript-tree-sitter--outdent (node) + (let-alist typescript-tree-sitter-scopes + (member (tsc-node-type node) .outdent))) + +(defun typescript-tree-sitter--highest-node-at-position (position) + (save-excursion + (goto-char position) + (let ((current-node (tree-sitter-node-at-pos))) + (while (and + current-node + (when-let ((parent-node (tsc-get-parent current-node))) + (when (and ;; parent and current share same position + (eq (tsc-node-start-byte parent-node) + (tsc-node-start-byte current-node))) + (setq current-node parent-node))))) + current-node))) + +(defun typescript-tree-sitter--parentwise-path (node) + (let ((path (list node)) + (next-parent-node (tsc-get-parent node))) + (while next-parent-node + (push next-parent-node path) + (setq next-parent-node (tsc-get-parent next-parent-node))) + path)) + +(cl-defun typescript-tree-sitter--indents-in-path (parentwise-path) + (seq-map + (lambda (current-node) + (cond + ((typescript-tree-sitter--outdent current-node) 'outdent) + ((typescript-tree-sitter--indent current-node) 'indent) + (t 'no-indent))) + parentwise-path)) + +(defun typescript-tree-sitter--updated-column (column indent) + (pcase indent + (`no-indent column) + (`indent (+ column typescript-tree-sitter-indent-offset)) + (`outdent (- column typescript-tree-sitter-indent-offset)) + (_ (error "Unexpected indent instruction: %s" indent)))) + +(cl-defun typescript-tree-sitter--indent-column () + (seq-reduce + #'typescript-tree-sitter--updated-column + (typescript-tree-sitter--indents-in-path + (typescript-tree-sitter--parentwise-path + (typescript-tree-sitter--highest-node-at-position + (save-excursion (back-to-indentation) (point))))) + 0)) + +;;;; Public API + +;;;###autoload +(defun typescript-tree-sitter-indent-line () + (let ((first-non-blank-pos ;; see savep in `smie-indent-line' + (save-excursion + (forward-line 0) + (skip-chars-forward " \t") + (point))) + (new-column + (typescript-tree-sitter--indent-column))) + (when (numberp new-column) + (if (< first-non-blank-pos (point)) + (save-excursion (indent-line-to new-column)) + (indent-line-to new-column))))) + +(defgroup typescript-tree-sitter-indent nil "Indent lines using Tree-sitter as backend" + :group 'tree-sitter) + +(defcustom typescript-tree-sitter-indent-offset 2 + "Indent offset for typescript-tree-sitter-mode." + :type 'integer + :group 'typescript) + +(defvar typescript-tree-sitter-mode-map + (let ((map (make-sparse-keymap))) + map) + "Keymap used in typescript-tree-sitter buffers.") + +(defvar typescript-tree-sitter-mode-syntax-table + (let ((table (make-syntax-table))) + (modify-syntax-entry ?@ "_" table) + table)) + +;;;###autoload +(define-derived-mode typescript-tree-sitter-mode prog-mode "typescriptreact" + "Major mode for editing Typescript code. + +Key bindings: +\\{typescript-tree-sitter-mode-map}" + :group 'typescript + :syntax-table typescript-tree-sitter-mode-syntax-table + + (setq-local indent-line-function #'typescript-tree-sitter-indent-line) + ;; (setq-local beginning-of-defun-function #'typescript-beginning-of-defun) + ;; (setq-local end-of-defun-function #'typescript-end-of-defun) + + ;; https://github.com/ubolonton/emacs-tree-sitter/issues/84 + (unless font-lock-defaults + (setq font-lock-defaults '(nil))) + + ;; Comments + (setq-local comment-start "// ") + (setq-local comment-start-skip "\\(?://+\\|/\\*+\\)\\s *") + (setq-local comment-end "") + + (tree-sitter-hl-mode)) + +(add-to-list 'tree-sitter-major-mode-language-alist '(typescript-tree-sitter-mode . tsx)) + +(provide 'typescript-tree-sitter) + +;;; typescript-tree-sitter.el ends here diff --git a/typescript-ts.el b/typescript-ts.el new file mode 100644 index 0000000..ea3c739 --- /dev/null +++ b/typescript-ts.el @@ -0,0 +1,320 @@ +;;; typescript-ts.el --- tree sitter support for Typescript -*- lexical-binding: t; -*- + +;; Copyright (C) Theodor Thornhill + +;; Author : Theodor Thornhill +;; Maintainer : Theodor Thornhill +;; Created : April 2022 +;; Modified : 2022 +;; Version : 0.4 +;; Keywords : typescript languages +;; X-URL : https://github.com/emacs-typescript/typescript.el +;; Package-Requires: ((emacs "26.1") (tree-sitter "0.12.1") (tree-sitter-indent "0.1") (tree-sitter-langs "0.9.1")) + +;; 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) + + +(defcustom typescript-ts-indent-offset 2 + "Number of spaces for each indentation step in `typescript-mode'." + :type 'integer + :safe 'integerp + :group 'typescript) + +(defvar typescript-ts-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 `typescript-ts-mode'.") + +(defun ts-backward-up-list () + (lambda (node parent bol &rest _) + (save-excursion + (backward-up-list 1 nil t) + (goto-char + (treesit-node-start + (treesit-node-at (point) (point) 'tsx))) + (back-to-indentation) + (treesit-node-start + (treesit-node-at (point) (point) 'tsx))))) + +(defvar typescript-ts-indent-rules + `((tsx + (no-node (ts-backward-up-list) ,typescript-ts-indent-offset) + ((node-is "}") parent-bol 0) + ((node-is ")") parent-bol 0) + ((node-is "]") parent-bol 0) + ((node-is ">") parent-bol 0) + ((node-is ".") parent-bol ,typescript-ts-indent-offset) + ((parent-is "named_imports") parent-bol ,typescript-ts-indent-offset) + ((parent-is "statement_block") parent-bol ,typescript-ts-indent-offset) + ((parent-is "type_arguments") parent-bol ,typescript-ts-indent-offset) + ((parent-is "variable_declarator") parent-bol ,typescript-ts-indent-offset) + ((parent-is "arguments") parent-bol ,typescript-ts-indent-offset) + ((parent-is "array") parent-bol ,typescript-ts-indent-offset) + ((parent-is "formal_parameters") parent-bol ,typescript-ts-indent-offset) + ((parent-is "template_substitution") parent-bol ,typescript-ts-indent-offset) + ((parent-is "object_pattern") parent-bol ,typescript-ts-indent-offset) + ((parent-is "object") parent-bol ,typescript-ts-indent-offset) + ((parent-is "object_type") parent-bol ,typescript-ts-indent-offset) + ((parent-is "enum_body") parent-bol ,typescript-ts-indent-offset) + ((parent-is "arrow_function") parent-bol ,typescript-ts-indent-offset) + ((parent-is "parenthesized_expression") parent-bol ,typescript-ts-indent-offset) + + ;; JSX + ((parent-is "jsx_opening_element") parent ,typescript-ts-indent-offset) + ((node-is "jsx_closing_element") parent 0) + ((parent-is "jsx_element") parent ,typescript-ts-indent-offset) + ;; TODO(Theo): This one is a little off. Meant to hit the dangling '/' in + ;; a jsx-element. But it is also division operator... + ((node-is "/") parent 0) + ((parent-is "jsx_self_closing_element") parent ,typescript-ts-indent-offset)))) + +(defvar typescript-ts-font-lock-settings-1 + '((tsx + ( + ((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)) + (number) @font-lock-constant-face + + (string) @font-lock-string-face + + ;; template strings need to be last in the file for embedded expressions + ;; to work properly + (template_string) @font-lock-string-face + + (template_substitution + "${" @font-lock-constant-face + (_) + "}" @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 + )))) + +(defun typescript-ts-move-to-node (fn) + (when-let ((found-node (treesit-parent-until + (treesit-node-at (point) (point) 'tsx) + (lambda (parent) + (let ((parent-type (treesit-node-type parent))) + (or (equal "function_declaration" parent-type) + (equal "interface_declaration" parent-type))))))) + (goto-char (funcall fn found-node)))) + +(defun typescript-ts-beginning-of-defun (&optional arg) + (typescript-ts-move-to-node #'treesit-node-start)) + +(defun typescript-ts-end-of-defun (&optional arg) + (typescript-ts-move-to-node #'treesit-node-end)) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-ts-mode)) + +;;;###autoload +(add-to-list 'auto-mode-alist '("\\.tsx\\'" . typescript-ts-mode)) + +(define-derived-mode typescript-ts-mode prog-mode "typescriptreact" + "Major mode for editing typescript. + +Key bindings: + +\\{typescript-mode-map}" + + :group 'typescript + :syntax-table typescript-ts-syntax-table + + (unless (or (treesit-should-enable-p) + (treesit-language-available-p 'tsx)) + (error "Tree sitter isn't available. Did you compile emacs from the feature/tree-sitter branch?")) + + ;; Comments + (setq-local comment-start "// ") + (setq-local comment-start-skip "\\(?://+\\|/\\*+\\)\\s *") + (setq-local comment-end "") + + (treesit-get-parser-create 'tsx) + (setq-local treesit-simple-indent-rules typescript-ts-indent-rules) + (setq-local indent-line-function #'treesit-indent) + (setq-local beginning-of-defun-function #'typescript-ts-beginning-of-defun) + (setq-local end-of-defun-function #'typescript-ts-end-of-defun) + + ;; This needs to be non-nil, because reasons + (unless font-lock-defaults + (setq font-lock-defaults '(nil t))) + + (setq-local treesit-font-lock-defaults + '((typescript-ts-font-lock-settings-1))) + + (treesit-font-lock-enable)) + +(provide 'typescript-ts) + +;;; typescript-ts.el ends here