Skip to content

Commit a16c6b4

Browse files
rrudakovbbatsov
authored andcommitted
[#23] Support syntax highlighting for embedded JS and C++
1 parent 545991d commit a16c6b4

File tree

5 files changed

+145
-6
lines changed

5 files changed

+145
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
- [#99](https://github.com/clojure-emacs/clojure-ts-mode/pull/99): Fix bug in `clojure-ts-align` when nested form has extra spaces.
1212
- [#99](https://github.com/clojure-emacs/clojure-ts-mode/pull/99): Fix bug in `clojure-ts-unwind` when there is only one expression after
1313
threading symbol.
14+
- Introduce `clojure-ts-jank-use-cpp-parser` customization which allows
15+
highlighting C++ syntax in Jank `native/raw` forms.
16+
- Introduce `clojure-ts-clojurescript-use-js-parser` customization which allows
17+
highlighting JS syntax in ClojureScript `js*` forms.
18+
1419

1520
## 0.4.0 (2025-05-15)
1621

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ Once installed, evaluate `clojure-ts-mode.el` and you should be ready to go.
133133
- [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
134134
`clojure-ts-use-regex-parser` is not `nil`.
135135

136+
`clojure-ts-clojurescript-mode` can optionally use `tree-sitter-javascript` grammar
137+
to highlight JS syntax in `js*` forms. This is enabled by default and can be
138+
turned off by setting `clojure-ts-clojurescript-use-js-parser` to `nil`.
139+
140+
`clojure-ts-jank-mode` can optionally use `tree-sitter-cpp` grammar to highlight C++
141+
syntax in `native/raw` forms. This is enabled by default and can be turned off by
142+
setting `clojure-ts-jank-use-cpp-parser` to `nil`.
143+
136144
If you have `git` and a C compiler (`cc`) available on your system's `PATH`,
137145
`clojure-ts-mode` will install the
138146
grammars when you first open a Clojure file and `clojure-ts-ensure-grammars` is

clojure-ts-mode.el

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ double quotes on the third column."
128128
:safe #'booleanp
129129
:package-version '(clojure-ts-mode . "0.4"))
130130

131+
(defcustom clojure-ts-clojurescript-use-js-parser t
132+
"When non-nil, use JS grammar to highlight syntax in js* forms."
133+
:type 'boolean
134+
:safe #'booleanp
135+
:package-version '(clojure-ts-mode . "0.5"))
136+
137+
(defcustom clojure-ts-jank-use-cpp-parser t
138+
"When non-nil, use C++ grammar to highlight syntax in native/raw forms."
139+
:type 'boolean
140+
:safe #'booleanp
141+
:package-version '(clojure-ts-mode . "0.5"))
142+
131143
(defcustom clojure-ts-auto-remap t
132144
"When non-nil, redirect all `clojure-mode' buffers to `clojure-ts-mode'."
133145
:safe #'booleanp
@@ -489,6 +501,34 @@ When USE-REGEX is non-nil, include range settings for regex parser."
489501
:local t
490502
'((regex_content) @capture)))))
491503

504+
(defun clojure-ts--fontify-string (node override _start _end &optional _rest)
505+
"Fontify string content NODE with `font-lock-string-face'.
506+
507+
In order to support embedded syntax highlighting for JS in ClojureScript
508+
and C++ in Jank we need to avoid fontifying string content in some
509+
special forms, such as native/raw in Jank and js* in ClojureScript,
510+
otherwise string face will interfere with embedded parser's faces.
511+
512+
This function respects OVERRIDE argument by passing it to
513+
`treesit-fontify-with-override'.
514+
515+
START and END arguments that are passed to this function are not start
516+
and end of the NODE, so we ignore them."
517+
(let* ((prev (treesit-node-prev-sibling (treesit-node-parent node)))
518+
(jank-native-p (and (derived-mode-p 'clojure-ts-jank-mode)
519+
clojure-ts-jank-use-cpp-parser
520+
(clojure-ts--symbol-node-p prev)
521+
(string= (treesit-node-text prev) "native/raw")))
522+
(js-interop-p (and (derived-mode-p 'clojure-ts-clojurescript-mode)
523+
clojure-ts-clojurescript-use-js-parser
524+
(clojure-ts--symbol-node-p prev)
525+
(string= (treesit-node-text prev) "js*"))))
526+
(when (not (or jank-native-p js-interop-p))
527+
(treesit-fontify-with-override (treesit-node-start node)
528+
(treesit-node-end node)
529+
'font-lock-string-face
530+
override))))
531+
492532
(defun clojure-ts--font-lock-settings (markdown-available regex-available)
493533
"Return font lock settings suitable for use in `treesit-font-lock-settings'.
494534
@@ -501,7 +541,9 @@ literals with regex grammar."
501541
(treesit-font-lock-rules
502542
:feature 'string
503543
:language 'clojure
504-
'((str_lit) @font-lock-string-face
544+
'((str_lit open: _ @font-lock-string-face
545+
(str_content) @clojure-ts--fontify-string
546+
close: _ @font-lock-string-face)
505547
(regex_lit) @font-lock-regexp-face)
506548

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

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

2567+
(defsubst clojure-ts--font-lock-setting-update-override (setting)
2568+
"Return SETTING with override set to TRUE."
2569+
(let ((new-setting (copy-tree setting)))
2570+
(setf (nth 3 new-setting) t)
2571+
new-setting))
2572+
2573+
(defun clojure-ts--harvest-treesit-configs (mode)
2574+
"Harvest tree-sitter configs from MODE.
2575+
Return a plist with the following keys and value:
2576+
2577+
:font-lock (from `treesit-font-lock-settings')
2578+
:simple-indent (from `treesit-simple-indent-rules')"
2579+
(with-temp-buffer
2580+
(funcall mode)
2581+
;; We need to set :override t for all external queries, otherwise new faces
2582+
;; won't be applied on top of the string face defined for `clojure-ts-mode'.
2583+
(list :font-lock (seq-map #'clojure-ts--font-lock-setting-update-override
2584+
treesit-font-lock-settings)
2585+
:simple-indent treesit-simple-indent-rules)))
2586+
2587+
(defun clojure-ts--add-config-for-mode (mode)
2588+
"Add configurations for MODE to current buffer.
2589+
2590+
Configuration includes font-lock and indent. For font-lock rules, use
2591+
the same features enabled in MODE."
2592+
(let ((configs (clojure-ts--harvest-treesit-configs mode)))
2593+
(setq treesit-font-lock-settings
2594+
(append treesit-font-lock-settings
2595+
(plist-get configs :font-lock)))
2596+
;; FIXME: This works a bit aggressively. `indent-region' always tries to
2597+
;; use rules for embedded parser. Without it users can format embedded code
2598+
;; in an arbitrary way.
2599+
;;
2600+
;; (setq treesit-simple-indent-rules
2601+
;; (append treesit-simple-indent-rules
2602+
;; (plist-get configs :simple-indent)))
2603+
))
2604+
25212605
(defun clojure-ts-mode-variables (&optional markdown-available regex-available)
25222606
"Initialize buffer-local variables for `clojure-ts-mode'.
25232607
@@ -2625,7 +2709,20 @@ REGEX-AVAILABLE."
26252709
(define-derived-mode clojure-ts-clojurescript-mode clojure-ts-mode "ClojureScript[TS]"
26262710
"Major mode for editing ClojureScript code.
26272711
2628-
\\{clojure-ts-clojurescript-mode-map}")
2712+
\\{clojure-ts-clojurescript-mode-map}"
2713+
(when (and clojure-ts-clojurescript-use-js-parser
2714+
(treesit-ready-p 'javascript t))
2715+
(setq-local treesit-range-settings
2716+
(append treesit-range-settings
2717+
(treesit-range-rules
2718+
:embed 'javascript
2719+
:host 'clojure
2720+
:local t
2721+
'(((list_lit (sym_lit) @_sym-name
2722+
:anchor (str_lit (str_content) @capture))
2723+
(:equal @_sym-name "js*"))))))
2724+
(clojure-ts--add-config-for-mode 'js-ts-mode)
2725+
(treesit-major-mode-setup)))
26292726

26302727
;;;###autoload
26312728
(define-derived-mode clojure-ts-clojurec-mode clojure-ts-mode "ClojureC[TS]"
@@ -2643,7 +2740,20 @@ REGEX-AVAILABLE."
26432740
(define-derived-mode clojure-ts-jank-mode clojure-ts-mode "Jank[TS]"
26442741
"Major mode for editing Jank code.
26452742
2646-
\\{clojure-ts-jank-mode-map}")
2743+
\\{clojure-ts-jank-mode-map}"
2744+
(when (and clojure-ts-jank-use-cpp-parser
2745+
(treesit-ready-p 'cpp t))
2746+
(setq-local treesit-range-settings
2747+
(append treesit-range-settings
2748+
(treesit-range-rules
2749+
:embed 'cpp
2750+
:host 'clojure
2751+
:local t
2752+
'(((list_lit (sym_lit) @_sym-name
2753+
:anchor (str_lit (str_content) @capture))
2754+
(:equal @_sym-name "native/raw"))))))
2755+
(clojure-ts--add-config-for-mode 'c++-ts-mode)
2756+
(treesit-major-mode-setup)))
26472757

26482758
(defun clojure-ts--register-novel-modes ()
26492759
"Set up Clojure modes not present in progenitor clojure-mode.el."

test/samples/embed.cljs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
(ns embed)
2+
3+
(js* "var hello = console.log('hello'); const now = new Date();")
4+
5+
(js* "const hello = new Date();
6+
const someOtherVar = 'Just a string';")
7+
8+
(println "This is a normal string")
9+
10+
"Standalone string"
11+
12+
(js* "var hello = 'world';")

test/samples/native.jank

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
(defn set-shader-source! [shader source]
55
(native/raw "auto const shader(detail::to_int(~{ shader }));
66
auto const &source(detail::to_string(~{ source }));
7+
__value = make_box();
78
__value = make_box(glShaderSource(shader, 1, &source.data, nullptr));"))
89

910
(defn compile-shader! [shader]
10-
(native/raw "__value = make_box(glCompileShader(detail::to_int(~{ shader })));"))
11+
(native/raw "__value = make_box(glCompileShader(detail::to_int(~{ shader })));")
12+
"Normal string")
13+
14+
"Normal string"

0 commit comments

Comments
 (0)