>From 9c2271065aa5408544fb09fe386de3f357a1d3e6 Mon Sep 17 00:00:00 2001 From: Jackson Ray Hamilton
Date: Sun, 17 Feb 2019 00:38:01 -0800 Subject: [PATCH 06/19] Add basic JSX font-locking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Font-lock JSX from the beginning of the buffer to the end. Tends to break temporarily when editing lines, because the parser doesnât yet look backwards to determine if the end of a tag in the current range starts before the range. This also re-breaks some tests fixed by previous commits, as we begin to take a different direction in our parsing code, looking for JSX, rather than for non-JSX. The parsing code will eventually provide information for indentation again. * lisp/progmodes/js.el (js--dotted-captured-name-re) (js-jsx--disambiguate-beginning-of-tag) (js-jsx--disambiguate-end-of-tag, js-jsx--disambiguate-syntax): Remove. (js-jsx--font-lock-keywords): New variable. (js--font-lock-keywords-3): Add JSX matchers. (js-jsx--match-tag-name, js-jsx--match-attribute-name): New functions. (js-jsx--syntax-propertize-tag): New function to aid in JSX font-locking and eventually indentation. (js-jsx--text-properties): New variable. (js-syntax-propertize): Propertize JSX properly using syntax-propertize-rules. --- lisp/progmodes/js.el | 216 +++++++++++++++++++++++++++++---------------------- 1 file changed, 124 insertions(+), 92 deletions(-) diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 4404ea04a0..1319fa1939 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -82,10 +82,6 @@ js--dotted-name-re (concat js--name-re "\\(?:\\." js--name-re "\\)*") "Regexp matching a dot-separated sequence of JavaScript names.") -(defconst js--dotted-captured-name-re - (concat "\\(" js--name-re "\\)\\(?:\\." js--name-re "\\)*") - "Like `js--dotted-name-re', but capture the first name.") - (defconst js--cpp-name-re js--name-re "Regexp matching a C preprocessor name.") @@ -1498,6 +1494,33 @@ js--variable-decl-matcher ;; Matcher always "fails" nil) +(defconst js-jsx--font-lock-keywords + `((js-jsx--match-tag-name 0 font-lock-function-name-face t) + (js-jsx--match-attribute-name 0 font-lock-variable-name-face t)) + "JSX font lock faces.") + +(defun js-jsx--match-tag-name (limit) + "Match JSXBoundaryElement names, until LIMIT." + (when js-jsx-syntax + (let ((pos (next-single-char-property-change (point) 'js-jsx-tag-name nil limit)) + value) + (when (and pos (> pos (point))) + (goto-char pos) + (or (and (setq value (get-text-property pos 'js-jsx-tag-name)) + (progn (set-match-data value) t)) + (js-jsx--match-tag-name limit)))))) + +(defun js-jsx--match-attribute-name (limit) + "Match JSXAttribute names, until LIMIT." + (when js-jsx-syntax + (let ((pos (next-single-char-property-change (point) 'js-jsx-attribute-name nil limit)) + value) + (when (and pos (> pos (point))) + (goto-char pos) + (or (and (setq value (get-text-property pos 'js-jsx-attribute-name)) + (progn (set-match-data value) t)) + (js-jsx--match-attribute-name limit)))))) + (defconst js--font-lock-keywords-3 `( ;; This goes before keywords-2 so it gets used preferentially @@ -1609,7 +1632,10 @@ js--font-lock-keywords-3 (forward-symbol -1) (end-of-line)) '(end-of-line) - '(0 font-lock-variable-name-face)))) + '(0 font-lock-variable-name-face))) + + ;; jsx (when enabled) + ,@js-jsx--font-lock-keywords) "Level three font lock for `js-mode'.") (defun js--inside-pitem-p (pitem) @@ -1743,94 +1769,100 @@ js--unary-keyword-p "Check if STRING is a unary operator keyword in JavaScript." (string-match-p js--unary-keyword-re string)) -(defun js-jsx--disambiguate-beginning-of-tag () - "Parse enough to determine if a JSX tag starts here. -Disambiguate JSX from equality operators by testing for syntax -only valid as JSX." - ;; ââ¦â - a JSXClosingElement. - ;; â<>â - a JSXOpeningFragment. - (if (memq (char-after) '(?\/ ?\>)) t - (save-excursion - (skip-chars-forward " \t\n") - (and - (looking-at js--dotted-captured-name-re) - ;; Donât match code like âif (i < await foo)â - (not (js--unary-keyword-p (match-string 1))) - (progn - (goto-char (match-end 0)) - (skip-chars-forward " \t\n") - (or - ;; â>â, â/>â - tag enders. - ;; â{â - a JSXExpressionContainer. - (memq (char-after) '(?\> ?\/ ?\{)) - ;; Check if a JSXAttribute follows. - (looking-at js--name-start-re))))))) - -(defun js-jsx--disambiguate-end-of-tag () - "Parse enough to determine if a JSX tag ends here. -Disambiguate JSX from equality operators by testing for syntax -only valid as JSX, or extremely unlikely except as JSX." - (save-excursion - (backward-char) - ;; ââ¦/>â - a self-closing JSXOpeningElement. - ;; â>â - a JSXClosingFragment. - (if (= (char-before) ?/) t - (let (last-tag-or-attr-name last-non-unary-p) - (catch 'match - (while t - (skip-chars-backward " \t\n") - ;; Check if the end of a JSXAttribute value or - ;; JSXExpressionContainer almost certainly precedes. - ;; The only valid JS this misses is - ;; - {} > foo - ;; - "bar" > foo - ;; which is no great loss, IMHO⦠- (if (memq (char-before) '(?\} ?\" ?\' ?\`)) (throw 'match t) - (if (and last-tag-or-attr-name last-non-unary-p - ;; â<â, ââ - tag starters. - (memq (char-before) '(?\< ?\/))) - ;; Leftmost name parsed was the name of a - ;; JSXOpeningElement. - (throw 'match t)) - ;; Technically the dotted name could span multiple - ;; lines, but dear God WHY?! Also, match greedily to - ;; ensure the entire name is valid. - (if (looking-back js--dotted-captured-name-re (point-at-bol) t) - (if (and (setq last-non-unary-p (not (js--unary-keyword-p (match-string 1)))) - last-tag-or-attr-name) - ;; Valid (non-unary) name followed rightwards by - ;; another name (any will do, including - ;; keywords) is invalid JS, but valid JSX. - (throw 'match t) - ;; Remember match and skip backwards over it when - ;; it is the first matched name or the N+1th - ;; matched unary name (unary names on the left are - ;; still ambiguously JS or JSX, so keep parsing to - ;; disambiguate). - (setq last-tag-or-attr-name (match-string 1)) - (goto-char (match-beginning 0))) - ;; Nothing else to look for; give up parsing. - (throw 'match nil))))))))) - -(defun js-jsx--disambiguate-syntax (start end) - "Figure out which â<â and â>â chars (from START to END) arenât JSX. - -Later, this info prevents âsgml-â functions from treating some -â<â and â>â chars as parts of tokens of SGML tags â a good thing, -since they are serving their usual function as some JS equality -operator or arrow function, instead." - (goto-char start) - (while (re-search-forward "[<>]" end t) - (unless (if (eq (char-before) ?<) (js-jsx--disambiguate-beginning-of-tag) - (js-jsx--disambiguate-end-of-tag)) - ;; Inform sgml- functions that this >, >=, >>>, <, <=, <<<, or - ;; => token is punctuation (and not an open or close parenthesis - ;; as per usual in sgml-mode). - (put-text-property (1- (point)) (point) 'syntax-table '(1))))) +(defun js-jsx--syntax-propertize-tag (end) + "Determine if a JSXBoundaryElement is before END and propertize it. +Disambiguate JSX from inequality operators and arrow functions by +testing for syntax only valid as JSX." + (let ((tag-beg (1- (point))) tag-end (type 'open) + name-beg name-match-data unambiguous + forward-sexp-function) ; Use Lisp version. + (catch 'stop + (while (and (< (point) end) + (progn (skip-chars-forward " \t\n" end) + (< (point) end))) + (cond + ((= (char-after) ?>) + (forward-char) + (setq unambiguous t + tag-end (point)) + (throw 'stop nil)) + ;; Handle a JSXSpreadChild (â