Skip to content

Commit

Permalink
Fix some unexpected behaviours with tags automatic synchronization.
Browse files Browse the repository at this point in the history
  • Loading branch information
llemaitre19 committed Jan 26, 2024
1 parent ceffda7 commit 7d46eed
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 18 deletions.
56 changes: 48 additions & 8 deletions jtsx.el
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ See `treesit-font-lock-level' for more informations."

(defvar jtsx-ts-indent-rules)

(defvar jtsx-last-buffer-chars-modifed-tick 0)

(defun jtsx-save-buffer-chars-modified-tick ()
"Save the returned value of `buffer-chars-modified-tick' function."
(setq jtsx-last-buffer-chars-modifed-tick (buffer-chars-modified-tick)))

(defun jtsx-command-modified-buffer-p ()
"Check if last command has modified the buffer."
(< jtsx-last-buffer-chars-modifed-tick (buffer-chars-modified-tick)))

(defun jtsx-node-jsx-context-p (node)
"Check if NODE inside JSX context."
(member (treesit-node-type node) '("jsx_expression"
Expand Down Expand Up @@ -332,6 +342,32 @@ Point can be in the opening or closing."
(jtsx-rename-jsx-element-tag-at-point new-name "close_tag"))))
(message "No JSX element to rename."))))

(defun jtsx-treesit-syntax-error-in-descendants-p (node)
"Check recursively if there are errors reported by treesit in NODE descendants."
(let ((children-nodes (treesit-node-children node))
(index 0))
(catch 'syntax-error-found
(while (< index (length children-nodes))
(let ((child-node (nth index children-nodes)))
(when (or (equal (treesit-node-type child-node) "ERROR")
;; Can happen in the TSX tree-sitter parser in that situation:
;; <>
;; <div
;; <p>
;; </p>
;; </>
;; In that case <p> is recognized as a type argument and the missing ">"
;; is registered by the parser as having a start and end at the same position.
(and (equal (treesit-node-type child-node) ">")
(eq (treesit-node-start child-node) (treesit-node-end child-node)))
(jtsx-treesit-syntax-error-in-descendants-p child-node))
(throw 'syntax-error-found t))
(setq index (1+ index)))))))

(defun jtsx-treesit-syntax-error-in-ancestors-p (node)
"Check recursively if there are errors reported by treesit in NODE ancestors."
(jtsx-enclosing-jsx-node node '("ERROR")))

(defun jtsx-jsx-element-tag-name (node)
"Return the NODE tag name."
(if-let (identifier-node (treesit-node-child-by-field-name node "name"))
Expand All @@ -349,12 +385,16 @@ Point can be in the opening or closing."

(defun jtsx-synchronize-jsx-element-tags ()
"Synchronize jsx element tags depending on the cursor position."
(when jtsx-enable-jsx-element-tags-auto-sync
(when (and jtsx-enable-jsx-element-tags-auto-sync (jtsx-command-modified-buffer-p))
(let* ((node (treesit-node-at (point)))
(parent-node (treesit-node-parent node))
(parent-node-type (treesit-node-type parent-node)))
(when (and (member (treesit-node-type node) '("identifier" ">" "<"))
(member parent-node-type jtsx-jsx-ts-element-tag-keys))
(member parent-node-type jtsx-jsx-ts-element-tag-keys)
;; Downstream syntax must be clean to prevent unexpected behaviours.
;; e.g.
(not (jtsx-treesit-syntax-error-in-descendants-p parent-node))
(not (jtsx-treesit-syntax-error-in-ancestors-p parent-node)))
(let* ((element-node (treesit-node-parent parent-node))
(opening-tag-node (treesit-node-child-by-field-name element-node "open_tag"))
(closing-tag-node (treesit-node-child-by-field-name element-node "close_tag")))
Expand All @@ -380,6 +420,7 @@ Point can be in the opening or closing."
closing-tag-name
opening-tag-name)))
(jtsx-rename-jsx-element-tag-at-point tag-name tag-to-rename))))))))))

(defun jtsx-first-child-jsx-node (node types &optional backward)
"Find the first child of NODE matching one of the TYPES.
If BACKWARD is not nil, start the search by the last children of NODE."
Expand Down Expand Up @@ -584,10 +625,6 @@ Step into sibling elements if possible."
(interactive)
(jtsx-move-jsx-element t t t))

(defun jtsx-treesit-syntax-error-p ()
"Check if there are errors reported by treesit."
(jtsx-enclosing-jsx-node (treesit-node-at (point)) '("ERROR")))

(defun jtsx-jsx-electric-closing-element (n)
"Insert `>' and the associated closing tag (`</xxx>') if expected.
N is a numeric prefix argument. If greater than 1, insert N times `>', but
Expand All @@ -608,9 +645,9 @@ N is a numeric prefix argument. If greater than 1, insert N times `>', but
;; We try to guess if auto adding the closing tag is expected or not. We assume that
;; before inserting the new opening tag, the code syntax was clean. So if after adding
;; the new opening tag we detect a syntax issue, that means a closing tag is expected.
;; This logic is quite basic, but no sure we can really do better with treesit
;; This logic is quite basic, but not sure we can really do better with treesit
;; informations about syntax issues.
(when (jtsx-treesit-syntax-error-p)
(when (jtsx-treesit-syntax-error-in-ancestors-p node)
(save-excursion (insert closing-tag)))))))))

(defun jtsx-inside-empty-inline-jsx-element-p ()
Expand Down Expand Up @@ -861,6 +898,9 @@ MODE, MODE-MAP, TS-LANG-KEY, INDENT-VAR-NAME variables allow customization
;; Add hook for electric new line
(add-hook 'post-self-insert-hook #'jtsx-electric-open-newline-between-jsx-element-tags-psif nil t)

;; Add hook to save the value of `jtsx-save-buffer-chars-modified-tick'
(add-hook 'pre-command-hook #'jtsx-save-buffer-chars-modified-tick nil t)

;; Add hook for automatic synchronization of jsx element tags.
;; `DEPTH' value explanation: some completion packages rely on `buffer-chars-modified-tick'
;; function to check if completion process is outdated. `jtsx-synchronize-jsx-element-tags' can
Expand Down
40 changes: 30 additions & 10 deletions tests/jtsx-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (rename-jsx-element-into-buffer content move-point #'jtsx-tsx-mode) result))))

;; TEST AUTOMATIC SYNCHRONIZATION OF JSX ELEMENT TAGS
(ert-deftest jtsx-test-resynchronize-jsx-element-tags-from-opening ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-from-opening ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 5)))
(content "(<AB>Hello</A>);")
Expand All @@ -491,7 +491,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-from-closing ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-from-closing ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 14)))
(content "(<AB>Hello</A>);")
Expand All @@ -501,7 +501,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-from-empty-opening ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-from-empty-opening ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 3)))
(content "(<>Hello</A>);")
Expand All @@ -511,7 +511,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-from-empty-closing ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-from-empty-closing ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 12)))
(content "(<A>Hello</>);")
Expand All @@ -521,7 +521,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-from-opening-with-attribute ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-from-opening-with-attribute ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 4)))
(content "(<A show>Hello</>);")
Expand All @@ -531,7 +531,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-from-closing-with-attribute ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-from-closing-with-attribute ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 21)))
(content "(<AB show>Hello</ABC>);")
Expand All @@ -541,7 +541,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-from-empty-opening-with-attribute ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-from-empty-opening-with-attribute ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 3)))
(content "(< show>Hello</ABC>);")
Expand All @@ -551,7 +551,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-from-empty-closing-with-attribute ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-from-empty-closing-with-attribute ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 18)))
(content "(<AB show>Hello</>);")
Expand All @@ -561,7 +561,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-failed ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-failed ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 7)))
(content "(<AB>Hello</A>);")
Expand All @@ -571,7 +571,7 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-resynchronize-jsx-element-tags-disabled ()
(ert-deftest jtsx-test-synchronize-jsx-element-tags-disabled ()
(let ((jtsx-enable-jsx-element-tags-auto-sync nil)
(move-point #'(lambda () (goto-char 5)))
(content "(<AB>Hello</A>);")
Expand All @@ -581,6 +581,26 @@ Turn this buffer in MODE mode if supplied or defaults to jtsx-tsx-mode."
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-synchronize-jsx-element-tags-when-created-new-element-aborted ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 15)))
(content "(\n <A> \n<B\n <C>\n </C>\n </A>\n);")
(result "(\n <A> \n<B\n <C>\n </C>\n </A>\n);"))
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-jsx-mode)
result))
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

(ert-deftest jtsx-test-synchronize-jsx-element-tags-when-created-new-element-aborted ()
(let ((jtsx-enable-jsx-element-tags-auto-sync t)
(move-point #'(lambda () (goto-char 15)))
(content "(\n <A> \n<B\n <C>\n </C>\n </A>\n);")
(result "(\n <A> \n<B\n <C>\n </C>\n </A>\n);"))
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-jsx-mode)
result))
(should (equal (synchronize-jsx-element-tags-into-buffer content move-point #'jtsx-tsx-mode)
result))))

;; TEST MOVE JSX OPENING OR CLOSING ELEMENT
(ert-deftest jtsx-test-move-jsx-opening-element-forward ()
(let ((move-point #'(lambda () (goto-char 0) (forward-line 2)))
Expand Down

0 comments on commit 7d46eed

Please sign in to comment.