branch: elpa/typst-ts-mode
commit 7646790a9626739f1fa6e1f7405c9d5922d5e46b
Author: Meow King <mr.meowk...@anche.no>
Commit: Meow King <mr.meowk...@anche.no>

    feat: load embedding language settings dynamically
---
 typst-ts-embedding-lang-settings.el | 230 ++++++++++++++++++++++++++++++++++++
 typst-ts-mode.el                    |  76 ++++++++++--
 2 files changed, 297 insertions(+), 9 deletions(-)

diff --git a/typst-ts-embedding-lang-settings.el 
b/typst-ts-embedding-lang-settings.el
new file mode 100644
index 0000000000..cde20a4e81
--- /dev/null
+++ b/typst-ts-embedding-lang-settings.el
@@ -0,0 +1,230 @@
+;;; typst-ts-embedding-lang-settings.el --- Embedding Languages Settings  -*- 
lexical-binding: t; -*-
+;; Copyright (C) 2023 Meow King <mr.meowk...@anche.no>
+
+;; This file is NOT part of 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 <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file contains settings for tree sitter languages.
+;; to test settings:
+;; emacs --batch -l ./typst-ts-embedding-lang-settings.el --eval 
"(typst-ts-embedding-lang-settings-test)"
+
+;;; Code:
+(require 'treesit)
+
+(defvar typst-ts-embedding-lang-settings
+  '((python . (:feature
+               python
+               :font-lock python--treesit-settings
+               :indentation nil
+               :ts-feature-list
+               '(( comment definition)
+                 ( keyword string type)
+                 ( assignment builtin constant decorator
+                   escape-sequence number string-interpolation )
+                 ( bracket delimiter function operator variable property ))))
+    (rust . (:feature
+             rust-ts-mode
+             :font-lock rust-ts-mode--font-lock-settings
+             :indentation rust-ts-mode--indent-rules
+             :ts-feature-list
+             '(( comment definition)
+               ( keyword string)
+               ( assignment attribute builtin constant escape-sequence
+                 number type)
+               ( bracket delimiter error function operator property 
variable)))))
+  "Settings for raw block languages.")
+
+
+;; from vimscript-ts-mode (https://github.com/nverno/vimscript-ts-mode)
+(defun typst-ts-els--merge-features (a b)
+  "Merge `treesit-font-lock-feature-list's A with B."
+  (cl-loop for x in a
+           for y in b
+           collect (seq-uniq (append x y))))
+
+;; hugely insprired by vimscript-ts-mode 
(https://github.com/nverno/vimscript-ts-mode)
+(defun typst-ts-els-merge-settings (settings)
+  "Merge SETTINGS."
+  (let ((feature (plist-get settings :feature))
+        (font-lock-settings (plist-get settings :font-lock))
+        (indentation-rules (plist-get settings :indentation))
+        (ts-feature-list (plist-get settings :ts-feature-list)))
+    (require feature)
+    (setq font-lock-settings (eval font-lock-settings))
+    (setq indentation-rules (eval indentation-rules))
+    (setq ts-feature-list (eval ts-feature-list))
+    (setq-local
+     treesit-font-lock-settings (append treesit-font-lock-settings
+                                        font-lock-settings)
+     treesit-simple-indent-rules (append treesit-simple-indent-rules
+                                         indentation-rules)
+     treesit-font-lock-feature-list (typst-ts-els--merge-features
+                                     treesit-font-lock-feature-list
+                                     ts-feature-list))))
+
+(defun typst-ts-els-merge-lang (lang)
+  "Merge embedding language LANG settings."
+  (let ((settings (alist-get lang typst-ts-embedding-lang-settings)))
+    (if settings
+        (typst-ts-els-merge-settings settings)
+      (signal 'lang-no-in-settings '(lang)))))
+
+(defun typst-ts-els--try-get-variable--try-name (prefixes postfixes)
+  (let (variable)
+    (catch 'exit
+      (cl-loop for prefix in prefixes
+               for postfix in postfixes
+               do
+               (ignore-errors
+                 (setq variable (intern (concat prefix postfix)))
+                 (throw 'exit (eval variable)))))))
+
+(defun typst-ts-els--try-get-fls--try-name (_lang prefixes)
+  (typst-ts-els--try-get-variable--try-name
+   prefixes
+   '("--font-lock-settings" "-font-lock-settings")))
+
+(defun typst-ts-els--try-get-ir--try-name (_lang prefixes)
+  (typst-ts-els--try-get-variable--try-name
+   prefixes
+   '("--indent-rules" "-indent-rules")))
+
+(defvar typst-ts-els--try-get-feature-name-list
+  '("-ts-mode" "-mode" "")
+  "Used by `typset-ts-els--try-require-feature'.")
+
+(defvar typst-ts-els--try-variable-prefix-list
+  '("-ts-mode" "-ts" "")
+  "Used by `typset-ts-els--try-require-feature'.")
+
+(defvar typst-ts-els--try-get-fls--func-list
+  '(typst-ts-els--try-get-fls--try-name)
+  "TODO documentation.")
+
+(defvar typst-ts-els--try-get-ir--func-list
+  '(typst-ts-els--try-get-ir--try-name)
+  "TODO documentation.")
+
+(defun typst-ts-els--try-require-feature (lang)
+  "Try to require feature for LANG.
+If success, then return the feature symbol, else nil."
+  (let (feature)
+    (catch 'exit
+      (dolist (elem typst-ts-els--try-get-feature-name-list)
+        (setq feature
+              (intern (concat (symbol-name lang) elem)))
+        (ignore-errors
+          (require feature)
+          (throw 'exit feature))))))
+
+(defun typst-ts-els--try-get-variable-value (lang prefixes func-list)
+  "Try to get value of a variable specified for LANG.
+The variable is like font lock settings or indentation rules.  It is specified
+by FUNC-LIST.
+Return nil if cannot get.
+PREFIXES: a list of variable name prefix.
+FUNC-LIST: functions to be called to get the variable value."
+  (let (value)
+    (catch 'exit
+      (dolist (func func-list)
+        (ignore-errors
+          (setq value (funcall func lang prefixes))
+          (when value
+            (throw 'exit value)))))))
+
+(defun typst-ts-els--try-get-font-lock-settings (lang prefixes)
+  (typst-ts-els--try-get-variable-value
+   lang prefixes typst-ts-els--try-get-fls--func-list))
+
+(defun typst-ts-els--try-get-indentation-rules (lang prefixes)
+  (typst-ts-els--try-get-variable-value
+   lang prefixes typst-ts-els--try-get-ir--func-list))
+
+;; NOTE this operation is high cost
+(defun typst-ts-els--try-get-ts-feature-list (mode)
+  (with-temp-buffer
+    (funcall mode)
+    treesit-font-lock-feature-list))
+
+(defvar-local typst-ts-els--include-languages
+    '(typst)
+  "DON'T MANUALLY CHANGE THIS VARIABLE!")
+
+(defun typst-ts-els-include-dynamically (_ranges _parser)
+  "Include language setting dynamically.
+Use this function as one notifier of `treesit-parser-notifiers'."
+  (let ((parsers (treesit-parser-list))
+        lang
+        feature prefixes font-lock-settings indentation-rules ts-feature-list)
+    (unless (eq (length parsers) (length typst-ts-els--include-languages))
+      (dolist (parser parsers)
+        (setq lang (treesit-parser-language parser))
+        (unless (member lang typst-ts-els--include-languages)
+          (unwind-protect
+              (condition-case _err
+                  ;; first try loading settings from configuration
+                  (typst-ts-els-merge-lang lang)
+                (error
+                 ;; if not found or encountering error during loading,
+                 ;; then guess variables to load
+                 (catch 'exit
+                   (ignore-errors
+                     ;; require feature
+                     (setq feature (typst-ts-els--try-require-feature lang))
+                     (unless feature
+                       (throw 'exit "no feature found"))
+
+                     (setq prefixes
+                           (cl-loop for postfix in 
typst-ts-els--try-variable-prefix-list
+                                    collect (concat (symbol-name lang) 
postfix)))
+
+                     ;; merge font lock settings
+                     (setq font-lock-settings
+                           (typst-ts-els--try-get-font-lock-settings lang 
prefixes))
+                     (setq treesit-font-lock-settings
+                           (append treesit-font-lock-settings
+                                   font-lock-settings))
+
+                     ;; merge indent rules
+                     (setq indentation-rules
+                           (typst-ts-els--try-get-indentation-rules lang 
prefixes))
+                     (setq treesit-simple-indent-rules
+                           (append treesit-simple-indent-rules
+                                   indentation-rules))
+                     
+                     ;; merge ts feature lists
+                     (setq ts-feature-list
+                           (typst-ts-els--try-get-ts-feature-list
+                            (intern (concat (symbol-name lang) postfix))))
+                     (setq treesit-font-lock-feature-list
+                           (typst-ts-els--merge-features
+                            treesit-font-lock-feature-list
+                            ts-feature-list))
+                     ))))
+            (add-to-list typst-ts-els--include-languages lang))
+          )))))
+
+(defun typst-ts-embedding-lang-settings-test ()
+  "Test typst-ts-embedding-lang-settings."
+  (dolist (setting-entry typst-ts-embedding-lang-settings)
+    (message "Testing %s ..." (car setting-entry))
+    (typst-ts-els-merge-settings (cdr setting-entry)))
+  (message "No problem found!"))
+
+
+(provide 'typst-ts-embedding-lang-settings)
+
+;;; typst-ts-embedding-lang-settings.el ends here
diff --git a/typst-ts-mode.el b/typst-ts-mode.el
index a41ace7372..baa09cdd5b 100644
--- a/typst-ts-mode.el
+++ b/typst-ts-mode.el
@@ -33,6 +33,8 @@
 (require 'compile)
 (require 'outline)
 
+(require 'typst-ts-embedding-lang-settings)
+
 (defgroup typst-ts nil
   "Tree Sitter enabled Typst Writing."
   :prefix "typst-ts"
@@ -61,6 +63,11 @@ The compile options will be passed to the end of
   :type 'string
   :group 'typst-ts)
 
+(defcustom typst-ts-mode-highlight-raw-block t
+  "Enable highlighting raw block."
+  :type 'boolean
+  :group 'typst-ts)
+
 (defvar typst-ts-mode-before-compile-hook nil
   "Hook runs after compile.")
 
@@ -133,6 +140,12 @@ is eliminated."
   :set-after '(typst-ts-markup-header-same-height)
   :group 'typst-ts-faces)
 
+(defcustom typst-ts-mode-raw-block-lang-list
+  '(python)  ; TODO
+  "Raw Block Lang List."
+  :type '(list symbol)
+  :group 'typst-ts)
+
 ;; Face 
=========================================================================
 (defface typst-ts-watch-modeline-indicator-face
   '((t :inherit (underline bold)))
@@ -323,7 +336,6 @@ is eliminated."
 
 
 ;; 
==============================================================================
-
 ;; TODO typst has three modes (namely 'markup', 'code' and 'math')
 ;; Currently only add common settings to syntax table
 (defvar typst-ts-mode-syntax-table
@@ -370,7 +382,8 @@ is eliminated."
      (raw_blck
       "```" @typst-ts-markup-rawblock-indicator-face
       (ident) :? @typst-ts-markup-rawspan-lang-face
-      (blob) @typst-ts-markup-rawblock-blob-face ;; TODO use function to 
fontify region
+      ;; NOTE let embedded language fontify blob
+      ;; (blob) @typst-ts-markup-rawblock-blob-face 
       "```" @typst-ts-markup-rawblock-indicator-face)
      (label) @typst-ts-markup-label-face ;; TODO more precise highlight 
(upstream)
      (ref) @typst-ts-markup-reference-face)
@@ -457,6 +470,12 @@ is eliminated."
      (attach ["^" "_"] @font-lock-operator-face)
      (align) @font-lock-operator-face)))
 
+(defconst typst-ts-mode-font-lock-feature-list
+  '((comment common)
+    (markup-basic code-basic math-basic)
+    (markup-standard code-standard math-standard)
+    (markup-extended code-extended math-extended)))
+
 (defconst typst-ts-mode--bracket-node-types
   '("block" "content" "group")
   "Bracket node types.")
@@ -879,7 +898,7 @@ TODO lack of documentation."
               (parent-node-type (treesit-node-type parent-node)))
     (cond
      ((or (equal cur-node-type "parbreak")
-          (eq (point) (point-max)))
+          (eobp))
       (when-let* ((cur-line-bol
                    (save-excursion
                      (back-to-indentation)
@@ -942,6 +961,35 @@ TODO lack of documentation."
     (define-key map (kbd "TAB") #'typst-ts-mode-cycle)
     map))
 
+(defun typst-ts-mode--language-at-point (pos)
+  "Get the treesit language should be used at POS.
+See `treesit-language-at-point-function'."
+  (let ((lang
+         (when-let* ((cur-node (treesit-node-at pos 'typst))
+                     ((equal (treesit-node-type cur-node) "blob"))
+                     (parent-node (treesit-node-parent cur-node))
+                     ((equal (treesit-node-type
+                              (treesit-node-parent cur-node)) "raw_blck"))
+                     (lang-node  ; (indent)
+                      (treesit-node-prev-sibling cur-node)))
+           (intern (treesit-node-text lang-node)))))
+    (if lang lang 'typst)))
+
+(defun typst-ts-mode--treesit-range-rules (langs)
+  ;; from vimscript-ts-mode.el
+  "Create range captures for LANGS."
+  (cl-loop for lang in langs
+           when (treesit-ready-p lang)
+           nconc
+           (treesit-range-rules
+            :host 'typst
+            :embed lang
+            :local t
+            `((raw_blck
+               lang: (_) @_lang
+               (blob) @capture
+               (:equal @_lang ,(symbol-name lang)))))))
+
 ;;;###autoload
 (define-derived-mode typst-ts-mode text-mode "Typst"
   "Major mode for editing Typst, powered by tree-sitter."
@@ -950,7 +998,12 @@ TODO lack of documentation."
 
   (unless (treesit-ready-p 'typst)
     (error "Tree-sitter for Typst isn't available"))
-  (treesit-parser-create 'typst)
+
+  (let ((parser (treesit-parser-create 'typst)))
+    (when typst-ts-mode-highlight-raw-block
+      (treesit-parser-add-notifier
+       parser
+       'typst-ts-els-include-dynamically)))
 
   ;; Comments.
   (typst-ts-mode-comment-setup)
@@ -968,11 +1021,7 @@ TODO lack of documentation."
   (setq-local treesit-font-lock-level 4)
   (setq-local treesit-font-lock-settings
               (apply #'treesit-font-lock-rules typst-ts-mode-font-lock-rules))
-  (setq-local treesit-font-lock-feature-list
-              '((comment common)
-                (markup-basic code-basic math-basic)
-                (markup-standard code-standard math-standard)
-                (markup-extended code-extended math-extended)))
+  (setq-local treesit-font-lock-feature-list 
typst-ts-mode-font-lock-feature-list)
 
   ;; Indentation
   (setq-local treesit-simple-indent-rules typst-ts-mode--indent-rules)
@@ -989,6 +1038,15 @@ TODO lack of documentation."
                       typst-ts-mode-executable-location
                       (file-name-nondirectory buffer-file-name)
                       typst-ts-mode-compile-options))
+  
+  ;; use parser notifier to add font lock dynamically
+  ;; TODO merge indentation rule
+  ;; TODO merge font lock feature list
+
+  (setq-local treesit-language-at-point-function
+              'typst-ts-mode--language-at-point)
+  (setq-local treesit-range-settings
+              (typst-ts-mode--treesit-range-rules 
typst-ts-mode-raw-block-lang-list))
 
   ;; Outline
   (setq-local outline-regexp typst-ts-mode-outline-regexp)

Reply via email to