summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDenis Zubarev <dvzubarev@yandex.ru>2023-11-11 04:55:44 +0300
committerEli Zaretskii <eliz@gnu.org>2023-12-30 13:15:07 +0200
commit4696869d3d57e0b28b0450515f2f3322607d845e (patch)
tree4f1d1ef2ff3190afa89b0601b4947db5a542254d
parent530315287254da2e6b0767ad343fa55f79be8536 (diff)
downloademacs-4696869d3d57e0b28b0450515f2f3322607d845e.tar.gz
Improve syntax highlighting for python-ts-mode
Fix fontification of strings inside of f-strings interpolation, e.g. for f"beg {'nested'}" - 'nested' was not fontified as string. Do not override the face of builtin functions (all, bytes etc.) with the function call face. Add missing assignment expressions (:= *=). Fontify built-ins (dict,list,etc.) as types when they are used in type hints. Highlight union types (type1|type2). Highlight base class names in the class definition. Fontify class patterns in case statements. Highlight the second argument as a type in isinstance/issubclass call. Highlight dotted decorator names. * lisp/progmodes/python.el (python--treesit-keywords): Add compound keyword "is not". (python--treesit-builtin-types): New variable that stores all python built-in types. (python--treesit-type-regex): New variable. Regex matches if text is either built-in type or text starts with capital letter. (python--treesit-builtins): Extract built-in types to other variable. (python--treesit-fontify-string): fix f-string interpolation. Enable interpolation highlighting only if string-interpolation is presented on the enabled levels of treesit-font-lock-feature-list. (python--treesit-fontify-string-interpolation): Remove function. (python--treesit-fontify-union-types): Fontify nested union types. (python--treesit-fontify-union-types-strict): Fontify nested union types, only if type identifier matches against python--treesit-type-regex. (python--treesit-fontify-dotted-decorator): Fontify all parts of dotted decorator name. (python--treesit-settings): Change/add rules. (Bug#67061) * test/lisp/progmodes/python-tests.el (python-ts-tests-with-temp-buffer): Function for setting up test buffer. (python-ts-mode-compound-keywords-face) (python-ts-mode-named-assignement-face-1) (python-ts-mode-assignement-face-2) (python-ts-mode-nested-types-face-1) (python-ts-mode-union-types-face-1) (python-ts-mode-union-types-face-2) (python-ts-mode-types-face-1) (python-ts-mode-types-face-2) (python-ts-mode-types-face-3) (python-ts-mode-isinstance-type-face-1) (python-ts-mode-isinstance-type-face-2) (python-ts-mode-isinstance-type-face-3) (python-ts-mode-superclass-type-face) (python-ts-mode-class-patterns-face) (python-ts-mode-dotted-decorator-face-1) (python-ts-mode-dotted-decorator-face-2) (python-ts-mode-builtin-call-face) (python-ts-mode-interpolation-nested-string) (python-ts-mode-disabled-string-interpolation) (python-ts-mode-interpolation-doc-string): Add tests.
-rw-r--r--lisp/progmodes/python.el240
-rw-r--r--test/lisp/progmodes/python-tests.el302
2 files changed, 480 insertions, 62 deletions
diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el
index d7250148fad..f5f89ad552c 100644
--- a/lisp/progmodes/python.el
+++ b/lisp/progmodes/python.el
@@ -969,19 +969,30 @@ It makes underscores and dots word constituent chars.")
"raise" "return" "try" "while" "with" "yield"
;; These are technically operators, but we fontify them as
;; keywords.
- "and" "in" "is" "not" "or" "not in"))
+ "and" "in" "is" "not" "or" "not in" "is not"))
+
+(defvar python--treesit-builtin-types
+ '("int" "float" "complex" "bool" "list" "tuple" "range" "str"
+ "bytes" "bytearray" "memoryview" "set" "frozenset" "dict"))
+
+(defvar python--treesit-type-regex
+ (rx-to-string `(seq bol (or
+ ,@python--treesit-builtin-types
+ (seq (? "_") (any "A-Z") (+ (any "a-zA-Z_0-9"))))
+ eol)))
(defvar python--treesit-builtins
- '("abs" "all" "any" "ascii" "bin" "bool" "breakpoint" "bytearray"
- "bytes" "callable" "chr" "classmethod" "compile" "complex"
- "delattr" "dict" "dir" "divmod" "enumerate" "eval" "exec"
- "filter" "float" "format" "frozenset" "getattr" "globals"
- "hasattr" "hash" "help" "hex" "id" "input" "int" "isinstance"
- "issubclass" "iter" "len" "list" "locals" "map" "max"
- "memoryview" "min" "next" "object" "oct" "open" "ord" "pow"
- "print" "property" "range" "repr" "reversed" "round" "set"
- "setattr" "slice" "sorted" "staticmethod" "str" "sum" "super"
- "tuple" "type" "vars" "zip" "__import__"))
+ (append python--treesit-builtin-types
+ '("abs" "all" "any" "ascii" "bin" "breakpoint"
+ "callable" "chr" "classmethod" "compile"
+ "delattr" "dir" "divmod" "enumerate" "eval" "exec"
+ "filter" "format" "getattr" "globals"
+ "hasattr" "hash" "help" "hex" "id" "input" "isinstance"
+ "issubclass" "iter" "len" "locals" "map" "max"
+ "min" "next" "object" "oct" "open" "ord" "pow"
+ "print" "property" "repr" "reversed" "round"
+ "setattr" "slice" "sorted" "staticmethod" "sum" "super"
+ "type" "vars" "zip" "__import__")))
(defvar python--treesit-constants
'("Ellipsis" "False" "None" "NotImplemented" "True" "__debug__"
@@ -1032,9 +1043,7 @@ NODE is the string node. Do not fontify the initial f for
f-strings. OVERRIDE is the override flag described in
`treesit-font-lock-rules'. START and END mark the region to be
fontified."
- (let* ((string-beg (treesit-node-start node))
- (string-end (treesit-node-end node))
- (maybe-expression (treesit-node-parent node))
+ (let* ((maybe-expression (treesit-node-parent node))
(grandparent (treesit-node-parent
(treesit-node-parent
maybe-expression)))
@@ -1062,28 +1071,92 @@ fontified."
(equal (treesit-node-type maybe-expression)
"expression_statement"))
'font-lock-doc-face
- 'font-lock-string-face)))
- ;; Don't highlight string prefixes like f/r/b.
- (save-excursion
- (goto-char string-beg)
- (when (re-search-forward "[\"']" string-end t)
- (setq string-beg (match-beginning 0))))
- (treesit-fontify-with-override
- string-beg string-end face override start end)))
-
-(defun python--treesit-fontify-string-interpolation
- (node _ start end &rest _)
- "Fontify string interpolation.
-NODE is the string node. Do not fontify the initial f for
-f-strings. START and END mark the region to be
+ 'font-lock-string-face))
+
+ (ignore-interpolation (not
+ (seq-some
+ (lambda (feats) (memq 'string-interpolation feats))
+ (seq-take treesit-font-lock-feature-list treesit-font-lock-level))))
+ ;; If interpolation is enabled, highlight only
+ ;; string_start/string_content/string_end children. Do not
+ ;; touch interpolation node that can occur inside of the
+ ;; string.
+ (string-nodes (if ignore-interpolation
+ (list node)
+ (treesit-filter-child
+ node
+ (lambda (ch) (member (treesit-node-type ch)
+ '("string_start"
+ "string_content"
+ "string_end")))
+ t))))
+
+ (dolist (string-node string-nodes)
+ (let ((string-beg (treesit-node-start string-node))
+ (string-end (treesit-node-end string-node)))
+ (when (or ignore-interpolation
+ (equal (treesit-node-type string-node) "string_start"))
+ ;; Don't highlight string prefixes like f/r/b.
+ (save-excursion
+ (goto-char string-beg)
+ (when (re-search-forward "[\"']" string-end t)
+ (setq string-beg (match-beginning 0)))))
+
+ (treesit-fontify-with-override
+ string-beg string-end face override start end)))))
+
+(defun python--treesit-fontify-union-types (node override start end &optional type-regex &rest _)
+ "Fontify nested union types in the type hints.
+For examlpe, Lvl1 | Lvl2[Lvl3[Lvl4[Lvl5 | None]], Lvl2]. This
+structure is represented via nesting binary_operator and
+subscript nodes. This function iterates over all levels and
+highlight identifier nodes. If TYPE-REGEX is not nil fontify type
+identifier only if it matches against TYPE-REGEX. NODE is the
+binary_operator node. OVERRIDE is the override flag described in
+`treesit-font-lock-rules'. START and END mark the region to be
+fontified."
+ (dolist (child (treesit-node-children node t))
+ (let (font-node)
+ (pcase (treesit-node-type child)
+ ((or "identifier" "none")
+ (setq font-node child))
+ ("attribute"
+ (when-let ((type-node (treesit-node-child-by-field-name child "attribute")))
+ (setq font-node type-node)))
+ ((or "binary_operator" "subscript")
+ (python--treesit-fontify-union-types child override start end type-regex)))
+
+ (when (and font-node
+ (or (null type-regex)
+ (let ((case-fold-search nil))
+ (string-match-p type-regex (treesit-node-text font-node)))))
+ (treesit-fontify-with-override
+ (treesit-node-start font-node) (treesit-node-end font-node)
+ 'font-lock-type-face override start end)))))
+
+(defun python--treesit-fontify-union-types-strict (node override start end &rest _)
+ "Fontify nested union types.
+Same as `python--treesit-fontify-union-types' but type identifier
+should match against `python--treesit-type-regex'. For NODE,
+OVERRIDE, START and END description see
+`python--treesit-fontify-union-types'."
+ (python--treesit-fontify-union-types node override start end python--treesit-type-regex))
+
+(defun python--treesit-fontify-dotted-decorator (node override start end &rest _)
+ "Fontify dotted decorators.
+For example @pytes.mark.skip. Iterate over all nested attribute
+nodes and highlight identifier nodes. NODE is the first attribute
+node. OVERRIDE is the override flag described in
+`treesit-font-lock-rules'. START and END mark the region to be
fontified."
- ;; This is kind of a hack, it basically removes the face applied by
- ;; the string feature, so that following features can apply their
- ;; face.
- (let ((n-start (treesit-node-start node))
- (n-end (treesit-node-end node)))
- (remove-text-properties
- (max start n-start) (min end n-end) '(face))))
+ (dolist (child (treesit-node-children node t))
+ (pcase (treesit-node-type child)
+ ("identifier"
+ (treesit-fontify-with-override
+ (treesit-node-start child) (treesit-node-end child)
+ 'font-lock-type-face override start end))
+ ("attribute"
+ (python--treesit-fontify-dotted-decorator child override start end)))))
(defvar python--treesit-settings
(treesit-font-lock-rules
@@ -1093,14 +1166,9 @@ fontified."
:feature 'string
:language 'python
- '((string) @python--treesit-fontify-string)
+ '((string) @python--treesit-fontify-string
+ (interpolation ["{" "}"] @font-lock-misc-punctuation-face))
- ;; HACK: This feature must come after the string feature and before
- ;; other features. Maybe we should make string-interpolation an
- ;; option rather than a feature.
- :feature 'string-interpolation
- :language 'python
- '((interpolation) @python--treesit-fontify-string-interpolation)
:feature 'keyword
:language 'python
@@ -1117,12 +1185,6 @@ fontified."
(parameters (identifier) @font-lock-variable-name-face)
(parameters (default_parameter name: (identifier) @font-lock-variable-name-face)))
- :feature 'function
- :language 'python
- '((call function: (identifier) @font-lock-function-call-face)
- (call function: (attribute
- attribute: (identifier) @font-lock-function-call-face)))
-
:feature 'builtin
:language 'python
`(((identifier) @font-lock-builtin-face
@@ -1133,6 +1195,19 @@ fontified."
eol))
@font-lock-builtin-face)))
+ :feature 'decorator
+ :language 'python
+ '((decorator "@" @font-lock-type-face)
+ (decorator (call function: (identifier) @font-lock-type-face))
+ (decorator (identifier) @font-lock-type-face)
+ (decorator [(attribute) (call (attribute))] @python--treesit-fontify-dotted-decorator))
+
+ :feature 'function
+ :language 'python
+ '((call function: (identifier) @font-lock-function-call-face)
+ (call function: (attribute
+ attribute: (identifier) @font-lock-function-call-face)))
+
:feature 'constant
:language 'python
'([(true) (false) (none)] @font-lock-constant-face)
@@ -1144,30 +1219,71 @@ fontified."
@font-lock-variable-name-face)
(assignment left: (attribute
attribute: (identifier)
- @font-lock-property-use-face))
- (pattern_list (identifier)
+ @font-lock-variable-name-face))
+ (augmented_assignment left: (identifier)
+ @font-lock-variable-name-face)
+ (named_expression name: (identifier)
+ @font-lock-variable-name-face)
+ (pattern_list [(identifier)
+ (list_splat_pattern (identifier))]
@font-lock-variable-name-face)
- (tuple_pattern (identifier)
+ (tuple_pattern [(identifier)
+ (list_splat_pattern (identifier))]
@font-lock-variable-name-face)
- (list_pattern (identifier)
- @font-lock-variable-name-face)
- (list_splat_pattern (identifier)
- @font-lock-variable-name-face))
+ (list_pattern [(identifier)
+ (list_splat_pattern (identifier))]
+ @font-lock-variable-name-face))
- :feature 'decorator
- :language 'python
- '((decorator "@" @font-lock-type-face)
- (decorator (call function: (identifier) @font-lock-type-face))
- (decorator (identifier) @font-lock-type-face))
:feature 'type
:language 'python
+ ;; Override built-in faces when dict/list are used for type hints.
+ :override t
`(((identifier) @font-lock-type-face
(:match ,(rx-to-string
`(seq bol (or ,@python--treesit-exceptions)
- eol))
+ eol))
@font-lock-type-face))
- (type (identifier) @font-lock-type-face))
+ (type [(identifier) (none)] @font-lock-type-face)
+ (type (attribute attribute: (identifier) @font-lock-type-face))
+ ;; We don't want to highlight a package of the type
+ ;; (e.g. pack.ClassName). So explicitly exclude patterns with
+ ;; attribute, since we handle dotted type name in the previous
+ ;; rule. The following rule handle
+ ;; generic_type/list/tuple/splat_type nodes.
+ (type (_ !attribute [[(identifier) (none)] @font-lock-type-face
+ (attribute attribute: (identifier) @font-lock-type-face) ]))
+ ;; collections.abc.Iterator[T] case.
+ (type (subscript (attribute attribute: (identifier) @font-lock-type-face)))
+ ;; Nested optional type hints, e.g. val: Lvl1 | Lvl2[Lvl3[Lvl4]].
+ (type (binary_operator) @python--treesit-fontify-union-types)
+ ;;class Type(Base1, Sequence[T]).
+ (class_definition
+ superclasses:
+ (argument_list [(identifier) @font-lock-type-face
+ (attribute attribute: (identifier) @font-lock-type-face)
+ (subscript (identifier) @font-lock-type-face)
+ (subscript (attribute attribute: (identifier) @font-lock-type-face))]))
+
+ ;; Patern matching: case [str(), pack0.Type0()]. Take only the
+ ;; last identifier.
+ (class_pattern (dotted_name (identifier) @font-lock-type-face :anchor))
+
+ ;; Highlight the second argument as a type in isinstance/issubclass.
+ ((call function: (identifier) @func-name
+ (argument_list :anchor (_)
+ [(identifier) @font-lock-type-face
+ (attribute attribute: (identifier) @font-lock-type-face)
+ (tuple (identifier) @font-lock-type-face)
+ (tuple (attribute attribute: (identifier) @font-lock-type-face))]
+ (:match ,python--treesit-type-regex @font-lock-type-face)))
+ (:match "^is\\(?:instance\\|subclass\\)$" @func-name))
+
+ ;; isinstance(t, int|float).
+ ((call function: (identifier) @func-name
+ (argument_list :anchor (_)
+ (binary_operator) @python--treesit-fontify-union-types-strict))
+ (:match "^is\\(?:instance\\|subclass\\)$" @func-name)))
:feature 'escape-sequence
:language 'python
diff --git a/test/lisp/progmodes/python-tests.el b/test/lisp/progmodes/python-tests.el
index e1b4c0a74c0..59287970ca0 100644
--- a/test/lisp/progmodes/python-tests.el
+++ b/test/lisp/progmodes/python-tests.el
@@ -7122,6 +7122,308 @@ buffer with overlapping strings."
"Unused import a.b.c (unused-import)"
"W0611: Unused import a.b.c (unused-import)"))))))
+;;; python-ts-mode font-lock tests
+
+(defmacro python-ts-tests-with-temp-buffer (contents &rest body)
+ "Create a `python-ts-mode' enabled temp buffer with CONTENTS.
+BODY is code to be executed within the temp buffer. Point is
+always located at the beginning of buffer."
+ (declare (indent 1) (debug t))
+ `(with-temp-buffer
+ (skip-unless (treesit-ready-p 'python))
+ (require 'python)
+ (let ((python-indent-guess-indent-offset nil))
+ (python-ts-mode)
+ (setopt treesit-font-lock-level 3)
+ (insert ,contents)
+ (font-lock-ensure)
+ (goto-char (point-min))
+ ,@body)))
+
+(ert-deftest python-ts-mode-compound-keywords-face ()
+ (dolist (test '("is not" "not in"))
+ (python-ts-tests-with-temp-buffer
+ (concat "t " test " t")
+ (forward-to-word 1)
+ (should (eq (face-at-point) font-lock-keyword-face))
+ (forward-to-word 1)
+ (should (eq (face-at-point) font-lock-keyword-face)))))
+
+(ert-deftest python-ts-mode-named-assignement-face-1 ()
+ (python-ts-tests-with-temp-buffer
+ "var := 3"
+ (should (eq (face-at-point) font-lock-variable-name-face))))
+
+(ert-deftest python-ts-mode-assignement-face-2 ()
+ (python-ts-tests-with-temp-buffer
+ "var, *rest = call()"
+ (dolist (test '("var" "rest"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-variable-name-face))))
+
+ (python-ts-tests-with-temp-buffer
+ "def func(*args):"
+ (dolist (test '("args"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-variable-name-face))))))
+
+(ert-deftest python-ts-mode-nested-types-face-1 ()
+ (python-ts-tests-with-temp-buffer
+ "def func(v:dict[ list[ tuple[str] ], int | None] | None):"
+ (dolist (test '("dict" "list" "tuple" "str" "int" "None" "None"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))))
+
+(ert-deftest python-ts-mode-union-types-face-1 ()
+ (python-ts-tests-with-temp-buffer
+ "def f(val: tuple[tuple, list[Lvl1 | Lvl2[Lvl3[Lvl4[Lvl5 | None]], Lvl2]]]):"
+ (dolist (test '("tuple" "tuple" "list" "Lvl1" "Lvl2" "Lvl3" "Lvl4" "Lvl5" "None" "Lvl2"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))))
+
+(ert-deftest python-ts-mode-union-types-face-2 ()
+ (python-ts-tests-with-temp-buffer
+ "def f(val: Type0 | Type1[Type2, pack0.Type3] | pack1.pack2.Type4 | None):"
+ (dolist (test '("Type0" "Type1" "Type2" "Type3" "Type4" "None"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))
+
+ (goto-char (point-min))
+ (dolist (test '("pack0" "pack1" "pack2"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-type-face))))))
+
+(ert-deftest python-ts-mode-types-face-1 ()
+ (python-ts-tests-with-temp-buffer
+ "def f(val: Callable[[Type0], (Type1, Type2)]):"
+ (dolist (test '("Callable" "Type0" "Type1" "Type2"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))))
+
+(ert-deftest python-ts-mode-types-face-2 ()
+ (python-ts-tests-with-temp-buffer
+ "def annot3(val:pack0.Type0)->pack1.pack2.pack3.Type1:"
+ (dolist (test '("Type0" "Type1"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))
+ (goto-char (point-min))
+ (dolist (test '("pack0" "pack1" "pack2" "pack3"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-type-face))))))
+
+(ert-deftest python-ts-mode-types-face-3 ()
+ (python-ts-tests-with-temp-buffer
+ "def annot3(val:collections.abc.Iterator[Type0]):"
+ (dolist (test '("Iterator" "Type0"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))
+ (goto-char (point-min))
+ (dolist (test '("collections" "abc"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-type-face))))))
+
+(ert-deftest python-ts-mode-isinstance-type-face-1 ()
+ (python-ts-tests-with-temp-buffer
+ "isinstance(var1, pkg.Type0)
+ isinstance(var2, (str, dict, Type1, type(None)))
+ isinstance(var3, my_type())"
+
+ (dolist (test '("var1" "pkg" "var2" "type" "None" "var3" "my_type"))
+ (let ((case-fold-search nil))
+ (search-forward test))
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-type-face))))
+
+ (goto-char (point-min))
+ (dolist (test '("Type0" "str" "dict" "Type1"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))))
+
+(ert-deftest python-ts-mode-isinstance-type-face-2 ()
+ (python-ts-tests-with-temp-buffer
+ "issubclass(mytype, int|list|collections.abc.Iterable)"
+ (dolist (test '("int" "list" "Iterable"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))))
+
+(ert-deftest python-ts-mode-isinstance-type-face-3 ()
+ (python-ts-tests-with-temp-buffer
+ "issubclass(mytype, typevar1)
+ isinstance(mytype, (Type1, typevar2, tuple, abc.Coll))
+ isinstance(mytype, pkg0.Type2|self.typevar3|typevar4)"
+
+ (dolist (test '("typevar1" "typevar2" "pkg0" "self" "typevar3" "typevar4"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-type-face))))
+
+ (goto-char (point-min))
+ (dolist (test '("Type1" "tuple" "Coll" "Type2"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))))
+
+(ert-deftest python-ts-mode-superclass-type-face ()
+ (python-ts-tests-with-temp-buffer
+ "class Temp(Base1, pack0.Base2, Sequence[T1, T2]):"
+
+ (dolist (test '("Base1" "Base2" "Sequence" "T1" "T2"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))
+
+ (goto-char (point-min))
+ (dolist (test '("pack0"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-type-face))))))
+
+(ert-deftest python-ts-mode-class-patterns-face ()
+ (python-ts-tests-with-temp-buffer
+ "match tt:
+ case str():
+ pass
+ case [Type0() | bytes(b) | pack0.pack1.Type1()]:
+ pass
+ case {'i': int(i), 'f': float() as f}:
+ pass"
+
+ (dolist (test '("str" "Type0" "bytes" "Type1" "int" "float"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))
+
+ (goto-char (point-min))
+ (dolist (test '("pack0" "pack1"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-type-face))))))
+
+(ert-deftest python-ts-mode-dotted-decorator-face-1 ()
+ (python-ts-tests-with-temp-buffer
+ "@pytest.mark.skip
+ @pytest.mark.skip(reason='msg')
+ def test():"
+
+ (dolist (test '("pytest" "mark" "skip" "pytest" "mark" "skip"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))))
+
+(ert-deftest python-ts-mode-dotted-decorator-face-2 ()
+ (python-ts-tests-with-temp-buffer
+ "@pytest.mark.skip(reason='msg')
+ def test():"
+
+ (setopt treesit-font-lock-level 4)
+ (dolist (test '("pytest" "mark" "skip"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-type-face)))))
+
+(ert-deftest python-ts-mode-builtin-call-face ()
+ (python-ts-tests-with-temp-buffer
+ "all()"
+ ;; enable 'function' feature from 4th level
+ (setopt treesit-font-lock-level 4)
+ (should (eq (face-at-point) font-lock-builtin-face))))
+
+(ert-deftest python-ts-mode-interpolation-nested-string ()
+ (python-ts-tests-with-temp-buffer
+ "t = f\"beg {True + 'string'}\""
+
+ (search-forward "True")
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-constant-face))
+
+ (goto-char (point-min))
+ (dolist (test '("f" "{" "+" "}"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-string-face))))
+
+
+ (goto-char (point-min))
+ (dolist (test '("beg" "'string'" "\""))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-string-face)))))
+
+(ert-deftest python-ts-mode-level-fontification-wo-interpolation ()
+ (python-ts-tests-with-temp-buffer
+ "t = f\"beg {True + var}\""
+
+ (setopt treesit-font-lock-level 2)
+ (search-forward "f")
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-string-face)))
+
+ (dolist (test '("\"" "beg" "{" "True" "var" "}" "\""))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-string-face)))))
+
+(ert-deftest python-ts-mode-disabled-string-interpolation ()
+ (python-ts-tests-with-temp-buffer
+ "t = f\"beg {True + var}\""
+
+ (unwind-protect
+ (progn
+ (setf (nth 2 treesit-font-lock-feature-list)
+ (remq 'string-interpolation (nth 2 treesit-font-lock-feature-list)))
+ (setopt treesit-font-lock-level 3)
+
+ (search-forward "f")
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-string-face)))
+
+ (dolist (test '("\"" "beg" "{" "True" "var" "}" "\""))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-string-face))))
+
+ (setf (nth 2 treesit-font-lock-feature-list)
+ (append (nth 2 treesit-font-lock-feature-list) '(string-interpolation))))))
+
+(ert-deftest python-ts-mode-interpolation-doc-string ()
+ (python-ts-tests-with-temp-buffer
+ "f\"\"\"beg {'s1' + True + 's2'} end\"\"\""
+
+ (search-forward "True")
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-constant-face))
+
+ (goto-char (point-min))
+ (dolist (test '("f" "{" "+" "}"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (not (eq (face-at-point) font-lock-string-face))))
+
+ (goto-char (point-min))
+ (dolist (test '("\"\"\"" "beg" "end" "\"\"\""))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-doc-face)))
+
+ (goto-char (point-min))
+ (dolist (test '("'s1'" "'s2'"))
+ (search-forward test)
+ (goto-char (match-beginning 0))
+ (should (eq (face-at-point) font-lock-string-face)))))
+
(provide 'python-tests)
;;; python-tests.el ends here