Skip to content

Implement experimental TSX-support via tree-sitter, if available. #155

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Eask
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

27 changes: 27 additions & 0 deletions install-tsx.sh
Original file line number Diff line number Diff line change
@@ -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"
197 changes: 197 additions & 0 deletions typescript-tree-sitter.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
;;; typescript-tree-sitter.el --- tree sitter support for Typescript -*- lexical-binding: t; -*-

;; Copyright (C) Theodor Thornhill

;; Author : Theodor Thornhill <theo@thornhill.no>
;; Maintainer : Jostein Kjønigsen <jostein@gmail.com>
;; Theodor Thornhill <theo@thornhill.no>
;; 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 <http://www.gnu.org/licenses/>.

;; 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
Loading