Skip to content

Support syntax highlighting for embedded JS and C++ #103

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

Merged
merged 1 commit into from
May 30, 2025
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
- [#99](https://github.com/clojure-emacs/clojure-ts-mode/pull/99): Fix bug in `clojure-ts-align` when nested form has extra spaces.
- [#99](https://github.com/clojure-emacs/clojure-ts-mode/pull/99): Fix bug in `clojure-ts-unwind` when there is only one expression after
threading symbol.
- Introduce `clojure-ts-jank-use-cpp-parser` customization which allows
highlighting C++ syntax in Jank `native/raw` forms.
- Introduce `clojure-ts-clojurescript-use-js-parser` customization which allows
highlighting JS syntax in ClojureScript `js*` forms.


## 0.4.0 (2025-05-15)

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ Once installed, evaluate `clojure-ts-mode.el` and you should be ready to go.
- [tree-sitter-regex](https://github.com/tree-sitter/tree-sitter-regex/releases/tag/v0.24.3), which will be used for regex literals if available and if
`clojure-ts-use-regex-parser` is not `nil`.

`clojure-ts-clojurescript-mode` can optionally use `tree-sitter-javascript` grammar
to highlight JS syntax in `js*` forms. This is enabled by default and can be
turned off by setting `clojure-ts-clojurescript-use-js-parser` to `nil`.

`clojure-ts-jank-mode` can optionally use `tree-sitter-cpp` grammar to highlight C++
syntax in `native/raw` forms. This is enabled by default and can be turned off by
setting `clojure-ts-jank-use-cpp-parser` to `nil`.

If you have `git` and a C compiler (`cc`) available on your system's `PATH`,
`clojure-ts-mode` will install the
grammars when you first open a Clojure file and `clojure-ts-ensure-grammars` is
Expand Down
120 changes: 115 additions & 5 deletions clojure-ts-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ double quotes on the third column."
:safe #'booleanp
:package-version '(clojure-ts-mode . "0.4"))

(defcustom clojure-ts-clojurescript-use-js-parser t
"When non-nil, use JS grammar to highlight syntax in js* forms."
:type 'boolean
:safe #'booleanp
:package-version '(clojure-ts-mode . "0.5"))

(defcustom clojure-ts-jank-use-cpp-parser t
"When non-nil, use C++ grammar to highlight syntax in native/raw forms."
:type 'boolean
:safe #'booleanp
:package-version '(clojure-ts-mode . "0.5"))

(defcustom clojure-ts-auto-remap t
"When non-nil, redirect all `clojure-mode' buffers to `clojure-ts-mode'."
:safe #'booleanp
Expand Down Expand Up @@ -489,6 +501,34 @@ When USE-REGEX is non-nil, include range settings for regex parser."
:local t
'((regex_content) @capture)))))

(defun clojure-ts--fontify-string (node override _start _end &optional _rest)
"Fontify string content NODE with `font-lock-string-face'.

In order to support embedded syntax highlighting for JS in ClojureScript
and C++ in Jank we need to avoid fontifying string content in some
special forms, such as native/raw in Jank and js* in ClojureScript,
otherwise string face will interfere with embedded parser's faces.

This function respects OVERRIDE argument by passing it to
`treesit-fontify-with-override'.

START and END arguments that are passed to this function are not start
and end of the NODE, so we ignore them."
(let* ((prev (treesit-node-prev-sibling (treesit-node-parent node)))
(jank-native-p (and (derived-mode-p 'clojure-ts-jank-mode)
clojure-ts-jank-use-cpp-parser
(clojure-ts--symbol-node-p prev)
(string= (treesit-node-text prev) "native/raw")))
(js-interop-p (and (derived-mode-p 'clojure-ts-clojurescript-mode)
clojure-ts-clojurescript-use-js-parser
(clojure-ts--symbol-node-p prev)
(string= (treesit-node-text prev) "js*"))))
(when (not (or jank-native-p js-interop-p))
(treesit-fontify-with-override (treesit-node-start node)
(treesit-node-end node)
'font-lock-string-face
override))))

(defun clojure-ts--font-lock-settings (markdown-available regex-available)
"Return font lock settings suitable for use in `treesit-font-lock-settings'.

Expand All @@ -501,7 +541,9 @@ literals with regex grammar."
(treesit-font-lock-rules
:feature 'string
:language 'clojure
'((str_lit) @font-lock-string-face
'((str_lit open: _ @font-lock-string-face
(str_content) @clojure-ts--fontify-string
close: _ @font-lock-string-face)
(regex_lit) @font-lock-regexp-face)

:feature 'regex
Expand Down Expand Up @@ -1400,7 +1442,6 @@ regexes with anchors matching the beginning and end of the line are
used."
`((clojure
((parent-is "^source$") parent-bol 0)
(clojure-ts--match-docstring parent 0)
;; Literal Sequences
((parent-is "^vec_lit$") parent 1) ;; https://guide.clojure.style/#bindings-alignment
((parent-is "^map_lit$") parent 1) ;; https://guide.clojure.style/#map-keys-alignment
Expand All @@ -1418,7 +1459,12 @@ used."
;; https://guide.clojure.style/#one-space-indent
((parent-is "^list_lit$") parent 1)
((parent-is "^anon_fn_lit$") parent 2)
(clojure-ts--match-with-metadata parent 0))))
(clojure-ts--match-with-metadata parent 0)
;; This is slow and only matches when point is inside of a docstring and
;; only when Markdown grammar is disabled. `indent-region' tries to match
;; all the rules from top to bottom, so order matters here (the slowest
;; rules should be at the bottom).
(clojure-ts--match-docstring parent 0))))

(defun clojure-ts--configured-indent-rules ()
"Gets the configured choice of indent rules."
Expand Down Expand Up @@ -2518,6 +2564,44 @@ function can also be used to upgrade the grammars if they are outdated."
(let ((treesit-language-source-alist clojure-ts-grammar-recipes))
(treesit-install-language-grammar grammar)))))

(defsubst clojure-ts--font-lock-setting-update-override (setting)
"Return SETTING with override set to TRUE."
(let ((new-setting (copy-tree setting)))
(setf (nth 3 new-setting) t)
new-setting))

(defun clojure-ts--harvest-treesit-configs (mode)
"Harvest tree-sitter configs from MODE.
Return a plist with the following keys and value:

:font-lock (from `treesit-font-lock-settings')
:simple-indent (from `treesit-simple-indent-rules')"
(with-temp-buffer
(funcall mode)
;; We need to set :override t for all external queries, otherwise new faces
;; won't be applied on top of the string face defined for `clojure-ts-mode'.
(list :font-lock (seq-map #'clojure-ts--font-lock-setting-update-override
treesit-font-lock-settings)
:simple-indent treesit-simple-indent-rules)))

(defun clojure-ts--add-config-for-mode (mode)
"Add configurations for MODE to current buffer.

Configuration includes font-lock and indent. For font-lock rules, use
the same features enabled in MODE."
(let ((configs (clojure-ts--harvest-treesit-configs mode)))
(setq treesit-font-lock-settings
(append treesit-font-lock-settings
(plist-get configs :font-lock)))
;; FIXME: This works a bit aggressively. `indent-region' always tries to
;; use rules for embedded parser. Without it users can format embedded code
;; in an arbitrary way.
;;
;; (setq treesit-simple-indent-rules
;; (append treesit-simple-indent-rules
;; (plist-get configs :simple-indent)))
))

(defun clojure-ts-mode-variables (&optional markdown-available regex-available)
"Initialize buffer-local variables for `clojure-ts-mode'.

Expand Down Expand Up @@ -2625,7 +2709,20 @@ REGEX-AVAILABLE."
(define-derived-mode clojure-ts-clojurescript-mode clojure-ts-mode "ClojureScript[TS]"
"Major mode for editing ClojureScript code.

\\{clojure-ts-clojurescript-mode-map}")
\\{clojure-ts-clojurescript-mode-map}"
(when (and clojure-ts-clojurescript-use-js-parser
(treesit-ready-p 'javascript t))
(setq-local treesit-range-settings
(append treesit-range-settings
(treesit-range-rules
:embed 'javascript
:host 'clojure
:local t
'(((list_lit (sym_lit) @_sym-name
:anchor (str_lit (str_content) @capture))
(:equal @_sym-name "js*"))))))
(clojure-ts--add-config-for-mode 'js-ts-mode)
(treesit-major-mode-setup)))

;;;###autoload
(define-derived-mode clojure-ts-clojurec-mode clojure-ts-mode "ClojureC[TS]"
Expand All @@ -2643,7 +2740,20 @@ REGEX-AVAILABLE."
(define-derived-mode clojure-ts-jank-mode clojure-ts-mode "Jank[TS]"
"Major mode for editing Jank code.

\\{clojure-ts-jank-mode-map}")
\\{clojure-ts-jank-mode-map}"
(when (and clojure-ts-jank-use-cpp-parser
(treesit-ready-p 'cpp t))
(setq-local treesit-range-settings
(append treesit-range-settings
(treesit-range-rules
:embed 'cpp
:host 'clojure
:local t
'(((list_lit (sym_lit) @_sym-name
:anchor (str_lit (str_content) @capture))
(:equal @_sym-name "native/raw"))))))
(clojure-ts--add-config-for-mode 'c++-ts-mode)
(treesit-major-mode-setup)))

(defun clojure-ts--register-novel-modes ()
"Set up Clojure modes not present in progenitor clojure-mode.el."
Expand Down
12 changes: 12 additions & 0 deletions test/samples/embed.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
(ns embed)

(js* "var hello = console.log('hello'); const now = new Date();")

(js* "const hello = new Date();
const someOtherVar = 'Just a string';")

(println "This is a normal string")

"Standalone string"

(js* "var hello = 'world';")
6 changes: 5 additions & 1 deletion test/samples/native.jank
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
(defn set-shader-source! [shader source]
(native/raw "auto const shader(detail::to_int(~{ shader }));
auto const &source(detail::to_string(~{ source }));
__value = make_box();
__value = make_box(glShaderSource(shader, 1, &source.data, nullptr));"))

(defn compile-shader! [shader]
(native/raw "__value = make_box(glCompileShader(detail::to_int(~{ shader })));"))
(native/raw "__value = make_box(glCompileShader(detail::to_int(~{ shader })));")
"Normal string")

"Normal string"