branch: elpa/typst-ts-mode commit 0e4894ffcde658037529ac038882bd76e8603b78 Merge: 2a3fb8e577 075b450e44 Author: meowking <mr.meowk...@tutamail.com> Commit: meowking <mr.meowk...@tutamail.com>
merge: fix: maarkup-extended font feature --- typst-ts-core.el | 73 ++++++++++++++++++++ typst-ts-editing.el | 195 ++++++++++++++++++++++++++++++++++------------------ typst-ts-mode.el | 155 ++++++++++++++++++++--------------------- 3 files changed, 275 insertions(+), 148 deletions(-) diff --git a/typst-ts-core.el b/typst-ts-core.el index fab4ba780f..b5ab461e23 100644 --- a/typst-ts-core.el +++ b/typst-ts-core.el @@ -32,6 +32,15 @@ :type 'natnum :group 'typst-ts) +(defconst typst-ts-core--container-node-types + ;; '_math_group' here is because `treesit-parent-until' doesn't hanlde node type alias well + ;; TODO file a bug + '("block" "content" "group" "math" "_math_group")) + +(defconst typst-ts-mode--container-node-types-regexp + (regexp-opt typst-ts-core--container-node-types) + "Container node types regexp.") + (defun typst-ts-core-column-at-pos (point) "Get the column at position POINT." (save-excursion @@ -45,6 +54,14 @@ (back-to-indentation) (point))) +(defun typst-ts-core-line-bol-nonwhite-pos (&optional pos) + "POS." + (save-excursion + (when pos + (goto-char pos)) + (back-to-indentation) + (point))) + (defun typst-ts-core-get-node-at-bol-nonwhite (&optional pos) "Get node at the first non-whitespace character at line beginning. If POS is given, operate on the line that POS locates at." @@ -60,6 +77,62 @@ POS. May return nil." (treesit-node-parent (typst-ts-core-get-node-at-bol-nonwhite pos))) +(defun typst-ts-core-for-lines-covered-by-node (node fn) + "Execute FN on all lines covered by NODE. +Currently the effect of FN shouldn't change line number." + (let ((ns (treesit-node-start node)) + ;; use line number not position since when editing, position of node end + ;; changes, but the information is not updated + (ne-line-num (line-number-at-pos (treesit-node-end node)))) + (save-excursion + (goto-char ns) + (while (< (line-number-at-pos) ne-line-num) + (funcall fn) + (forward-line 1)) + ;; in case the last line is the last line of buffer, we separate this + ;; operation from while loop + (funcall fn)))) + + +;; Emacs 29 doesn't support string type PRED, so this function is created for +;; convenience +(defun typst-ts-core-parent-util-type (node type &optional include-node same-context) + "See `treesit-parent-until'. +TYPE is an regexp expression for matching types. +SAME-CONTEXT: whether the parent should be in the current context with NODE. +The following example means parent item node is in a different context with +`hi' text node +- #[ +hi +] +NODE TYPE INCLUDE-NODE see `treesit-parent-until'." + (let ((matched-node + (treesit-parent-until + node + (lambda (node) + (let ((node-type (treesit-node-type node))) + (or (and same-context + (string-match-p + typst-ts-mode--container-node-types-regexp node-type)) + (string-match-p type node-type)))) + include-node))) + (when (and matched-node + (string-match-p type (treesit-node-type matched-node))) + matched-node))) + +(defun typst-ts-core-prev-sibling-ignore-types (node types) + "Find previous sibling node ignoring nodes whose type matches TYPES. +NODE: current node. +TYPES is an regexp expression." + (let* ((prev-node (treesit-node-prev-sibling node)) + (prev-node-type (treesit-node-type prev-node))) + (while (and prev-node-type + (string-match-p types prev-node-type)) + (setq + prev-node (treesit-node-prev-sibling prev-node) + prev-node-type (treesit-node-type prev-node))) + prev-node)) + (defun typst-ts-core-node-get (node instructions) "Get things from NODE by INSTRUCTIONS. It's a copy of Emacs 30's `treesit-node-get' function." diff --git a/typst-ts-editing.el b/typst-ts-editing.el index d757421513..e1c7edd6be 100644 --- a/typst-ts-editing.el +++ b/typst-ts-editing.el @@ -104,36 +104,80 @@ Using ARG argument will ignore the context and it will insert a heading instead. "Handle RET depends on condition. When prefix ARG is non-nil, call global return function." (interactive "P") - (let (execute-result node) + (let (execute-result) (unless current-prefix-arg (setq execute-result (catch 'execute-result - (when-let* ((cur-pos (point)) - (cur-node (treesit-node-at cur-pos)) - (cur-node-type (treesit-node-type cur-node)) - (parent-node (treesit-node-parent cur-node)) ; could be nil - (parent-node-type (treesit-node-type parent-node))) + (let* ((cur-pos (point)) + (cur-node (treesit-node-at cur-pos)) + (cur-node-type (treesit-node-type cur-node)) + (parent-node (treesit-node-parent cur-node)) ; could be nil + (parent-node-type (treesit-node-type parent-node)) + node) ;; (message "%s %s" cur-node parent-node) (cond - (arg (throw 'execute-result 'default)) ;; on item node end ((and (eolp) - (setq node (typst-ts-core-get-parent-of-node-at-bol-nonwhite)) - (equal (treesit-node-type node) "item")) - (if (> (treesit-node-child-count node) 1) - (typst-ts-mode-insert--item node) - ;; no text means delete the item on current line: (item -) instead of (item - (text)) - (beginning-of-line) - (kill-line) - (indent-according-to-mode)) + (setq node (typst-ts-core-parent-util-type + (typst-ts-core-get-parent-of-node-at-bol-nonwhite) + "item" t t))) + (let* ((item-node node) + (has-children (treesit-node-child item-node 1)) + (next-line-node + (typst-ts-core-get-parent-of-node-at-bol-nonwhite + (save-excursion + (forward-line 1) + (point)))) + (next-line-top-node ; get container type or `item' type node + (typst-ts-core-parent-util-type + next-line-node + (regexp-opt + (append + typst-ts-core--container-node-types + '("item"))) + t))) + (if has-children + ;; example: + ;; - #[| <- return + ;; ] + (if (and next-line-top-node + ;; end of buffer situation (or next line is the end + ;; line (and no newline character)) + (not (equal + (line-number-at-pos + (save-excursion + (forward-line 1) + (point))) + (line-number-at-pos (point-max))))) + (call-interactively #'newline) + (typst-ts-mode-insert--item item-node)) + ;; no text means delete the item on current line: (item -) + (beginning-of-line) + (kill-line) + ;; whether the previous line is in an item + (let* ((prev-line-item-node + (typst-ts-core-parent-util-type + (typst-ts-core-get-parent-of-node-at-bol-nonwhite + (save-excursion + (forward-line -1) + (point))) + "item" t t))) + (if prev-line-item-node + (progn + ;; sometimes there is no newlines characters at the EOL + (ignore-errors + (kill-line)) + (forward-line -1) + (end-of-line) + (call-interactively #'newline)) + (indent-according-to-mode))))) (throw 'execute-result 'success)) ))))) ;; execute default action if not successful (unless (eq execute-result 'success) ;; we only need to look for global keybinding, see `(elisp) Active Keymaps' - (let ((global-ret-function - (global-key-binding (kbd "RET")))) + (let ((global-ret-function (global-key-binding (kbd "RET")))) (if (not current-prefix-arg) (call-interactively global-ret-function) (if (yes-or-no-p @@ -188,10 +232,25 @@ When there is no section it will insert a heading below point." (insert heading-level " ") (indent-according-to-mode))) +(defun typst-ts-editing--indent-item-node-lines (node offset) + (let ((item-node-min-column + (typst-ts-core-column-at-pos + (typst-ts-core-line-bol-nonwhite-pos + (treesit-node-start node))))) + (if (< (+ item-node-min-column offset) 0) + (setq offset (- item-node-min-column))) + (typst-ts-core-for-lines-covered-by-node + node + (lambda () + (indent-line-to + (+ (typst-ts-core-column-at-pos + (typst-ts-core-line-bol-nonwhite-pos)) + offset)))))) + (defun typst-ts-mode-cycle (&optional _arg) "Cycle." (interactive "P") - (let (execute-result) + (let (execute-result node) (setq execute-result ;; plz manually throw `\'success' to `execute-result' @@ -199,6 +258,10 @@ When there is no section it will insert a heading below point." (when-let* ((cur-pos (point)) (cur-node (treesit-node-at cur-pos)) (cur-node-type (treesit-node-type cur-node)) + (cur-line-nonwhite-bol-node + (typst-ts-core-get-node-at-bol-nonwhite)) + (cur-line-nonwhite-bol-node-type + (treesit-node-type cur-line-nonwhite-bol-node)) (parent-node (treesit-node-parent cur-node)) ; could be nil (parent-node-type (treesit-node-type parent-node))) (cond @@ -206,56 +269,52 @@ When there is no section it will insert a heading below point." (insert-tab) (throw 'execute-result 'success)) - ((or (equal cur-node-type "parbreak") - (equal parent-node-type "item") - ;; please turn on whitespace-mode to test the following conditions - (eobp) - (eq (point) (1- (point-max)))) - (when-let* ((cur-line-bol - (save-excursion - (back-to-indentation) - (point))) - (prev-nonwhite-pos (save-excursion - (goto-char cur-line-bol) - (skip-chars-backward "\s\r\n\t") - (1- (point)))) - ((and (not (eq prev-nonwhite-pos 0)) ; first line - (not (eq ; has previous sibling - (line-number-at-pos prev-nonwhite-pos) - (line-number-at-pos (point)))))) - (prev-nonwhite-line-node - (treesit-node-at prev-nonwhite-pos)) - (prev-nonwhite-line-bol - ;; TODO typst-ts-core-get-node-bol - (save-excursion - (goto-char prev-nonwhite-pos) - (back-to-indentation) - (point))) - (prev-nonwhite-line-top-node - (treesit-node-parent - (treesit-node-at prev-nonwhite-line-bol))) - (cur-line-bol-column (typst-ts-core-column-at-pos cur-line-bol)) - (prev-nonwhite-line-bol-column - (typst-ts-core-column-at-pos prev-nonwhite-line-bol))) - (cond - ;; 1. el - ;; 2. psy| <- can toggle indent - ((and - (equal (treesit-node-type prev-nonwhite-line-top-node) "item") - ;; previous nonwhite-line ending is not '\' character - (not (equal (treesit-node-type prev-nonwhite-line-node) "linebreak"))) - ;; TODO cycle all its children - (let (point) - (if (not (eq cur-line-bol-column prev-nonwhite-line-bol-column)) - (progn - (setq point (point)) - (indent-line-to prev-nonwhite-line-bol-column) - (goto-char (- point typst-ts-mode-indent-offset))) - (setq point (point)) - (indent-line-to (+ typst-ts-mode-indent-offset - prev-nonwhite-line-bol-column)) - (goto-char (+ typst-ts-mode-indent-offset point))) - (throw 'execute-result 'success)))))) + + ((setq node + (typst-ts-core-parent-util-type + cur-line-nonwhite-bol-node "item" t t)) + (let* ((cur-item-node node) + (prev-significant-node + (typst-ts-core-prev-sibling-ignore-types + cur-item-node + "parbreak")) + (prev-significant-node-type + (treesit-node-type prev-significant-node)) + prev-item-node) + + (if (equal prev-significant-node-type "item") + (setq prev-item-node prev-significant-node) + (if (equal + "item" + (treesit-node-type + (treesit-node-parent prev-significant-node))) + (setq prev-item-node (treesit-node-parent + prev-significant-node)))) + + ;; (message "%s, %s" cur-item-node prev-item-node) + + (unless prev-item-node + (throw 'execute-result 'default)) + + (let* ((cur-item-node-start-column + (typst-ts-core-column-at-pos + (treesit-node-start cur-item-node))) + (prev-item-node-start-column + (typst-ts-core-column-at-pos + (treesit-node-start prev-item-node))) + (offset + (- cur-item-node-start-column + prev-item-node-start-column))) + (if (>= offset typst-ts-mode-indent-offset) + (typst-ts-editing--indent-item-node-lines + cur-item-node + (- (+ offset typst-ts-mode-indent-offset))) + (typst-ts-editing--indent-item-node-lines + cur-item-node + (- typst-ts-mode-indent-offset (abs offset))))) + + (throw 'execute-result 'success))) + (t nil))))) ;; execute default action if not successful (unless (eq execute-result 'success) diff --git a/typst-ts-mode.el b/typst-ts-mode.el index b292cf6732..0f9f1a8edd 100644 --- a/typst-ts-mode.el +++ b/typst-ts-mode.el @@ -388,55 +388,6 @@ If you want to customize the rules, please customize the same name variable (markup-standard code-standard math-standard) (markup-extended code-extended math-extended))) -(defconst typst-ts-mode--container-node-types-regexp - ;; '_math_group' here is because `treesit-parent-until' doesn't hanlde node type alias well - ;; TODO file a bug - (regexp-opt '("block" "content" "group" "math" "_math_group")) - "Container node types regexp.") - -(defun typst-ts-mode--identation-item-linebreak (_node _parent bol) - "Where the current line is underneath a item with linebreak as ending. -Ignore whitespaces. -BOL: beginning of the current line. -See `treesit-simple-indent-rules'." - (when-let* ((prev-nonwhite-pos (save-excursion - (goto-char bol) - (skip-chars-backward "\s\r\n\t") - (1- (point)))) - ((and (not (eq prev-nonwhite-pos 0)) ; first line - (not (eq ; has previous sibling - (line-number-at-pos prev-nonwhite-pos) - (line-number-at-pos (point)))))) - (prev-nonwhite-line-node - (treesit-node-at prev-nonwhite-pos)) - ((equal (treesit-node-type prev-nonwhite-line-node) "linebreak")) - - (prev-nonwhite-line-heading-node - (save-excursion - (goto-char prev-nonwhite-pos) - (back-to-indentation) - (treesit-node-at (point)))) - ((equal (treesit-node-type prev-nonwhite-line-heading-node) "-")) - - (prev-nonwhite-line-top-node (treesit-node-parent - prev-nonwhite-line-heading-node))) - (equal (treesit-node-type prev-nonwhite-line-top-node) "item"))) - -(defun typst-ts-mode--indentation-item-linebreak-get-pos (_node _parent bol) - "Get the previous item indentation position. -See `typst-ts-mode--identation-item-linebreak'. -BOL: beginning of the current line. -This function is used instead of `parent-bol' is to make sure in the situation -where current point is point-max with no newline character at ending can also -work well. Example: -1. el \\$ - 2. psy \\$ - | <- insert cursor should be here." - (save-excursion - (goto-char bol) - (skip-chars-backward "\s\r\n\t") - (back-to-indentation) - (point))) (defun typst-ts-mode-indent--grand-parent-bol (_node parent _bol) "Return the grand parent beginning of line position. @@ -503,34 +454,32 @@ NODE, PARENT and BOL see `treesit-simple-indent-rules'." ;; .split(" ") ((n-p-gp "." "field" nil) parent-bol typst-ts-mode-indent-offset) - ;; multi-line item - ;; - foo - ;; bar - ((and (parent-is "item") - (lambda (node &rest parent bol) - (treesit-node-prev-sibling node))) - (lambda (node &rest parent bol) - (treesit-node-start (treesit-node-prev-sibling node))) - 0) + ((parent-is "comment") prev-adaptive-prefix 0) ;; item - child item - ((and (node-is "item") (parent-is "item")) parent-bol typst-ts-mode-indent-offset) - - ;; item - previous nonwhite line is item type and the ending is a linebreak - (typst-ts-mode--identation-item-linebreak - typst-ts-mode--indentation-item-linebreak-get-pos typst-ts-mode-indent-offset) - - ;; item - item should follow its previous line item's indentation level + ((and (node-is "item") (parent-is "item")) parent-bol + typst-ts-mode-indent-offset) + + ;; multi-line item + ;; - #[hi] foo + ;; bar + ;; my try with `prev-adaptive-prefix' failed even after set the + ;; `adaptive-fill-regexp' + ((match nil "item" nil 2 nil) + typst-ts-mode--indentation-multiline-item-get-anchor 0) + + ;; item - new item content should follow its previous line's indentation + ;; level ((and no-node - (lambda (node parent &rest _) - (save-excursion - (forward-line -1) - (back-to-indentation) - (string= "item" (treesit-node-type - (treesit-node-parent - (treesit-node-at (point)))))))) - prev-line - 0) + typst-ts-mode--indentation-prev-line-is-item-p + ;; not in container + ;; example: + ;; - hi + ;; hi #[ + ;; - hello | <- return + ;; ] + (not (n-p-gp nil "parbreak" ,typst-ts-mode--container-node-types-regexp))) + typst-ts-mode--indentation-multiline-item-get-anchor_ 0) ;; raw block ;; whether normally or in insertion, the current node is always nil... @@ -560,6 +509,32 @@ NODE, PARENT and BOL see `treesit-simple-indent-rules'." (catch-all prev-line 0))) "Tree-sitter indent rules for `typst-ts-mode'.") +(defun typst-ts-mode--indentation-multiline-item-get-anchor (_node parent _bol) + "Return the start of second child of PARENT." + (treesit-node-start (treesit-node-child parent 1))) + +(defun typst-ts-mode--indentation-multiline-item-get-anchor_ (_node _parent _bol) + "Return the start of second child of the current item. +This function is meant to be used when user hits a return key." + (treesit-node-start + (treesit-node-child + (typst-ts-core-parent-util-type + (treesit-node-at + (save-excursion + (forward-line -1) + (back-to-indentation) + (point))) + "item" t) + 1))) + +(defun typst-ts-mode--indentation-prev-line-is-item-p (_node _parent _bol) + (save-excursion + (forward-line -1) + (back-to-indentation) + (typst-ts-core-parent-util-type + (treesit-node-at (point)) + "item" t))) + (defun typst-ts-mode-comment-setup() "Setup comment related stuffs for `typst-ts-mode'." @@ -580,6 +555,7 @@ NODE, PARENT and BOL see `treesit-simple-indent-rules'." (grandparent-node (treesit-node-parent parent-node))) (and (equal (treesit-node-type node) "ident") (equal (treesit-node-type parent-node) "call") + (equal (treesit-node-field-name parent-node) "pattern") (equal (treesit-node-type grandparent-node) "let")))) (defun typst-ts-mode--imenu-name-function (node) @@ -587,9 +563,13 @@ NODE, PARENT and BOL see `treesit-simple-indent-rules'." (treesit-node-text node)) ;; outline-minor-mode -(defconst typst-ts-mode-outline-regexp "^[[:space:]]*\\(=+\\) " +(defconst typst-ts-mode-outline-regexp "^[[:space:]]*\\(=+\\)" "Regexp identifying Typst header.") +(defconst typst-ts-mode-outline-heading-alist + '(("=" . 1) ("==" . 2) ("===" . 3) ("====" . 4) ("=====" . 5) ("======" . 6)) + "See `outline-heading-alist'.") + (defun typst-ts-mode-outline-level () "Return the level of the heading at point." (save-excursion @@ -695,6 +675,17 @@ typst tree sitter grammar (at least %s)!" (current-time-string min-time)) (file-name-nondirectory buffer-file-name) typst-ts-compile-options)))) + ;; Although without enabling `outline-minor-mode' also works, enabling it + ;; provides outline ellipsis (if you use `set-display-table-slot' to set) + (outline-minor-mode t) + + ;; FIXME + ;; necessary for + ;; `typst-ts-mode-cycle'(`typst-ts-editing--indent-item-node-lines') + ;; since it calculate offset based on character + ;; (maybe also some indentation rules) + (indent-tabs-mode -1) + (typst-ts-mode-check-grammar-version)) ;;;###autoload @@ -736,6 +727,14 @@ typst tree sitter grammar (at least %s)!" (current-time-string min-time)) ;; Imenu (setq-local treesit-simple-imenu-settings + ;; Here we uses a trick. In the docs of + ;; `treesit-simple-imenu-settings', the second parameter should + ;; be a regexp string. However, it can be anything that + ;; the PRED in `treesit-thing-settings' can be + ;; For emacs 30, there are some restriction (second param must be + ;; regexp string) when you use default settings for outline + ;; (outline from imenu) see `treesit-major-mode-setup' and + ;; `treesit-outline-predicate' `(("Functions" typst-ts-mode--imenu-function-defintion-p nil typst-ts-mode--imenu-name-function) ("Headings" "^heading$" nil typst-ts-mode--imenu-name-function))) @@ -748,17 +747,13 @@ typst tree sitter grammar (at least %s)!" (current-time-string min-time)) ;; (setq-local treesit-thing-settings ;; `((typst ()))) - ;; Outline (if nil ; (>= emacs-major-version 30) ;; FIXME maybe it's a upstream bug. Circle top-level section will cycle all the content below (setq treesit-outline-predicate (regexp-opt '("section" "source_file"))) (setq-local outline-regexp typst-ts-mode-outline-regexp) (setq-local outline-level #'typst-ts-mode-outline-level)) - ;; Although without enabling `outline-minor-mode' also works, enabling it - ;; provides outline ellipsis - ;; TODO add it to after-hook - (outline-minor-mode t) + (setq-local outline-heading-alist typst-ts-mode-outline-heading-alist) (treesit-major-mode-setup)